Add a Grid in Magento 2 (Step-by-Step Guide)

This post will provide a simple, easy to follow step-by-step guide on how to add a grid in Magento 2, using the UI component functionality. To view further information about the file structure and why the functionality works as it does, view the UI Grid Component in Magento 2 article.

Presumptions

  • The Magento application is in developer mode.
  • If you pause to view changes, it is assumed you will refresh the cache (if enabled) on your own accord.
  • [Vendor] and [Module] that are mentioned below in the steps should be replaced by the vendor and module names that you use when constructing the module, i.e. app/code/[Vendor]/[Module]. Please also take note of when to use lowercase and uppercase.
  • The file paths named at the top of the file in this post i.e. app/code/[Vendor]/[Module]/etc/di.xml are references to where the files should be created/edited. The line does not need to reside within the files themselves.

Steps

1. Add the module’s registration.php and module.xml files.

// app/code/[Vendor]/[Module]/registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    '[Vendor]_[Module]',
    __DIR__
);
// app/code/[Vendor]/[Module]/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="[Vendor]_[Module]" setup_version="1.0.0" />
</config>

2. Add an InstallSchema class to add a database table for your custom entity.

// app/code/[Vendor]/[Module]/Setup/InstallSchema.php

<?php
namespace [Vendor]\[Module]\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;

class InstallSchema implements InstallSchemaInterface
{
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;
        $installer->startSetup();
        $tableName = $installer->getTable('[module]_entity_table');

        if ($installer->getConnection()->isTableExists($tableName) !== true) {
            $table = $installer->getConnection()
                ->newTable($tableName)
                ->addColumn(
                    'entity_id',
                    Table::TYPE_INTEGER,
                    null,
                    [
                        'identity' => true,
                        'unsigned' => true,
                        'nullable' => false,
                        'primary' => true
                    ],
                    'Entity ID'
                )
                ->addColumn(
                    'name',
                    Table::TYPE_TEXT,
                    255,
                    ['nullable' => false, 'default' => ''],
                    'Name'
                )
                ->addColumn(
                    'description',
                    Table::TYPE_TEXT,
                    null,
                    ['nullable' => false, 'default' => ''],
                    'Description'
                )
                ->addColumn(
                    'is_active',
                    Table::TYPE_SMALLINT,
                    null,
                    ['unsigned' => true, 'nullable' => false, 'default' => '0'],
                    'Status'
                )
                ->addColumn(
                    'created_at',
                    Table::TYPE_TIMESTAMP,
                    null,
                    ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
                    'Created At'
                )
                ->addColumn(
                    'updated_at',
                    Table::TYPE_TIMESTAMP,
                    null,
                    ['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE],
                    'Updated At'
                );
            $installer->getConnection()->createTable($table);
        }
        $installer->endSetup();
    }
}

3. Enable the module and run the database upgrade commands from the Magento root directory.

$ /path/to/your/php bin/magento module:enable [Vendor]_[Module]
$ /path/to/your/php bin/magento setup:upgrade

4. Define an admin route so that the grid can be accessed within the admin.

// app/code/[Vendor]/[Module]/etc/adminhtml/routes.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd">
    <router id="admin">
        <route id="[module]" frontName="[module]">
            <module name="[Vendor]_[Module]" />
        </route>
    </router>
</config>

5. Now add a admin menu item via configuration in the module’s menu.xml file.

// app/code/[Vendor]/[Module]/etc/adminhtml/menu.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
    <menu>
        <add id="[Vendor]_[Module]::entity" title="[Module] Entity" module="[Vendor]_[Module]" sortOrder="1000"
             resource="[Vendor]_[Module]::entity" action="[module]/entity/index"/>
    </menu>
</config>

6. Now for the ACL configuration.

// app/code/[Vendor]/[Module]/etc/acl.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="[Vendor]_[Module]::entity" title="[Vendor] [Module]" translate="title" sortOrder="1000"/>
            </resource>
        </resources>
    </acl>
</config>

7. A controller class will now be needed to handle the [module]/entity/index route. As there will be several routes involved in this UI component (index, edit, save, delete etc.), an abstract controller will also be created that the other controllers will extend from.

