Ir para o conteúdo
Bondry

Widgets e surfaces

Deixando o Designer posicionar seu conteúdo, e abrindo suas telas para ele, sem nunca depender dele.

Os contratos vivem no core (App\Support\Widgets e App\Support\Designer\*), e o módulo Designer os consome. Esse é o truque inteiro: você registra contra o core, e se o Designer não estiver instalado, nada acontece.

Nota Todo registro nesta página é inerte sem o Designer. Seu módulo precisa funcionar identicamente com ele desinstalado.

Áreas de widget#

Uma tela pode abrir uma região para widgets sem saber quem vai preenchê-la:

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

E na view:

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

Um documento do Designer publicado para essa área vence a pilha de widgets; despublicar devolve a pilha imediatamente.

Registrando um widget#

Um widget do Designer é um widget do core com uma chave designer no descritor dele. Sem essa chave ele continua funcionando nas áreas legadas e simplesmente não aparece no editor.

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

        return $items->isEmpty()
            ? ''                                  // nada a mostrar, sem caixa vazia
            : 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'],
        ],
    ],
);

Labels são sempre callables: no momento do boot os seus arquivos de idioma podem ainda não estar carregados.

O $context que o seu renderizador recebe carrega settings (valores do schema com os padrões aplicados e as tags dinâmicas resolvidas), item quando está dentro de um loop, node para ids de ARIA estáveis, e o que quer que a surface tenha declarado em data.

Surfaces#

Uma surface é uma tela sua cujo meio o Designer pode substituir:

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'),
]);

E na sua view:

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

O corpo é capturado num buffer. Se existir um documento publicado para essa chave, o layout o substitui; se não, o buffer é emitido sem alteração. Sem o Designer instalado, a diretiva retorna o fallback e não toca em nada.

Collections#

Uma collection é uma fonte de dados para o elemento de loop:

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(),
]);

A visibilidade é responsabilidade da collection, nunca de quem a usa. Aplique as mesmas guardas que as suas próprias telas aplicam. Um loop montado por um administrador no editor nunca deve revelar o que a sua tela esconderia.

Tags dinâmicas, condições e form actions#

O mesmo formato se aplica aos outros três registries: uma chave, um label preguiçoso, o módulo, e um callable que faz o trabalho.

  • Tags dinâmicas transformam um item num valor para um controle.
  • Condições decidem se um nó é renderizado. São avaliadas no servidor, então conteúdo protegido nunca chega ao HTML.
  • Form actions rodam depois de um envio válido. Uma action que estoura é reportada e o envio ainda é bem-sucedido: o visitante não pode perder a mensagem dele porque um webhook de terceiro está fora do ar.

Breadcrumbs#

Empurre, e o widget de breadcrumb desenha:

PHP
\App\Support\Breadcrumbs::push(__('directory::labels.title'), route('directory.index'));
\App\Support\Breadcrumbs::push($item->title);   // o último nível não carrega URL