Skip to content

Rendering

This page follows a resolved definition to the HTML the browser receives, and then to the JavaScript that makes it interactive.

Blocks

Two block classes do the work:

Block Reads Renders
Nebula\Grid\Block\Grid grid_id argument, or an inline_definition array The grid shell, toolbar, filters, table or cards, pager, mass actions
Nebula\Form\Block\Form form_id argument, or inline_definition The form shell, action bar, fieldsets or layout tree, fields

Nebula\Form\Block\EavForm is a separate block that renders definitions with "type": "eav".

inline_definition is how a definition that was not loaded from a file reaches a block. The quick-edit drawer builds a small form definition from a grid's editable block and hands it to the form block that way, and the UI Bridge uses the same door.

Each block asks the resolver for its definition, checks access, and hands the result to a .phtml template. The templates live in the module (view/adminhtml/templates/grid/*.phtml, form.phtml, form-shell.phtml, field/*.phtml) and can be overridden in a theme like any Magento template.

Renderer pool

A column names either a type or a renderer; the schema rejects both on one column. A field has a type and may add a renderer. RendererPool decides which renderer produces the cell or the field:

  1. A renderer starting with snippet. resolves the snippet and renders its template.
  2. Any other renderer is looked up in the pool by name.
  3. Otherwise the type is looked up: text, select, badge, boolean, price, date, thumbnail, link, actions, html for columns; the 17 field types for forms.

A named renderer is dispatched before a built-in type. Column renderers implement ColumnRendererInterface and are bound in di.xml under columnRenderers; field types are bound under fieldRenderers. See Form field types for a custom renderer registration.

Templates and the data bag

A renderer template receives a fixed set of variables. For a grid cell: column, key, item, value, view_model, and the shared pair record and nebula_context ("grid"). For a form section: entity_data, form_block, section, view_model, record and nebula_context ("form"). A template that needs a view model throws if it is missing, by design, so a snippet used in both places gets the same object in both.

Form values and the model store

A Nebula form is a native <form> that posts to its saveUrl. Fields do not write hidden inputs; each field's Alpine component registers a model in Alpine.store('nebulaModels'), and on the form's submit event the store serialises every model as [{name, value}] into the request. Serialisation runs on submit, not on the Save button, so Enter and programmatic submits carry the data too.

Two rules follow from this. Every field must be inside the one form element; a nested <form> from a widget closes the outer form early and everything after it is silently not submitted. And a form posts every field it renders, so a save controller should merge with addData() rather than replace with setData().

The bundles

The theme's TypeScript compiles with esbuild into 20 IIFE bundles under web/js/dist:

Bundle Loaded Contains
nebula-core.js every admin page Alpine.js, the model store, validator, toasts, modal, confirm and prompt dialogs, field components, column components, menu, the rule editor, configuration form helpers
nebula-grid.js pages with a grid Grid state: filters, sort, paging, selection, saved views, quick edit, export
nebula-form.js pages with a form Form state, dirty tracking, section behaviour
nebula-vendor-sortable.js every admin page, before the core bundle Installs window.Sortable
nebula-vendor-chart.js the adminhtml_dashboard_index handle Installs window.Chart, Chart.js with every registerable
nebula-rule-editor.js by layout handle The condition tree built standalone, for a page that does not load nebula-core.js
nebula-email-kit.js by the package that consumes it Installs window.NebulaEmailKit: email template splitting, CSS scoping, the token model
nebula-email-code-editor.js by the package that consumes it Installs window.NebulaEmailCodeEditor, split out of the kit because CodeMirror is most of its weight
page bundles by layout handle nebula-media.js, nebula-quill.js, nebula-catalog-product.js, nebula-merchandiser.js, and the rest

A page bundle is loaded by a small scripts/*.phtml block in the layout handle that needs it, and a module may load one of the bundles above the same way. The bundles are committed, so an install does not run Node.

The public JavaScript surface a module may call is window.Nebula: validation, confirm(), prompt(), modal(), postAction(), t() for translations, and window.NebulaField.base() for a custom field component — all declared in the theme's web/types/nebula.d.ts. Toasts are window.nebulaToast(type, message). The four globals in the table are a published contract too, so a template that needs drag-and-drop, a chart or the email helpers reads them off window rather than bundling its own copy. window.Sortable is already on the page everywhere; window.Chart needs the same one-line script block on your own handle. See CSS and JavaScript in your module.

The command palette is the exception to the bundle rule. Nebula_Core::command-palette.phtml registers Alpine.data('nebulaCommandPalette', ...) in an inline script rendered through SecureHtmlRenderer, guarded by window._nebulaCommandPaletteRegistered, and renders nothing unless its view_model is a Nebula\Core\ViewModel\CommandPalette holding items. If you override that template in a child theme, the palette's JavaScript goes with it. What nebula-core.js does carry is the companion helper that opens the <details> group a palette result links to on a configuration page.

Content Security Policy

Nebula templates render inline <script> and <style> through Magento's SecureHtmlRenderer, so the tag carries the request nonce. The rule is not yet universal. Four admin templates still ship a raw un-nonced inline script, one of them the passkey button on the admin login screen, so the admin inline policy has to stay on or passkey sign-in stops working. Alpine's expression evaluator also needs 'unsafe-eval'. Templates you add should follow the SecureHtmlRenderer pattern; the snippet is on the CSS and JavaScript page.

Dark mode

A script in the document head, before the stylesheet arrives, reads localStorage['nebula_dark'] and falls back to prefers-color-scheme. It adds the dark class to the root element, and sets color-scheme when a preference is stored so light-dark() tokens follow it. With nothing stored the stylesheet's own color-scheme: light dark already follows the operating system. Alpine mirrors the class on body afterwards. A stylesheet you add needs no dark variant if it reads tokens rather than literal colours.