// app/code/[Vendor]/[Module]/Controller/Adminhtml/Entity.php

<?php
namespace [Vendor]\[Module]\Controller\Adminhtml;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\Registry;
use Magento\Framework\View\Result\PageFactory;
use Magento\Backend\Model\View\Result\ForwardFactory;

abstract class Entity extends Action
{
    /**
     * Authorization level of a basic admin session
     *
     * @see _isAllowed()
     */
    const ACTION_RESOURCE = '[Vendor]_[Module]::entity';

    /**
     * Core registry
     *
     * @var Registry
     */
    protected $coreRegistry;

    /**
     * Result Page Factory
     *
     * @var PageFactory
     */
    protected $resultPageFactory;

    /**
     * Result Forward Factory
     *
     * @var ForwardFactory
     */
    protected $resultForwardFactory;

    /**
     * Data constructor.
     *
     * @param Registry $registry
     * @param PageFactory $resultPageFactory
     * @param ForwardFactory $resultForwardFactory
     * @param Context $context
     */
    public function __construct(
        Registry $registry,
        PageFactory $resultPageFactory,
        ForwardFactory $resultForwardFactory,
        Context $context
    ) {
        $this->coreRegistry         = $registry;
        $this->resultPageFactory    = $resultPageFactory;
        $this->resultForwardFactory = $resultForwardFactory;
        parent::__construct($context);
    }
}
// app/code/[Vendor]/[Module]/Controller/Adminhtml/Entity/Index.php

<?php
namespace [Vendor]\[Module]\Controller\Adminhtml\Entity;

use [Vendor]\[Module]\Controller\Adminhtml\Entity;

class Index extends Entity
{
    /**
     * @return \Magento\Framework\View\Result\Page
     */
    public function execute()
    {
        return $this->resultPageFactory->create();
    }
}

Now if you’ve made it this far, you should see the custom menu item created in the admin, and when clicking on it, you should be faced with a Magento admin page with no real content between the header and footer.

Add a Grid in Magento 2

Now it’s time to construct the grid. This will involve a few main steps: Creating the model classes, the interfaces required, and the repository classes with a few configuration lines added to di.xml.

8. Start with the model classes:

// app/code/[Vendor]/[Module]/Model/Entity.php

<?php
namespace [Vendor]\[Module]\Model;

use Magento\Framework\Model\AbstractModel;

class Entity extends AbstractModel
{
    /**
     * Cache tag
     */
    const CACHE_TAG = '[vendor]_[module]_entity';

    /**
     * Initialise resource model
     * @codingStandardsIgnoreStart
     */
    protected function _construct()
    {
        // @codingStandardsIgnoreEnd
        $this->_init('[Vendor]\[Module]\Model\ResourceModel\Entity');
    }

    /**
     * Get cache identities
     *
     * @return array
     */
    public function getIdentities()
    {
        return [self::CACHE_TAG . '_' . $this->getId()];
    }
}
// app/code/[Vendor]/[Module]/Model/ResourceModel/Entity.php

<?php
namespace [Vendor]\[Module]\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\Context;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Stdlib\DateTime\DateTime;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class Entity extends AbstractDb
{
    /**
     * @var \Magento\Framework\Stdlib\DateTime\DateTime
     */
    protected $date;

    /**
     * Data constructor.
     *
     * @param Context $context
     * @param DateTime $date
     */
    public function __construct(
        Context $context,
        DateTime $date
    ) {
        $this->date = $date;
        parent::__construct($context);
    }

    /**
     * Resource initialisation
     * @codingStandardsIgnoreStart
     */
    protected function _construct()
    {
        // @codingStandardsIgnoreEnd
        $this->_init('[module]_entity_table', 'entity_id');
    }

    /**
     * Before save callback
     *
     * @param AbstractModel|\[Vendor]\[Module]\Model\Entity $object
     * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb
     * @codingStandardsIgnoreStart
     */
    protected function _beforeSave(AbstractModel $object)
    {
        // @codingStandardsIgnoreEnd
        $object->setUpdatedAt($this->date->gmtDate());
        if ($object->isObjectNew()) {
            $object->setCreatedAt($this->date->gmtDate());
        }
        return parent::_beforeSave($object);
    }
}
// app/code/[Vendor]/[Module]/Model/ResourceModel/Entity/Collection.php

