Snippets
Reusable rendering components that bridge JSON definitions and phtml templates. The escape hatch for anything more complex than a single form field — without falling back to hand-wired blocks.
Why snippets exist
Built-in form types (text, select, etc.) cover the 80% case. Snippets cover the 20% where you need:
- Complex stateful UI — media gallery, category tree, rule conditions builder
- Server-side data prep — load related entities, build option lists, pre-compute config
- Type-safe injection — ViewModels via DI, no
ObjectManager::getInstance()in templates - Reuse across boundaries — the same snippet works in a form, a grid cell, a page block
Crucially, snippets are JSON-discoverable: a form or grid declares"renderer": "snippet.media_gallery" and Nebula resolves the snippet, injects the ViewModel, renders the template. No layout XML, no manual block wiring.
Two flavors
A. Static snippets
JSON config + phtml template. No ViewModel. Good for small reusable bits — a status badge format, a country picker, a help-text block.
{
"id": "status_badge",
"template": "Acme_Module::snippet/status_badge.phtml",
"options": {
"1": { "label": "Enabled", "class": "success" },
"0": { "label": "Disabled","class": "error" }
}
}B. Dynamic snippets (ViewModel)
JSON config + phtml + ViewModel class registered in SnippetViewModelRegistry. Use when you need server-side logic, type-safe helpers, or shared state across instances.
{
"id": "media_gallery",
"template": "Acme_Module::snippet/media_gallery.phtml"
}
// di.xml
<type name="Qoliber\NebulaComponent\Model\SnippetViewModelRegistry">
<arguments>
<argument name="viewModels" xsi:type="array">
<item name="media_gallery" xsi:type="object">
Acme\Module\ViewModel\MediaGallery
</item>
</argument>
</arguments>
</type>The ViewModel registry, end to end
Form / grid references the snippet
"fields": {
"media_gallery": {
"label": "Media",
"renderer": "snippet.media_gallery",
"position": 50
}
}SectionRenderer resolves it
SnippetResolver::resolve('media_gallery') loads every JSON file atview/adminhtml/snippet/media_gallery.json across enabled modules, deep-merges them, and extracts the template path.
Registry hands over the ViewModel
SnippetViewModelRegistry::get('media_gallery') returns the bound class instance (constructor- injected like any Magento DI service). It's passed into the rendered block as view_model.
$viewModel = $this->viewModelRegistry->get($snippetId);
if ($viewModel !== null) {
$blockData['view_model'] = $viewModel;
}
$blockData['entity'] = $entity;
$blockData['form_block'] = $formBlock;
$blockData['section'] = $sectionConfig;Template uses it
The phtml retrieves the ViewModel, asks it for shaped data, and renders Alpine markup. A defensive fallback is allowed for the rare case where the snippet renders outside the registered path (e.g. a transient template block created at runtime).
<?php
declare(strict_types=1);
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Acme\Module\ViewModel\MediaGallery $viewModel */
$viewModel = $block->getData('view_model');
if (!$viewModel instanceof \Acme\Module\ViewModel\MediaGallery) {
// nebula:allow-object-manager dynamic-snippet-view-model fallback
$viewModel = \Magento\Framework\App\ObjectManager::getInstance()
->get(\Acme\Module\ViewModel\MediaGallery::class);
}
$entity = $block->getData('entity');
$config = $viewModel->buildConfig($entity);
$json = json_encode($config, JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<div x-data="acmeMediaGallery(<?= $block->escapeHtmlAttr($json) ?>)">
...
</div>ObjectManager::getInstance() is only safe inside snippets — flagged with thenebula:allow-object-manager comment so PHPStan's ban rule lets it through. Anywhere else, inject normally.Author your own snippet
Create the snippet JSON
{
"id": "custom_widget",
"template": "Your_Module::snippet/custom_widget.phtml"
}Anything else you put on this object is config the template can read via $block->getData('options') etc.
Write the phtml
<?php
declare(strict_types=1);
/** @var \Magento\Framework\View\Element\Template $block */
$entity = $block->getData('entity');
$section = $block->getData('section');
$value = $entity ? (string) $entity->getData($section['attribute_code'] ?? '') : '';
?>
<div x-data="{ open: false }">
<button @click="open = !open" class="...">Toggle</button>
<div x-show="open" x-cloak>
<input type="text" name="<?= $block->escapeHtmlAttr($section['name'] ?? '') ?>"
value="<?= $block->escapeHtmlAttr($value) ?>" />
</div>
</div>(optional) Bind a ViewModel
Whenever the template needs server data — collections, formatting, helpers — write a ViewModel that implements ArgumentInterface and register it.
<type name="Qoliber\NebulaComponent\Model\SnippetViewModelRegistry">
<arguments>
<argument name="viewModels" xsi:type="array">
<item name="custom_widget" xsi:type="object">
Your\Module\ViewModel\CustomWidget
</item>
</argument>
</arguments>
</type>Reference it from a form / grid
"fields": {
"my_field": {
"label": "My Field",
"renderer": "snippet.custom_widget",
"attribute_code": "my_field",
"position": 50
}
}Any extra keys on the field definition (here attribute_code) are passed straight through to the snippet's block data, so the template can use them as configuration.
Snippet or Alpine factory?
They're complementary, not alternatives. A snippet renders the HTML; an Alpine factory adds runtime behavior to it.
| If you need… | Snippet | Alpine factory |
|---|---|---|
| Reuse across forms / grids | yes | no — tied to one component |
| Server-side data (entities, collections) | yes | no — runs in the browser |
| Type-safe ViewModel injection | yes | no — TS, but separate concern |
| Stateful client behavior (collapse, drag) | usually pairs with Alpine inside the template | yes — registers with Alpine.data |
| Tiny one-off interaction | overkill | yes — inline x-data may be enough |
| Complex UI with mixed server + client | yes | yes — snippet renders, factory drives |
x-data="nebulaXxx(...)" binding; the registered factory in web/ts owns the runtime state. See the JS architecture guide.Snippets shipped with Nebula
snippet.media_galleryProduct image manager — drag/reorder, role assignment, alt text
ViewModel: yessnippet.category_treeHierarchical category picker for product edit
ViewModel: yessnippet.rule_conditionsMagento rule conditions builder (catalog/cart rules)
ViewModel: yessnippet.product_selectorSearch-and-pick UI for related/up-sell/cross-sell products
ViewModel: no — pure Alpinesnippet.searchable_multiselectMultiselect with inline search + chips
ViewModel: nosnippet.attribute_optionsPer-store-view label table for select/multiselect attributes
ViewModel: yessnippet.seo_previewLive Google-result preview that watches title/url/desc inputs
ViewModel: yessnippet.status_badgeReusable colored pill for grids
ViewModel: noReference paths
- code/Qoliber/NebulaComponent/docs/SNIPPETS.md
- code/Qoliber/NebulaComponent/Model/Definition/SnippetResolver.php
- code/Qoliber/NebulaComponent/Model/SnippetViewModelRegistry.php
- code/Qoliber/NebulaForm/Model/Form/SectionRenderer.php
- code/Qoliber/NebulaTheme/view/adminhtml/snippet/* (real examples)
- code/Qoliber/NebulaTheme/view/adminhtml/templates/eav/snippet/*