Skip to content

Les hooks & slots de vue

Un hook (ou slot) est un point d'insertion déclaré dans une vue : un package y greffe son contenu — un onglet sur la fiche produit, un bandeau dans le front — sans forker la vue.

Le problème

Le package « promotions » veut ajouter un onglet « Promotions » à la fiche produit du back-office. Surcharger toute la vue produit pour ça serait fragile (perdu à chaque évolution du cœur). Il faut un point d'ancrage que le cœur expose et que le package remplit.

Le principe

Le cœur (ou le thème) place un <x-hook name="…" :context="[…]"/> dans sa vue. Les packages enregistrent une vue partielle pour ce hook via le ViewHookRegistry. Au rendu, le composant interroge for($hook) et @include chaque vue enregistrée, triée par priorité croissante (à priorité égale, l'ordre d'enregistrement départage), en lui passant le contexte du hook.

Greffer une vue sur un hook

Dans le boot() du provider :

php
use Slab\Framework\Core\View\Contracts\ViewHookRegistry;

$hooks = $this->app->make(ViewHookRegistry::class);
$hooks->add('back.products.edit.tabs', 'promotion::back.products.tab', priority: 20);
$hooks->add('back.products.edit.sections', 'promotion::back.products.section');

La vue partielle est incluse (@include) avec le contexte passé au hook : ses variables — ici $product — sont directement disponibles. Ce n'est pas un composant Blade, donc pas de @props.

blade
{{-- promotion::back.products.tab --}}
<x-back.header.tab href="#promotions-section">{{ __('Promotions') }}</x-back.header.tab>

Un onglet contribué n'est qu'un lien : la section correspondante porte le data-tab qui lui répond.

blade
{{-- promotion::back.products.section --}}
<x-back.section data-tab="promotions-section" :title="__('Promotions')">
    {{-- … $product est disponible ici … --}}
</x-back.section>

Les slots du front

Le front expose des slots garantis, publiés par la classe Slab\Framework\Core\View\FrontSlotContract : groups() les rend groupés par zone d'écran (layout, header, catalog, cart, checkout, account, forms), names() à plat. Un thème qui se prétend complet doit les exposer ; les packages les remplissent de la même façon que les hooks du back-office.