<?php
namespace [Vendor]\[Module]\Model\ResourceModel\Entity;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;

class Collection extends AbstractCollection
{
    /**
     * @var string
     * @codingStandardsIgnoreStart
     */
    protected $_idFieldName = 'entity_id';

    /**
     * Collection initialisation
     */
    protected function _construct()
    {
        // @codingStandardsIgnoreEnd
        $this->_init('[Vendor]\[Module]\Model\Entity', '[Vendor]\[Module]\Model\ResourceModel\Entity');
    }
}

9. Now add the interfaces.

// app/code/[Vendor]/[Module]/Api/Data/EntityInterface.php

<?php
namespace [Vendor]\[Module]\Api\Data;

interface EntityInterface
{
    const ENTITY_ID         = 'entity_id';
    const DATA_TITLE        = 'name';
    const DATA_DESCRIPTION  = 'description';
    const IS_ACTIVE         = 'is_active';
    const CREATED_AT        = 'created_at';
    const UPDATED_AT        = 'updated_at';
    
    /**
     * Get ID
     *
     * @return int|null
     */
    public function getEntityId();

    /**
     * Set ID
     *
     * @param $id
     * @return EntityInterface
     */
    public function setEntityId($id);

    /**
     * Get Name
     *
     * @return string
     */
    public function getName();

    /**
     * Set Name
     *
     * @param $name
     * @return EntityInterface
     */
    public function setName($name);

    /**
     * Get Description
     *
     * @return string
     */
    public function getDescription();

    /**
     * Set Description
     *
     * @param $description
     * @return EntityInterface
     */
    public function setDescription($description);

    /**
     * Get is active
     *
     * @return bool|int
     */
    public function getIsActive();

    /**
     * Set is active
     *
     * @param $isActive
     * @return EntityInterface
     */
    public function setIsActive($isActive);

    /**
     * Get created at
     *
     * @return string
     */
    public function getCreatedAt();

    /**
     * set created at
     *
     * @param $createdAt
     * @return EntityInterface
     */
    public function setCreatedAt($createdAt);

    /**
     * Get updated at
     *
     * @return string
     */
    public function getUpdatedAt();

    /**
     * set updated at
     *
     * @param $updatedAt
     * @return EntityInterface
     */
    public function setUpdatedAt($updatedAt);
}
// app/code/[Vendor]/[Module]/Api/Data/EntitySearchResultsInterface.php

<?php
namespace [Vendor]\[Module]\Api\Data;

use Magento\Framework\Api\SearchResultsInterface;

interface EntitySearchResultsInterface extends SearchResultsInterface
{
    /**
     * Get entity list.
     *
     * @return \[Vendor]\[Module]\Api\Data\EntityInterface[]
     */
    public function getItems();

    /**
     * Set entity list.
     *
     * @param \[Vendor]\[Module]\Api\Data\EntityInterface[] $items
     * @return $this
     */
    public function setItems(array $items);
}
// app/code/[Vendor]/[Module]/Api/EntityRepositoryInterface.php

<?php
namespace [Vendor]\[Module]\Api;

use Magento\Framework\Api\SearchCriteriaInterface;
use [Vendor]\[Module]\Api\Data\EntityInterface;

interface EntityRepositoryInterface
{

    /**
     * @param EntityInterface $entity
     * @return mixed
     */
    public function save(EntityInterface $entity);


    /**
     * @param $entityId
     * @return mixed
     */
    public function getById($entityId);

    /**
     * @param SearchCriteriaInterface $searchCriteria
     * @return \[Vendor]\[Module]\Api\Data\EntitySearchResultsInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getList(SearchCriteriaInterface $searchCriteria);

    /**
     * @param EntityInterface $entity
     * @return mixed
     */
    public function delete(EntityInterface $entity);

    /**
     * @param $entityId
     * @return mixed
     */
    public function deleteById($entityId);
}

10. Add the preferences in di.xml.

// app/code/[Vendor]/[Module]/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="[Vendor]\[Module]\Api\DataRepositoryInterface" type="[Vendor]\[Module]\Model\EntityRepository" />
    <preference for="[Vendor]\[Module]\Api\Data\EntityInterface" type="[Vendor]\[Module]\Model\Entity" />

