NebulaNebula
Developer guide

JSON Grids

Listing pages without ui-component XML. One JSON file declares the columns, data source, filters, and mass actions — Nebula renders the toolbar, table, pager, and Alpine wiring.

The shape in 30 seconds

A grid file lives at view/adminhtml/grid/<id>.json. Like forms, it follows base + overlay merge — any module can extend an existing grid.

view/adminhtml/grid/product_listing.json
{
  "id": "product_listing",
  "acl": "Magento_Catalog::products",
  "dataSource": {
    "provider": "grid.collection",
    "config": { "collection": "Magento\\Catalog\\Model\\ResourceModel\\Product\\Collection" }
  },
  "settings": {
    "pageSize": 25,
    "pageSizes": [10, 25, 50, 100],
    "defaultSort": { "field": "entity_id", "direction": "desc" },
    "addButton": { "label": "Add Product", "url": "catalog/product/new" },
    "massActions": [
      { "id": "delete", "label": "Delete Selected", "url": "catalog/product/massDelete",
        "confirm": "Delete these products?" }
    ]
  },
  "columns": {
    "entity_id": { "label": "ID",    "type": "text",  "sortable": true, "position": 10 },
    "name":      { "label": "Name",  "type": "text",  "sortable": true, "searchable": true, "filter": "text", "position": 20 },
    "price":     { "label": "Price", "type": "price", "sortable": true, "filter": "range", "position": 30 },
    "status":    {
      "label": "Status", "type": "badge", "filter": "select", "position": 40,
      "options": {
        "1": { "label": "Enabled",  "class": "success" },
        "2": { "label": "Disabled", "class": "error"   }
      }
    },
    "actions": {
      "label": "", "type": "actions", "position": 999,
      "actions": [
        { "label": "Edit", "url": "catalog/product/edit/id/{{entity_id}}" }
      ]
    }
  }
}

That file renders a sortable, filterable, paginated, multi-select admin grid with a primary "Add" button and a delete-with-confirmation mass action.

Anatomy

KeyRequiredPurpose
idbase onlySnake_case identifier; overlays omit it.
aclnoMagento ACL resource — controls page access
dataSourceyesprovider alias + provider config (collection FQCN, identifierParam, etc.)
columnsyesMap of column_id → column definition. Order by `position`.
settingsnoPagination, default sort, mass actions, primary toolbar button

Column types

Every column needs either a built-in type or a renderer (snippet or template).

typeWhat it rendersNotable config
textPlain stringsortable, searchable, filter
linkClickable URLhref, target
thumbnailImage cellsrc, alt, width, height
badgeColored pilloptions: { value → { label, class } } — class ∈ success/error/warning/info/neutral
dateLocalized dateParsed by JS Date, formatted per locale
priceCurrency-formatted amountUses Magento PriceCurrencyInterface
booleanYes/NotrueLabel, falseLabel
htmlUnescaped HTMLTrusted content only
actionsInline row actionsactions: [{ label, url }] — URL supports {{field}} placeholders
A badge column
"status": {
  "label": "Status",
  "type": "badge",
  "sortable": true,
  "filter": "select",
  "options": {
    "1": { "label": "Enabled",  "class": "success" },
    "0": { "label": "Disabled", "class": "error"   }
  },
  "position": 30
}
A price + range filter
"price": {
  "label": "Price",
  "type": "price",
  "sortable": true,
  "filter": "range",
  "position": 40
}
Row actions (with URL placeholders)
"actions": {
  "label": "",
  "type": "actions",
  "position": 999,
  "actions": [
    { "label": "Edit",   "url": "catalog/product/edit/id/{{entity_id}}" },
    { "label": "View",   "url": "catalog/product/view/id/{{entity_id}}" },
    { "label": "Delete", "url": "catalog/product/delete/id/{{entity_id}}", "confirm": "Sure?" }
  ]
}

Per-column properties

PropertyEffect
labelHeader text
positionRender order (0..998; reserve 999 for actions)
sortableHeader click toggles sort
searchableGlobal search box includes this column
filter"text" | "select" | "range" | "date" — adds a filter row
filterOptions{ value: label } — for filter:"select" inline options
filterOptionsSourceAlias for dynamic options (resolved via OptionSourceResolver)
optionsFor type:"badge" — { cellValue: { label, class } }
rendererSnippet reference or template path (instead of type)

