Aller au contenu
Bondry

Widgets et surfaces

Laisser le Designer placer votre contenu, et ouvrir vos écrans à lui, sans jamais en dépendre.

Les contrats vivent dans le core (App\Support\Widgets et App\Support\Designer\*), et le module Designer les consomme. C'est toute l'astuce : vous vous enregistrez contre le core, et si le Designer n'est pas installé, il ne se passe rien.

Remarque Chaque enregistrement de cette page est inerte sans le Designer. Votre module doit fonctionner à l'identique s'il est désinstallé.

Zones de widgets#

Un écran peut ouvrir une région pour des widgets sans savoir qui la remplit :

PHP
\App\Support\Widgets::declareArea('profile.sidebar');

Et dans la vue :

Blade
{{ \App\Support\Widgets::render('profile.sidebar', ['member' => $member]) }}

Un document Designer publié pour cette zone l'emporte sur la pile de widgets ; dépublier rend la pile immédiatement.

Enregistrer un widget#

Un widget Designer est un widget du core avec une clé designer dans son descripteur. Sans cette clé, il fonctionne quand même dans les zones legacy et n'apparaît simplement pas dans l'éditeur.

PHP
Widgets::register(
    'directory.latest',
    static fn (): string => __('directory::widgets.latest'),
    static function (array $context) {
        $items = /* ... */;

        return $items->isEmpty()
            ? ''                                  // rien à montrer, pas de boîte vide
            : view('directory::widgets.latest', ['items' => $items]);
    },
    [
        'areas' => ['profile.sidebar'],
        'cache_ttl' => 300,
        'designer' => [
            'category' => 'directory',
            'icon' => 'list',
            'keywords' => ['directory', 'latest'],
            'schema' => [
                'limit' => ['type' => 'number', 'label' => 'directory::widgets.limit', 'min' => 1, 'max' => 20],
            ],
            'defaults' => ['limit' => 5],
            'supports' => ['tag', 'conditions'],
        ],
    ],
);

Les libellés sont toujours des callables : au moment du boot, vos fichiers de langue ne sont peut-être pas encore chargés.

Le $context que reçoit votre renderer porte settings (les valeurs du schéma avec les defaults appliqués et les balises dynamiques résolues), item quand on est dans une boucle, node pour des ids ARIA stables, et tout ce que la surface a déclaré dans data.

Surfaces#

Une surface est un écran à vous dont le Designer peut remplacer le milieu :

PHP
Surfaces::register('directory.index', [
    'label' => static fn (): string => __('directory::admin.surface_index'),
    'module' => 'directory',
    'data' => static fn (array $ctx): array => ['items' => $ctx['items'] ?? null],
    'item' => 'directory.item',
    'preview' => static fn (): string => route('directory.index'),
]);

Et dans votre vue :

Blade
@designer('directory.index', ['items' => $items])
    @foreach ($items as $item)
        @include('directory::partials.card', ['item' => $item])
    @endforeach
@enddesigner

Le corps est capturé dans un buffer. Si un document publié existe pour cette clé, la mise en page le remplace ; sinon, le buffer est émis sans changement. Sans le Designer installé, la directive retourne le fallback et ne touche à rien.

Collections#

Une collection est une source de données pour l'élément de boucle :

PHP
Collections::register('directory.items', [
    'label' => static fn (): string => __('directory::admin.collection_items'),
    'module' => 'directory',
    'item' => 'directory.item',
    'orders' => ['created_at', 'title', 'random'],
    'filters' => [
        'category' => ['type' => 'select', 'label' => 'directory::admin.f_category', 'options' => []],
    ],
    'query' => static fn (array $args) => Item::query()->visibleToCurrentMember(),
]);

La visibilité est la responsabilité de la collection, jamais de celui qui l'utilise. Appliquez les mêmes guards que vos propres écrans appliquent. Une boucle construite par un administrateur dans l'éditeur ne doit jamais révéler ce que votre écran cacherait.

Balises dynamiques, conditions et actions de formulaire#

La même forme s'applique aux trois autres registres : une clé, un libellé paresseux, le module, et un callable qui fait le travail.

  • Les balises dynamiques transforment un item en valeur pour un contrôle.
  • Les conditions décident si un nœud se rend. Elles sont évaluées côté serveur, donc le contenu protégé n'atteint jamais le HTML.
  • Les actions de formulaire s'exécutent après une soumission valide. Une action qui lève une exception est signalée et la soumission réussit quand même : le visiteur ne doit pas perdre son message parce qu'un webhook tiers est en panne.

Fil d'Ariane#

Poussez, et le widget de fil d'Ariane dessine :

PHP
\App\Support\Breadcrumbs::push(__('directory::labels.title'), route('directory.index'));
\App\Support\Breadcrumbs::push($item->title);   // le dernier niveau ne porte aucune URL