</config>

11. Edit the app/code/[Vendor]/[Module]/Model/Entity.php file and ensure the class implements [Vendor]\[Module]\Api\Data\EntityInterface, and include the get() and set() methods.

// app/code/[Vendor]/[Module]/Model/Entity.php

<?php
namespace [Vendor]\[Module]\Model;

use Magento\Framework\Model\AbstractModel;
use [Vendor]\[Module]\Api\Data\EntityInterface;

class Entity extends AbstractModel implements EntityInterface
{
     /**
     * Cache tag
     */
    const CACHE_TAG = '[vendor]_[module}_entity';

    /**
     * Initialise resource model
     * @codingStandardsIgnoreStart
     */
    protected function _construct()
    {
        // @codingStandardsIgnoreEnd
        $this->_init('[Vendor]\[Module]\Model\ResourceModel\Entity');
    }

    /**
     * Get cache identities
     *
     * @return array
     */
    public function getIdentities()
    {
        return [self::CACHE_TAG . '_' . $this->getId()];
    }

    /**
     * Get Name
     *
     * @return string
     */
    public function getName()
    {
        return $this->getData(EntityInterface::DATA_TITLE);
    }

    /**
     * Set Name
     *
     * @param $title
     * @return $this
     */
    public function setName($title)
    {
        return $this->setData(EntityInterface::DATA_TITLE, $title);
    }
    /**
     * Get Description
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->getData(EntityInterface::DATA_TITLE);
    }
    /**
     * Set Description
     *
     * @param $description
     * @return $this
     */
    public function setDescription($description)
    {
        return $this->setData(EntityInterface::DATA_DESCRIPTION, $description);
    }

    /**
     * Get Is Active
     *
     * @return bool|int
     */
    public function getIsActive()
    {
        return $this->getData(EntityInterface::IS_ACTIVE);
    }

    /**
     * Set Is Active
     *
     * @param $isActive
     * @return $this
     */
    public function setIsActive($isActive)
    {
        return $this->setData(EntityInterface::IS_ACTIVE, $isActive);
    }

    /**
     * Get Created At
     *
     * @return string
     */
    public function getCreatedAt()
    {
        return $this->getData(EntityInterface::CREATED_AT);
    }

    /**
     * Set Created At
     *
     * @param $createdAt
     * @return $this
     */
    public function setCreatedAt($createdAt)
    {
        return $this->setData(EntityInterface::CREATED_AT, $createdAt);
    }

    /**
     * Get Updated At
     *
     * @return string
     */
    public function getUpdatedAt()
    {
        return $this->getData(EntityInterface::UPDATED_AT);
    }

    /**
     * Set Updated At
     *
     * @param $updatedAt
     * @return $this
     */
    public function setUpdatedAt($updatedAt)
    {
        return $this->setData(EntityInterface::UPDATED_AT, $updatedAt);
    }
}

12. Create the app/code/[Vendor]/[Module]/Model/EntityRepository.php file as defined in di.xml.

// app/code/[Vendor]/[Module]/Model/EntityRepository.php

<?php
namespace [Vendor]\[Module]\Model;

use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\Search\FilterGroup;
use Magento\Framework\Api\SortOrder;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\StateException;
use Magento\Framework\Exception\ValidatorException;
use Magento\Framework\Exception\NoSuchEntityException;
use [Vendor]\[Module]\Api\EntityRepositoryInterface;
use [Vendor]\[Module]\Api\Data\EntityInterface;
use [Vendor]\[Module]\Api\Data\EntityInterfaceFactory;
use [Vendor]\[Module]\Api\Data\EntitySearchResultsInterfaceFactory;
use [Vendor]\[Module]\Model\ResourceModel\Entity as ResourceEntity;
use [Vendor]\[Module]\Model\ResourceModel\Entity\CollectionFactory as EntityCollectionFactory;

class EntityRepository implements EntityRepositoryInterface
{
    /**
     * @var array
     */
    protected $instances = [];

    /**
     * @var ResourceEntity
     */
    protected $resource;

    /**
     * @var EntityCollectionFactory
     */
    protected $entityCollectionFactory;

