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:
\App\Support\Widgets::declareArea('profile.sidebar');
E na view:
{{ \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.
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:
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:
@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:
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:
\App\Support\Breadcrumbs::push(__('directory::labels.title'), route('directory.index'));
\App\Support\Breadcrumbs::push($item->title); // o último nível não carrega URL