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:
<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:
<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¶
{
"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:
idmust equal the file name. It is what the layout refers to and what overlays merge on.aclis required. Without it nobody can open the grid.dataSource.providerisgrid.collection, the provider that wraps any Magento collection.config.collectionis the alias from step 2.settings.defaultSortis applied when the administrator has not chosen a sort.massActionIdFieldnames the column whose values are posted asids[].- Columns are a map keyed by the field name in the collection.
filterchooses the filter widget;rangegives a from-to pair,selecta dropdown fromfilterOptions,texta LIKE search.searchableadds the column to the keyword search. - The
actionscolumn 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¶
<?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:
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;
}
}
<router id="admin">
<route id="vendor_module" frontName="vendor_module">
<module name="Vendor_Module"/>
</route>
</router>
<resource id="Magento_Backend::admin">
<resource id="Vendor_Module::note" title="Notes"/>
</resource>
<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¶
<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¶
- Add a quick-edit drawer: the
editableblock in the Grid reference. - Feed the grid from something that is not a collection: Data providers.
- Build the edit form the row links point at: Your first form.