    /**
     * @var EntitySearchResultsInterfaceFactory
     */
    protected $searchResultsFactory;

    /**
     * @var EntityInterfaceFactory
     */
    protected $entityInterfaceFactory;

    /**
     * @var DataObjectHelper
     */
    protected $dataObjectHelper;

    public function __construct(
        ResourceEntity $resource,
        EntityCollectionFactory $entityCollectionFactory,
        EntitySearchResultsInterfaceFactory $entitySearchResultsInterfaceFactory,
        EntityInterfaceFactory $entityInterfaceFactory,
        DataObjectHelper $dataObjectHelper
    ) {
        $this->resource = $resource;
        $this->entityCollectionFactory = $entityCollectionFactory;
        $this->searchResultsFactory = $entitySearchResultsInterfaceFactory;
        $this->entityInterfaceFactory = $entityInterfaceFactory;
        $this->dataObjectHelper = $dataObjectHelper;
    }

    /**
     * @param EntityInterface $entity
     * @return EntityInterface
     * @throws CouldNotSaveException
     */
    public function save(EntityInterface $entity)
    {
        try {
            /** @var EntityInterface|\Magento\Framework\Model\AbstractModel $entity */
            $this->resource->save($entity);
        } catch (\Exception $exception) {
            throw new CouldNotSaveException(__(
                'Could not save the entity: %1',
                $exception->getMessage()
            ));
        }
        return $entity;
    }

    /**
     * Get entity record
     *
     * @param $entityId
     * @return mixed
     * @throws NoSuchEntityException
     */
    public function getById($entityId)
    {
        if (!isset($this->instances[$entityId])) {
            /** @var \[Vendor]\[Module]\Api\Data\EntityInterface|\Magento\Framework\Model\AbstractModel $entity */
            $entity = $this->entityInterfaceFactory->create();
            $this->resource->load($entity, $entityId);
            if (!$entity->getId()) {
                throw new NoSuchEntityException(__('Requested entity doesn\'t exist'));
            }
            $this->instances[$entityId] = $entity;
        }
        return $this->instances[$entityId];
    }

    /**
     * @param SearchCriteriaInterface $searchCriteria
     * @return \[Vendor]\[Module]\Api\Data\EntitySearchResultsInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria)
    {
        /** @var \[Vendor]\[Module]\Api\Data\EntitySearchResultsInterface $searchResults */
        $searchResults = $this->searchResultsFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);

        /** @var \[Vendor]\[Module]\Model\ResourceModel\Entity\Collection $collection */
        $collection = $this->entityCollectionFactory->create();

        //Add filters from root filter group to the collection
        /** @var FilterGroup $group */
        foreach ($searchCriteria->getFilterGroups() as $group) {
            $this->addFilterGroupToCollection($group, $collection);
        }
        $sortOrders = $searchCriteria->getSortOrders();
        /** @var SortOrder $sortOrder */
        if ($sortOrders) {
            foreach ($searchCriteria->getSortOrders() as $sortOrder) {
                $field = $sortOrder->getField();
                $collection->addOrder(
                    $field,
                    ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC'
                );
            }
        } else {
            $field = 'entity_id';
            $collection->addOrder($field, 'ASC');
        }
        $collection->setCurPage($searchCriteria->getCurrentPage());
        $collection->setPageSize($searchCriteria->getPageSize());

        $data = [];
        foreach ($collection as $datum) {
            $dataDataObject = $this->entityInterfaceFactory->create();
            $this->dataObjectHelper->populateWithArray($dataDataObject, $datum->getData(), EntityInterface::class);
            $data[] = $dataDataObject;
        }
        $searchResults->setTotalCount($collection->getSize());
        return $searchResults->setItems($data);
    }

    /**
     * @param EntityInterface $entity
     * @return bool
     * @throws CouldNotSaveException
     * @throws StateException
     */
    public function delete(EntityInterface $entity)
    {
        /** @var \[Vendor]\[Module]\Api\Data\EntityInterface|\Magento\Framework\Model\AbstractModel $entity */
        $id = $entity->getId();
        try {
            unset($this->instances[$id]);
            $this->resource->delete($entity);
        } catch (ValidatorException $e) {
            throw new CouldNotSaveException(__($e->getMessage()));
        } catch (\Exception $e) {
            throw new StateException(
                __('Unable to remove entity %1', $id)
            );
        }
        unset($this->instances[$id]);
        return true;
    }

    /**
     * @param $entityId
     * @return bool
     */
    public function deleteById($entityId)
    {
        $entity = $this->getById($entityId);
        return $this->delete($entity);
    }
}