SlotEmplacement
front.headdans <head> (méta, styles, balises tierces)
front.body.start / front.body.endouverture et fin de <body>
front.main.start / front.main.endouverture et fin du contenu principal
front.header.start / front.header.endentête, avant et après la navigation
front.product.summary.aftersous le prix (stock, réassurance, note d'avis)
front.product.form.fieldsdans le formulaire d'ajout au panier — les champs contribués sont postés avec lui
front.product.afterbas de fiche (cross-selling, avis, description longue)
front.category.filters.after / front.category.products.afterpage de catégorie
front.search.results.aftersous les résultats de la recherche instantanée
front.cart.item.aftersous une ligne du panier (contexte : la ligne)
front.cart.totals.after / front.cart.actionstotaux et bouton de commande
front.checkout.summaryprès du récapitulatif du tunnel
front.checkout.step.start / front.checkout.step.endouverture et fin de l'étape courante
front.account.linksrubriques du compte client
front.order.details.aftersous le détail d'une commande
front.login.form.end / front.register.form.end / front.address.form.endbas des formulaires
php
$this->app->make(ViewHookRegistry::class)->add('front.body.end', 'mon-package::tracking');

Back-office et e-mails

Le back-office expose sa propre grille, publiée par BackSlotContract : back.head, back.body.start / back.body.end, back.main.start / back.main.end, plus back.header.corner (près du menu utilisateur), back.header.actions (barre d'actions de l'écran) et back.menu.end (bas du menu latéral). Elle ne fait pas partie du contrat de thème front.

Les gabarits d'e-mail ouvrent mail.body.start et mail.body.end, présents dans tous les mails transactionnels — de quoi y poser des mentions légales ou un encart de campagne une fois pour toutes.

Le hook d'écran : cibler n'importe quelle page

Les slots ci-dessus nomment un emplacement de contenu. Pour cibler une page entière, il n'y a rien à déclarer : toute route nommée ouvre deux hooks, dérivés de son nom.

php
// Sur la page servie par la route `back.products.index` :
$hooks->add('back.products.index.start', 'mon-package::alerte-stock');
$hooks->add('back.products.index.end', 'mon-package::export');

// Et sur la fiche produit du thème :
$hooks->add('front.products.show.end', 'mon-package::avis');

C'est le point d'extension de vue le plus général du framework : un package atteint ainsi un écran du cœur, du thème ou d'un autre package, sans que celui-ci ait eu à le prévoir. Les vues contribuées reçoivent en contexte les paramètres résolus de la route — sur back.products.edit, $product est directement disponible.

Les deux positions (start, end) encadrent le contenu de l'écran ; elles sont posées une fois pour toutes dans les layouts (<x-screen-hook position="start"/>), y compris ceux de la page de connexion du back-office. Hors d'une route nommée (rendu d'un mail, d'un job), le hook ne cible aucun écran et ne rend rien.

Exposer un hook dans votre propre thème

Un thème déclare ses points d'ancrage avec le composant <x-hook> :

blade
<head>
    {{-- … --}}
    <x-hook name="front.head"/>
</head>

Disponibles sur tous les écrans d'édition

Chaque écran d'édition du back-office expose deux hooks :

  • back.<ressource>.edit.tabs — pour ajouter un onglet ;
  • back.<ressource>.edit.sections — pour ajouter une section sous le formulaire.

<ressource>areas, attributes, categories, currencies, customers, languages, order_statuses, products, taxes, tax_rules, team. Le contexte porte le modèle édité (ex. ['tax' => $tax] ; ['user' => $user] pour customers et team). Greffer un bloc sur l'écran des taxes :

php
$this->app->make(ViewHookRegistry::class)
    ->add('back.taxes.edit.sections', 'mon-package::taxes.bloc');

Ces écrans étant tous bâtis sur la charpente déclarée (voir ci-dessous), ils ajoutent une troisième famille : back.<ressource>.edit.tab.<clé>, rendue en fin de l'onglet de cette clé — le point où ajouter des champs à un onglet existant sans toucher à ses sections.

La charpente d'un écran : le FormLayoutRegistry

Le ViewHookRegistry ajoute du contenu à un point nommé ; le FormLayoutRegistry rend la structure de l'écran manipulable. Le cœur déclare ses onglets et ses sections par clé (dans Slab\Framework\Core\View\BackFormLayouts), l'écran les rend par <x-back.form-layout screen="…" :context="[…]"/> — plus rien n'est écrit en dur dans le blade. Tous les écrans d'édition du back-office fonctionnent ainsi, de la fiche produit à celle des taxes. La fiche produit, par exemple :

php
$layout->tab('back.products.edit', 'general', __('Général'), priority: 10);
$layout->section('back.products.edit', 'identity', 'back.products.sections.identity', 'general', priority: 10);
$layout->section('back.products.edit', 'prices', 'back.products.sections.prices', 'general', priority: 20);

Un package réorganise cet écran depuis l'extérieur : il déclare son onglet, y range ses sections, et déplace celles du cœur par leur clé.

php
use Slab\Framework\Core\View\Contracts\FormLayoutRegistry;

$layout = $this->app->make(FormLayoutRegistry::class);

$layout->tab('back.products.edit', 'pricing', __('Tarifs'), priority: 20);
$layout->section('back.products.edit', 'grids', 'mon-package::products.grids', 'pricing');

// La section « prices » du cœur rejoint l'onglet « Tarifs ».
$layout->moveSection('back.products.edit', 'prices', 'pricing');

Les règles qui gouvernent le rendu :

  • la clé fait foi — réenregistrer tab() ou section() avec une clé existante remplace la déclaration : c'est ainsi qu'on renomme ou réordonne un élément du cœur ;
  • ordre par priorité croissante, onglets comme sections ;
  • when — une Closure recevant le contexte de l'écran conditionne l'affichage d'un onglet ou d'une section (le cœur s'en sert pour n'afficher « Variantes » que sur un produit parent enregistré) ;
  • columns: false dispose les sections d'un onglet sur toute la largeur, plutôt qu'en colonnes ;
  • la barre d'onglets ne s'affiche que s'il y a de quoi naviguer — un écran à onglet unique reste tel quel ; elle apparaît dès qu'un second onglet est déclaré, ou qu'un package en contribue un au hook <screen>.tabs ;
  • un onglet sans section applicable est écarté — un onglet vide serait un lien vers du vide ; c'est aussi ce qui permet de vider un onglet du cœur en déplaçant ses sections ;
  • moveSection() sur une clé inconnue est sans effet : un package ne casse pas l'écran parce que le cœur a renommé une section — corollaire, un déplacement joué avant la déclaration de sa cible est perdu silencieusement. L'ordre d'enregistrement des providers ne garantissant rien, contribuez depuis $this->app->booted(…) dès que vous touchez à une déclaration du cœur.

Onglet, section ou charpente ?

Sur un écran bâti sur le FormLayoutRegistry, passez par lui : c'est le seul moyen d'ordonner vos blocs parmi ceux du cœur ou de les déplacer. Les hooks …edit.tabs / …edit.sections restent la voie pour contribuer un onglet sans connaître la charpente (la section contribuée porte alors son propre data-tab), et …edit.tab.<clé> pour ajouter des champs à la fin d'un onglet existant. Sur un écran simple, une section reste le moyen le plus direct. Sans contribution, ces hooks sont invisibles (aucun changement d'affichage).

Voir aussi

  • Les registres — le ViewHookRegistry en est un ; le DatatableExtensionRegistry en est le pendant pour les listings du back-office.
  • Créer un thème — exposer les slots front garantis.
  • Surcharger une vue — quand il n'y a pas (encore) de hook.