Mass actions

Declared in settings.massActions. When the user selects rows and picks an action, Nebula POSTs a dynamically-built form to the action URL with ids[] and form_key. Controllers receive the standard $this->getRequest()->getPost('ids').

"settings": {
  "massActions": [
    { "id": "delete", "label": "Delete",  "url": "catalog/product/massDelete", "confirm": "Are you sure?" },
    { "id": "enable", "label": "Enable",  "url": "catalog/product/massEnable" },
    { "id": "disable","label": "Disable", "url": "catalog/product/massDisable" }
  ]
}
The form submission is handled entirely in nebula-grid.ts — the template only owns checkbox state. Confirmation dialogs use the native browser confirm() when confirm is set.

Filters & paging

Filter values submit as URL params:

Text / select / date

?filters[status]=1

One value per column id.

Range (price / number / date)

?filters[price_from]=10
&filters[price_to]=100

Two URL params per ranged column, suffixed with _from / _to.

Paging via ?page=N&pageSize=N. Page sizes come fromsettings.pageSizes; users get a dropdown.

Custom column — snippet or template

When none of the built-in types fit, point at a snippet:

"thumbnail": {
  "label": "Image",
  "renderer": "snippet.product_thumbnail",
  "position": 5
}

Or render a raw phtml template:

"availability": {
  "label": "Stock",
  "renderer": "Acme_Catalog::grid/column/availability.phtml",
  "position": 50
}
Snippet renderers receive the cell value, the full row, and the column config as block data. Templates stay short — typically just escape and wrap, with any Alpine state living in a registered factory.

Render flow

view/adminhtml/grid/<id>.json    (any module)
        │
        ▼
DefinitionResolver.resolve('grid', $id)
        │   Loader.collect → Merger.merge → GridDefinitionNormalizer
        ▼
Qoliber\NebulaGrid\Block\Grid
        │   getDefinition()  → resolved JSON
        │   getColumns()     → sorted by position
        │   getItems()       → DataProviderResolver.fetch(provider, config)
        ▼
view/adminhtml/templates/grid.phtml
        │   <div x-data="nebulaGrid({...})">
        │     toolbar / header / body / pager
        ▼
ColumnDispatcher.render(col, key, item)
        │   → SnippetRenderer for renderer:"snippet.*"
        │   → TemplateRenderer for renderer:"Vendor::template"
        │   → RendererPool.getColumnRenderer(type)  (BadgeRenderer, PriceRenderer, ...)
        ▼
Alpine.start()
        │   nebulaGrid factory owns row selection + mass-action submission

Authoring a new grid — checklist

1

Drop the JSON

Path: YourModule/view/adminhtml/grid/blog_post_listing.json. Snake_case id matches the filename.

2

Bind a data provider

Either use the generic grid.collection provider with a collection FQCN, or register your own via DataProviderRegistry in di.xml.

"dataSource": {
  "provider": "grid.collection",
  "config": { "collection": "Acme\\Blog\\Model\\ResourceModel\\Post\\Collection" }
}
3

Define columns

Start with id, label, status, and an actions column. Add more as you go; reorder via position.

4

Wire the controller

One Index controller that creates a Qoliber\NebulaGrid\Block\Grid with the grid id.

public function execute() {
    $page = $this->resultPageFactory->create();
    $page->getLayout()->createBlock(
      \Qoliber\NebulaGrid\Block\Grid::class,
      'blog.post.grid',
      ['data' => ['grid_id' => 'blog_post_listing']]
    );
    return $page;
}
5

Add mass action endpoints

Each mass action URL is a regular admin controller. Read $this->getRequest()->getPost('ids'), iterate, save / delete, then return a redirect.

Reference paths

  • code/Qoliber/NebulaComponent/schemas/grid.schema.json
  • code/Qoliber/NebulaComponent/docs/schemas/GRID.md
  • code/Qoliber/NebulaGrid/Block/Grid.php
  • code/Qoliber/NebulaGrid/Model/ColumnLoader.php · DataProvider/CollectionProvider.php
  • code/Qoliber/NebulaGrid/Renderer/Column/* (BadgeRenderer, PriceRenderer, ActionsRenderer, …)
  • design/adminhtml/Qoliber/Nebula/web/ts/grid.ts