13. Add the adminhtml layout file defining the UI component grid XML file.

// app/code/[Vendor]/[Module]/view/adminhtml/layout/[module]_entity_index.xml

<?xml version="1.0"?>
<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">
            <uiComponent name="[module]_entity_grid" />
        </referenceContainer>
    </body>
</page>

14. Create the UI component grid XML file.

// app/code/[Vendor]/[Module]/view/adminhtml/ui_component/[module]_entity_grid.xml

<?xml version="1.0" encoding="UTF-8"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid_source</item>
            <item name="deps" xsi:type="string">[module]_entity_grid.[module]_entity_grid_data_source</item>
        </item>
        <item name="spinner" xsi:type="string">[module]_entity_grid_columns</item>
        <item name="buttons" xsi:type="array">
            <item name="add" xsi:type="array">
                <item name="name" xsi:type="string">add</item>
                <item name="label" xsi:type="string" translate="true">Add Entity</item>
                <item name="class" xsi:type="string">primary</item>
                <item name="url" xsi:type="string">*/*/add</item>
            </item>
        </item>
    </argument>
    <dataSource name="[module]_entity_grid_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <argument name="class" xsi:type="string">[Module]EntityGridDataProvider</argument>
            <argument name="name" xsi:type="string">[module]_entity_grid_data_source</argument>
            <argument name="primaryFieldName" xsi:type="string">entity_id</argument>
            <argument name="requestFieldName" xsi:type="string">entity_id</argument>
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="update_url" xsi:type="url" path="mui/index/render"/>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
                    <item name="storageConfig" xsi:type="array">
                        <item name="indexField" xsi:type="string">entity_id</item>
                    </item>
                </item>
            </argument>
        </argument>
    </dataSource>
    <container name="listing_top">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
               <item name="template" xsi:type="string">ui/grid/toolbar</item>
                <item name="stickyTmpl" xsi:type="string">ui/grid/sticky/toolbar</item>
            </item>
        </argument>
        <bookmark name="bookmarks">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="storageConfig" xsi:type="array">
                        <item name="namespace" xsi:type="string">[module]_entity_grid</item>
                    </item>
                </item>
            </argument>
        </bookmark>
        <component name="columns_controls">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="columnsData" xsi:type="array">
                        <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.[module]_entity_grid_columns</item>
                    </item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/controls/columns</item>
                    <item name="displayArea" xsi:type="string">entityGridActions</item>
                </item>
            </argument>
        </component>
        <filterSearch name="fulltext">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid_data_source</item>
                    <item name="chipsProvider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.listing_top.listing_filters_chips</item>
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.listing_top.bookmarks</item>
                        <item name="namespace" xsi:type="string">current.search</item>
                    </item>
                </item>
            </argument>
        </filterSearch>
        <filters name="listing_filters">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="columnsProvider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.[module]_entity_grid_columns</item>
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.listing_top.bookmarks</item>
                        <item name="namespace" xsi:type="string">current.filters</item>
                    </item>
                    <item name="templates" xsi:type="array">
                        <item name="filters" xsi:type="array">
                            <item name="select" xsi:type="array">
                                <item name="component" xsi:type="string">Magento_Ui/js/form/element/ui-select</item>
                                <item name="template" xsi:type="string">ui/grid/filters/elements/ui-select</item>
                            </item>
                        </item>
                    </item>
                    <item name="childDefaults" xsi:type="array">
                        <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.listing_top.listing_filters</item>
                        <item name="imports" xsi:type="array">
                            <item name="visible" xsi:type="string">[module]_entity_grid.[module]_entity_grid.[module]_entity_grid_columns.${ $.index }:visible</item>
                        </item>
                    </item>
                </item>
                <item name="observers" xsi:type="array">
                    <item name="column" xsi:type="string">column</item>
                </item>
            </argument>
        </filters>
        <paging name="listing_paging">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.listing_top.bookmarks</item>
                        <item name="namespace" xsi:type="string">current.paging</item>
                    </item>
                    <item name="selectProvider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.[module]_entity_grid_columns.ids</item>
                </item>
            </argument>
        </paging>
    </container>
    <columns name="[module]_entity_grid_columns">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="storageConfig" xsi:type="array">
                    <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.listing_top.bookmarks</item>
                    <item name="namespace" xsi:type="string">current</item>
                </item>
                <item name="childDefaults" xsi:type="array">
                    <item name="fieldAction" xsi:type="array">
                        <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.[module]_entity_grid_columns.actions</item>
                        <item name="target" xsi:type="string">applyAction</item>
                        <item name="params" xsi:type="array">
                            <item name="0" xsi:type="string">edit</item>
                            <item name="1" xsi:type="string">${ $.$data.rowIndex }</item>
                        </item>
                    </item>
                    <item name="controlVisibility" xsi:type="boolean">true</item>
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.listing_top.bookmarks</item>
                        <item name="root" xsi:type="string">columns.${ $.index }</item>
                        <item name="namespace" xsi:type="string">current.${ $.storageConfig.root}</item>
                    </item>
                </item>
            </item>
        </argument>
        <selectionsColumn name="ids">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="resizeEnabled" xsi:type="boolean">false</item>
                    <item name="resizeDefaultWidth" xsi:type="string">55</item>
                    <item name="indexField" xsi:type="string">entity_id</item>
                </item>
            </argument>
        </selectionsColumn>
        <column name="entity_id">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">textRange</item>
                    <item name="sorting" xsi:type="string">asc</item>
                    <item name="label" xsi:type="string" translate="true">Entity ID</item>
                </item>
            </argument>
        </column>
        <column name="name">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">text</item>
                    <item name="editor" xsi:type="array">
                        <item name="editorType" xsi:type="string">text</item>
                        <item name="validation" xsi:type="array">
                            <item name="required-entry" xsi:type="boolean">true</item>
                        </item>
                    </item>
                    <item name="label" xsi:type="string" translate="true">Name</item>
                </item>
            </argument>
        </column>
        <column name="description">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">text</item>
                    <item name="editor" xsi:type="array">
                        <item name="editorType" xsi:type="string">text</item>
                        <item name="validation" xsi:type="array">
                            <item name="required-entry" xsi:type="boolean">true</item>
                        </item>
                    </item>
                    <item name="label" xsi:type="string" translate="true">Description</item>
                </item>
            </argument>
        </column>
        <column name="is_active">
            <argument name="data" xsi:type="array">
                <item name="options" xsi:type="object">[Vendor]\[Module]\Model\Entity\Source\Status</item>
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">select</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/select</item>
                    <item name="editor" xsi:type="string">select</item>
                    <item name="dataType" xsi:type="string">select</item>
                    <item name="label" xsi:type="string" translate="true">Status</item>
                </item>
            </argument>
        </column>
        <column name="created_at" class="Magento\Ui\Component\Listing\Columns\Date">
            <argument name="data" xsi:type="array">
                <item name="js_config" xsi:type="array">
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item>
                </item>
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">dateRange</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item>
                    <item name="dataType" xsi:type="string">date</item>
                    <item name="label" xsi:type="string" translate="true">Created At</item>
                </item>
            </argument>
        </column>
        <column name="updated_at" class="Magento\Ui\Component\Listing\Columns\Date">
            <argument name="data" xsi:type="array">
                <item name="js_config" xsi:type="array">
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item>
                </item>
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">dateRange</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item>
                    <item name="dataType" xsi:type="string">date</item>
                    <item name="label" xsi:type="string" translate="true">Updated At</item>
                </item>
            </argument>
        </column>
    </columns>
</listing>

15. Add the Status.php model that will contain the ‘Enabled’ and ‘Disabled’ options for the is_active column.

// app/code/[Vendor]/[Module]/view/adminhtml/ui_component/[module]_entity_grid.xml

<?php
namespace [Vendor]\[Module]\Model\Entity\Source;

use Magento\Framework\Data\OptionSourceInterface;

class Status implements OptionSourceInterface
{
    /**
     * Get options
     *
     * @return array
     */
    public function toOptionArray()
    {
        return [
            ['value' => 1, 'label' => __('Enabled')],
            ['value' => 0, 'label' => __('Disabled')]
        ];
    }
}

