Widgets y surfaces
Cómo dejar que el Designer coloque tu contenido, y abrir tus pantallas a él, sin depender nunca de él.
Los contratos viven en el core (App\Support\Widgets y
App\Support\Designer\*), y el módulo Designer los consume. Ese es todo el
truco: registras contra el core, y si el Designer no está instalado, no
pasa nada.
Nota Cada registro de esta página es inerte sin el Designer. Tu módulo debe funcionar igual con él desinstalado.
Áreas de widgets#
Una pantalla puede abrir una región para widgets sin saber quién la llena:
\App\Support\Widgets::declareArea('profile.sidebar');
Y en la vista:
{{ \App\Support\Widgets::render('profile.sidebar', ['member' => $member]) }}
Un documento del Designer publicado para esa área gana sobre la pila de widgets; despublicarlo devuelve la pila de inmediato.
Registrar un widget#
Un widget del Designer es un widget del core con una clave designer en
su descriptor. Sin esa clave sigue funcionando en las áreas heredadas y
simplemente no aparece en el editor.
Widgets::register(
'directory.latest',
static fn (): string => __('directory::widgets.latest'),
static function (array $context) {
$items = /* ... */;
return $items->isEmpty()
? '' // nada que mostrar, sin caja vacía
: 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'],
],
],
);
Las etiquetas siempre son callables: en el momento del boot tus archivos de idioma pueden no estar cargados todavía.
El $context que recibe tu renderer lleva settings (los valores del
schema con los defaults aplicados y los dynamic tags resueltos), item
cuando está dentro de un bucle, node para ids de ARIA estables, y lo que
sea que la surface declaró en data.
Surfaces#
Una surface es una pantalla tuya cuyo centro el Designer puede reemplazar:
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'),
]);
Y en tu vista:
@designer('directory.index', ['items' => $items])
@foreach ($items as $item)
@include('directory::partials.card', ['item' => $item])
@endforeach
@enddesigner
El cuerpo se captura en un buffer. Si existe un documento publicado para esa clave, el layout lo reemplaza; si no, el buffer se emite sin cambios. Sin el Designer instalado, la directiva devuelve el fallback y no toca nada.
Collections#
Una collection es una fuente de datos para el elemento de bucle:
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 visibilidad es responsabilidad de la collection, nunca de quien la usa. Aplica las mismas guardas que aplican tus propias pantallas. Un bucle armado por un administrador en el editor nunca debe revelar lo que tu pantalla ocultaría.
Dynamic tags, conditions y form actions#
La misma forma aplica a los otros tres registros: una clave, una etiqueta perezosa, el módulo, y un callable que hace el trabajo.
- Dynamic tags convierten un item en un valor para un control.
- Conditions deciden si un nodo se renderiza. Se evalúan en el servidor, así que el contenido protegido nunca llega al HTML.
- Form actions corren después de un envío válido. Una acción que lanza una excepción se reporta y el envío igual tiene éxito: el visitante no debe perder su mensaje porque un webhook de terceros esté caído.
Breadcrumbs#
Empuja, y el widget de breadcrumb dibuja:
\App\Support\Breadcrumbs::push(__('directory::labels.title'), route('directory.index'));
\App\Support\Breadcrumbs::push($item->title); // el último nivel no lleva URL