Skip to content

Your first grid

This page builds a listing for a table of notes in a module called Vendor_Module. The same files are what bin/magento nebula:crud:generate writes, so you can generate them and read this page to understand what you got.

The result is five files on top of a normal Magento model:

app/code/Vendor/Module/
├── etc/di.xml                                       ← the collection alias
├── view/adminhtml/grid/vendor_module_note_listing.json
├── view/adminhtml/layout/vendor_module_note_index.xml
├── Controller/Adminhtml/Note/Index.php
└── etc/adminhtml/menu.xml, etc/acl.xml, etc/adminhtml/routes.xml

1. The table and the model

Any Magento model with a resource model and a collection will do. For this walkthrough:

etc/db_schema.xml
<table name="vendor_module_note" resource="default" engine="innodb" comment="Notes">
    <column xsi:type="int" name="note_id" unsigned="true" nullable="false" identity="true"/>
    <column xsi:type="varchar" name="name" length="255" nullable="false"/>
    <column xsi:type="smallint" name="is_active" nullable="false" default="1"/>
    <constraint xsi:type="primary" referenceId="PRIMARY">
        <column name="note_id"/>
    </constraint>
</table>

With Model\Note, Model\ResourceModel\Note and Model\ResourceModel\Note\Collection written the usual way.

2. Register the collection alias

A definition never names a class. Bind the collection to an alias in DI:

etc/di.xml
<type name="Nebula\Grid\Model\Registry\CollectionRegistry">
    <arguments>
        <argument name="bindings" xsi:type="array">
            <item name="vendor_module_note.collection" xsi:type="string">Vendor\Module\Model\ResourceModel\Note\Collection</item>
        </argument>
    </arguments>
</type>

The alias can be any string. The convention in the shipped definitions is <table>.collection.

3. Write the definition

view/adminhtml/grid/vendor_module_note_listing.json
{
    "id": "vendor_module_note_listing",
    "acl": "Vendor_Module::note",
    "dataSource": {
        "provider": "grid.collection",
        "config": { "collection": "vendor_module_note.collection" }
    },
    "settings": {
        "pageSize": 20,
        "pageSizes": [10, 20, 50],
        "defaultSort": { "field": "note_id", "direction": "asc" },
        "addButton": { "label": "Add New Note", "url": "vendor_module/note/new" },
        "massActionIdField": "note_id",
        "massActions": [
            { "id": "delete", "label": "Delete", "url": "vendor_module/note/massDelete", "confirm": "Delete selected?" }
        ]
    },
    "columns": {
        "note_id":   { "label": "Note ID", "type": "text", "sortable": true, "position": 10, "filter": "range" },
        "name":      { "label": "Name", "type": "text", "sortable": true, "position": 20, "filter": "text", "searchable": true },
        "is_active": { "label": "Active", "type": "boolean", "sortable": true, "position": 30, "filter": "select",
                       "filterOptions": { "1": "Yes", "0": "No" } },
        "actions": {
            "label": "", "type": "actions", "position": 999,
            "actions": [
                { "label": "Edit",   "url": "vendor_module/note/edit/note_id/{{note_id}}" },
                { "label": "Delete", "url": "vendor_module/note/delete/note_id/{{note_id}}", "method": "post", "confirm": "Are you sure?" }
            ]
        }
    }
}

Line by line:

  • id must equal the file name. It is what the layout refers to and what overlays merge on.
  • acl is required. Without it nobody can open the grid.
  • dataSource.provider is grid.collection, the provider that wraps any Magento collection. config.collection is the alias from step 2.
  • settings.defaultSort is applied when the administrator has not chosen a sort. massActionIdField names the column whose values are posted as ids[].
  • Columns are a map keyed by the field name in the collection. filter chooses the filter widget; range gives a from-to pair, select a dropdown from filterOptions, text a LIKE search. searchable adds the column to the keyword search.
  • The actions column renders row links. {{note_id}} is replaced with the row's value. method: "post" renders a form-key protected POST, which is what a delete action needs.

4. Mount it

view/adminhtml/layout/vendor_module_note_index.xml
<?xml version="1.0" encoding="UTF-8"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="Nebula\Grid\Block\Grid" name="nebula.vendor_module_note_listing">
                <arguments>
                    <argument name="grid_id" xsi:type="string">vendor_module_note_listing</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

The handle name is the full action name of the controller, so vendor_module/note/index loads vendor_module_note_index.xml.

5. The controller, route, menu and ACL

These are plain Magento:

Controller/Adminhtml/Note/Index.php
class Index extends \Magento\Backend\App\Action implements HttpGetActionInterface
{
    public const ADMIN_RESOURCE = 'Vendor_Module::note';

    public function __construct(Context $context, private readonly PageFactory $pageFactory)
    {
        parent::__construct($context);
    }

    public function execute(): Page
    {
        $page = $this->pageFactory->create();
        $page->setActiveMenu('Vendor_Module::note');
        $page->getConfig()->getTitle()->prepend(__('Notes'));
        return $page;
    }
}
etc/adminhtml/routes.xml
<router id="admin">
    <route id="vendor_module" frontName="vendor_module">
        <module name="Vendor_Module"/>
    </route>
</router>
etc/acl.xml
<resource id="Magento_Backend::admin">
    <resource id="Vendor_Module::note" title="Notes"/>
</resource>
etc/adminhtml/menu.xml
<add id="Vendor_Module::note" title="Notes" module="Vendor_Module" parent="Magento_Backend::content"
     sortOrder="10" action="vendor_module/note/index" resource="Vendor_Module::note"/>

The MassDelete controller reads ids from the POST and deletes each one through the repository. The Delete controller reads note_id from the URL.

6. Declare the dependency

etc/module.xml
<module name="Vendor_Module">
    <sequence>
        <module name="Nebula_Component"/>
        <module name="Nebula_Grid"/>
        <module name="Nebula_Form"/>
        <module name="Nebula_Theme"/>
    </sequence>
</module>

7. Enable and open

bin/magento module:enable Vendor_Module
bin/magento setup:upgrade
bin/magento nebula:lint          # validates the new definition against the schema
bin/magento nebula:compile       # production mode only
bin/magento cache:clean nebula_definitions layout

Open Content › Notes. You have sorting, filters, search on the name, paging, export, saved views and a Delete mass action.

Generate it instead

bin/magento nebula:crud:generate vendor_module_note --module=Vendor_Module --menu-parent=Magento_Backend::content

writes every file above plus the edit form, the repository, the API interfaces and the save controller. --dry-run prints what it would write. See Generators.

Next