16. Edit di.xml and add in the [Module]EntityGridDataProvider <virtualType> defined in [module]_entity_grid.xml. This in turn will define a [Module]EntityGridFilterPool <virtualType>.

// app/code/[Vendor]/[Module]/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="[Vendor]\[Module]\Api\DataRepositoryInterface" type="[Vendor]\[Module]\Model\EntityRepository" />
    <preference for="[Vendor]\[Module]\Api\Data\EntityInterface" type="[Vendor]\[Module]\Model\Entity" />

    <virtualType name="[Module]EntityGridDataProvider" type="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider">
        <arguments>
            <argument name="collection" xsi:type="object" shared="false">[Vendor]\[Module]\Model\Resource\Entity\Collection</argument>
            <argument name="filterPool" xsi:type="object" shared="false">[Module]EntityGridFilterPool</argument>
        </arguments>
    </virtualType>

    <virtualType name="[Module]EntityGridFilterPool" type="Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool">
        <arguments>
            <argument name="appliers" xsi:type="array">
                <item name="regular" xsi:type="object">Magento\Framework\View\Element\UiComponent\DataProvider\RegularFilter</item>
                <item name="fulltext" xsi:type="object">Magento\Framework\View\Element\UiComponent\DataProvider\FulltextFilter</item>
            </argument>
        </arguments>
    </virtualType>
</config>

17. Continue editing di.xml, adding in the [vendor]_entity_grid_data_source <type> which will define a [Vendor]\[Module]\Model\ResourceModel\Entity\Grid\Collection <virtualType>.

Note that the [Vendor]\[Module]\Model\ResourceModel\Entity\Grid\Collection class does not need to be physically created.

// app/code/[Vendor]/[Module]/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="[Vendor]\[Module]\Api\DataRepositoryInterface" type="[Vendor]\[Module]\Model\EntityRepository" />
    <preference for="[Vendor]\[Module]\Api\Data\EntityInterface" type="[Vendor]\[Module]\Model\Entity" />

    <virtualType name="[Module]EntityGridDataProvider" type="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider">
        <arguments>
            <argument name="collection" xsi:type="object" shared="false">[Vendor]\[Module]\Model\ResourceModel\Entity\Collection</argument>
            <argument name="filterPool" xsi:type="object" shared="false">[Module]EntityGridFilterPool</argument>
        </arguments>
    </virtualType>

    <virtualType name="[Module]EntityGridFilterPool" type="Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool">
        <arguments>
            <argument name="appliers" xsi:type="array">
                <item name="regular" xsi:type="object">Magento\Framework\View\Element\UiComponent\DataProvider\RegularFilter</item>
                <item name="fulltext" xsi:type="object">Magento\Framework\View\Element\UiComponent\DataProvider\FulltextFilter</item>
            </argument>
        </arguments>
    </virtualType>

    <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
        <arguments>
            <argument name="collections" xsi:type="array">
                <item name="[module]_entity_grid_data_source" xsi:type="string">[Vendor]\[Module]\Model\ResourceModel\Entity\Grid\Collection</item>
            </argument>
        </arguments>
    </type>

    <virtualType name="[Vendor]\[Module]\Model\ResourceModel\Entity\Grid\Collection" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
        <arguments>
            <argument name="mainTable" xsi:type="string">[vendor]_entity_table</argument>
            <argument name="resourceModel" xsi:type="string">[Vendor]\[Module]\Model\ResourceModel\Entity</argument>
        </arguments>
    </virtualType>
</config>

18. Refresh the cache one last time and you should notice that a grid has successfully rendered within the admin!

Add a Grid in Magento 2

Currently the Add Entity button does not have a route, and the Mass Action dropdown and Actions column have been stripped out to keep this guide as simple as possible.

The above will be included when the step-by-step guide to adding a UI form component has been written.

Note: This article is based on Magento Open Source version 2.1.8.