In the previous article, we’ve described how to create a module with the basic conditions and the interface that will let us work with them. During the creation process we used the standard Magento blocks. However, Magento 2 is capable of doing a lot more.
I’m talking about the possibility to improve the interface with the help of UI components. These components are added with the Magento/UI module.
*although these components can be found in v.2.0, we strongly recommend you to use the 2.1 version.
From this article, you’ll learn how to remake the standard Grid (located in the layout: app/code/Vendor/Rules/view/adminhtml/layout/vendor_rules_example_rule_index.xml) and enrich it with UI components.
Just compare. This is an old grid:
with a new one, made with UX components:
As you can see, an upgraded Grid will be a lot more user-friendly and time-saving, easier to scale, has a lot of great extra features (e.g. bookmarks that are capable to save the current Grid state) and set smart filters.
So, let’s start.
First, you need to make some changes in the module. Below is how to do that:
1) Create a new file to declare the necessary components:
> app/code/Vendor/Rules/etc/di.xml
<?xml version="1.0"?> <config xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <virtualType name="VendorRulesRuleGridDataProvider" type="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider"> <arguments> <argument name="collection" xsi:type="object" shared="false">Vendor\Rules\Model\ResourceModel\Rule\Collection</argument> <argument name="filterPool" xsi:type="object" shared="false">VendorRulesRuleGridFilterPool</argument> </arguments> </virtualType> <virtualType name="VendorRulesRuleGridFilterPool" 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="Vendor\Rules\Model\ResourceModel\Rule\Grid\Collection"> <arguments> <argument name="mainTable" xsi:type="string">vendor_rules</argument> <argument name="eventPrefix" xsi:type="string">vendor_rules_rule_grid_collection</argument> <argument name="eventObject" xsi:type="string">rule_grid_collection</argument> <argument name="resourceModel" xsi:type="string">Vendor\Rules\Model\ResourceModel\Rule</argument> </arguments> </type> <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> <arguments> <argument name="collections" xsi:type="array"> <item name="vendor_rules_rule_listing_data_source" xsi:type="string">Vendor\Rules\Model\ResourceModel\Rule\Grid\Collection</item> </argument> </arguments> </type> </config>
VendorRulesRuleGridDataProvide — this virtual type provides data for UI grid of rules. VendorRulesRuleGridFilterPool, in turn, adds filtering functionality that lets you add/ modify any existing filters.
Note, to make the Grid work right with this particular collection you need to add it to the list of all available collections. To do that, add vendor_rules_rule_listing_data_source with the collection value-based class: Vendor\Rules\Model\ResourceModel\Rule\Grid\Collection into Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory collections.
2) For UI Grid, we need a separate collection that will represent `Magento\Framework\Api\Search\SearchResultInterface` interface.
It contains the standard methods that will let you work with the Grid and filters. This class can be customized to your personal needs by changing the way the search by collections works.
> app/code/Vendor/Rules/Model/ResourceModel/Rule/Grid/Collection.php
<?php namespace Vendor\Rules\Model\ResourceModel\Rule\Grid; use Vendor\Rules\Model\ResourceModel\Rule\Collection as RuleCollection; use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; use Magento\Framework\Data\Collection\EntityFactory; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Psr\Log\LoggerInterface; class Collection extends RuleCollection implements SearchResultInterface { /** * Aggregations * * @var \Magento\Framework\Search\AggregationInterface */ protected $aggregations; /** * constructor * * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param $mainTable * @param $eventPrefix * @param $eventObject * @param $resourceModel * @param $model * @param $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource */ public function __construct( EntityFactory $entityFactory, LoggerInterface $logger, FetchStrategyInterface $fetchStrategy, ManagerInterface $eventManager, $mainTable, $eventPrefix, $eventObject, $resourceModel, $model = 'Magento\Framework\View\Element\UiComponent\DataProvider\Document', $connection = null, AbstractDb $resource = null ) { parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource); $this->_eventPrefix = $eventPrefix; $this->_eventObject = $eventObject; $this->_init($model, $resourceModel); $this->setMainTable($mainTable); } /** * @return \Magento\Framework\Search\AggregationInterface */ public function getAggregations() { return $this->aggregations; } /** * @param \Magento\Framework\Search\AggregationInterface $aggregations * @return $this */ public function setAggregations($aggregations) { $this->aggregations = $aggregations; } /** * Retrieve all ids for collection * Backward compatibility with EAV collection * * @param int $limit * @param int $offset * @return array */ public function getAllIds($limit = null, $offset = null) { return $this->getConnection()->fetchCol($this->_getAllIdsSelect($limit, $offset), $this->_bindParams); } /** * Get search criteria. * * @return \Magento\Framework\Api\SearchCriteriaInterface|null */ public function getSearchCriteria() { return null; } /** * Set search criteria. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function setSearchCriteria(SearchCriteriaInterface $searchCriteria = null) { return $this; } /** * Get total count. * * @return int */ public function getTotalCount() { return $this->getSize(); } /** * Set total count. * * @param int $totalCount * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function setTotalCount($totalCount) { return $this; } /** * Set items list. * * @param \Magento\Framework\Api\ExtensibleDataInterface[] $items * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function setItems(array $items = null) { return $this; } } ?>
3) Modify the main collection the way described below (it’s important, as our custom collection is inherited from it). You should make the following changes:
> app/code/Vendor/Rules/Model/ResourceModel/Rule/Collection.php
<?php namespace Vendor\Rules\Model\ResourceModel\Rule; class Collection extends \Magento\Rule\Model\ResourceModel\Rule\Collection\AbstractCollection { /** * Set resource model and determine field mapping * * @return void */ protected function _construct() { $this->_init('Vendor\Rules\Model\Rule', 'Vendor\Rules\Model\ResourceModel\Rule'); } /** * Filter collection by specified date. * Filter collection to only active rules. * * @param string|null $now * @use $this->addStoreGroupDateFilter() * @return $this */ public function setValidationFilter($now = null) { if (!$this->getFlag('validation_filter')) { $this->addDateFilter($now); $this->addIsActiveFilter(); $this->setOrder('sort_order', self::SORT_ORDER_DESC); $this->setFlag('validation_filter', true); } return $this; } /** * From date or to date filter * * @param $now * @return $this */ public function addDateFilter($now) { $this->getSelect()->where( 'from_date is null or from_date <= ?', $now )->where( 'to_date is null or to_date >= ?', $now ); return $this; } } ?>
4) Next, remove the old markup from the Grid layout and add a UI-listing there:
> app/code/Vendor/Rules/view/adminhtml/layout/vendor_rules_example_rule_index.xml
<?xml version="1.0"?> <page xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceBlock name="menu"> <action method="setActive"> <argument name="itemId" xsi:type="string">Vendor_Rules::vendor_rules</argument> </action> </referenceBlock> <referenceBlock name="page.title"> <action method="setTitleClass"> <argument name="class" xsi:type="string">complex</argument> </action> </referenceBlock> <referenceContainer name="content"> <uiComponent name="vendor_rules_rule_listing"/> </referenceContainer> </body> </page>
Basically, we just add the mentioned `vendor_rules_rule_listing` into the page content (the main action), change the status of our product menu to ‘Active’, and set the title class.
5) At the next step, we crate the UI listing, that will be placed here:
> app/code/Vendor/Rules/view/adminhtml/ui_component/vendor_rules_rule_listing.xml
<?xml version="1.0"?> <listing xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Ui/etc/ui_configuration.xsd"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="provider" xsi:type="string">vendor_rules_rule_listing.vendor_rules_rule_listing_data_source</item> <item name="deps" xsi:type="string">vendor_rules_rule_listing.vendor_rules_rule_listing_data_source</item> </item> <item name="spinner" xsi:type="string">vendor_rules_rule_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 New Rule</item> <item name="class" xsi:type="string">primary</item> <item name="url" xsi:type="string">*/*/newaction</item> </item> </item> </argument> <dataSource name="vendor_rules_rule_listing_data_source"> <argument name="dataProvider" xsi:type="configurableObject"> <argument name="class" xsi:type="string">VendorRulesRuleGridDataProvider</argument> <argument name="name" xsi:type="string">vendor_rules_rule_listing_data_source</argument> <argument name="primaryFieldName" xsi:type="string">rule_id</argument> <argument name="requestFieldName" xsi:type="string">rule_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> </argument> </argument> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item> </item> </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">vendor_rules_rule_listing</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">vendor_rules_rule_listing.vendor_rules_rule_listing.vendor_rules_rule_columns</item> </item> <item name="component" xsi:type="string">Magento_Ui/js/grid/controls/columns</item> <item name="displayArea" xsi:type="string">dataGridActions</item> </item> </argument> </component> <filters name="listing_filters"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="columnsProvider" xsi:type="string">vendor_rules_rule_listing.vendor_rules_rule_listing.vendor_rules_rule_columns</item> <item name="storageConfig" xsi:type="array"> <item name="provider" xsi:type="string">vendor_rules_rule_listing.vendor_rules_rule_listing.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">vendor_rules_rule_listing.vendor_rules_rule_listing.listing_top.listing_filters</item> <item name="imports" xsi:type="array"> <item name="visible" xsi:type="string">vendor_rules_rule_listing.vendor_rules_rule_listing.vendor_rules_rule_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">vendor_rules_rule_listing.vendor_rules_rule_listing.listing_top.bookmarks</item> <item name="namespace" xsi:type="string">current.paging</item> </item> <item name="selectProvider" xsi:type="string">vendor_rules_rule_listing.vendor_rules_rule_listing.vendor_rules_rule_columns.ids</item> </item> </argument> </paging> </container> <columns name="vendor_rules_rule_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">vendor_rules_rule_listing.vendor_rules_rule_listing.listing_top.bookmarks</item> <item name="namespace" xsi:type="string">current</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">rule_id</item> </item> </argument> </selectionsColumn> <column name="rule_id"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">textRange</item> <item name="dataType" xsi:type="string">text</item> <item name="sorting" xsi:type="string">asc</item> <item name="align" xsi:type="string">left</item> <item name="label" xsi:type="string" translate="true">ID</item> <item name="sortOrder" xsi:type="number">1</item> </item> </argument> </column> <column name="name"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">text</item> <item name="dataType" xsi:type="string">text</item> <item name="align" xsi:type="string">left</item> <item name="label" xsi:type="string" translate="true">Name</item> </item> </argument> </column> <column name="is_active"> <argument name="data" xsi:type="array"> <item name="options" xsi:type="array"> <item name="active" xsi:type="array"> <item name="value" xsi:type="string">1</item> <item name="label" xsi:type="string" translate="true">Active</item> </item> <item name="inactive" xsi:type="array"> <item name="value" xsi:type="string">0</item> <item name="label" xsi:type="string" translate="true">Inactive</item> </item> </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">Is Active</item> <item name="sortOrder" xsi:type="number">65</item> </item> </argument> </column> <column name="sort_order"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/column</item> </item> <item name="config" xsi:type="array"> <item name="filter" xsi:type="string">text</item> <item name="dataType" xsi:type="string">number</item> <item name="align" xsi:type="string">left</item> <item name="label" xsi:type="string" translate="true">Priority</item> </item> </argument> </column> <column name="from_date" class="Magento\Ui\Component\Listing\Columns\Date"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="sorting" xsi:type="string">desc</item> <item name="filter" xsi:type="string">dateRange</item> <item name="dataType" xsi:type="string">date</item> <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item> <item name="label" xsi:type="string" translate="true">Start</item> </item> </argument> </column> <column name="to_date" class="Magento\Ui\Component\Listing\Columns\Date"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="sorting" xsi:type="string">desc</item> <item name="filter" xsi:type="string">dateRange</item> <item name="dataType" xsi:type="string">date</item> <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item> <item name="label" xsi:type="string" translate="true">End</item> </item> </argument> </column> <actionsColumn name="actions" class="Vendor\Rules\Ui\Component\Listing\Column\RuleActions"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="indexField" xsi:type="string">rule_id</item> <item name="urlEntityParamName" xsi:type="string">rule_id</item> <item name="sortOrder" xsi:type="number">70</item> </item> </argument> </actionsColumn> </columns> <container name="sticky"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="component" xsi:type="string">Magento_Ui/js/grid/sticky/sticky</item> <item name="toolbarProvider" xsi:type="string">vendor_rules_rule_listing.vendor_rules_rule_listing.listing_top</item> <item name="listingProvider" xsi:type="string">vendor_rules_rule_listing.vendor_rules_rule_listing.vendor_rules_rule_columns</item> </item> </argument> </container> </listing>
Listing `dataSource`contains a link to `dataProvider` – the type we’ve created in `di.xml`. It contains the input data that is necessary to for the Grid.
From here, we need to define the key filed — in our case it is `rule_id`.
Additionally, you can also add your custom buttons, just describe them in the ‘buttons’ section. In our example, we’ve added the standard ‘Add’ button with an address `*/*/newaction` ( * in the path corresponds to the current meaning).
`container name=”listing_top”` contains some extra listing components: filters, pagination,etc. You can change them according to your personal requirements.
`columns name=“vendor_rules_rule_columns”` contains the column that are almost the same, as in the default Grid. The only meaningful difference is a new `actionsColumn` column that introduces a set of actions: edit and delete. These actions can also be extended, if needed.
As you’ve probably noticed, this column has a new class. Let’s learn how we can create it:
> app/code/Vendor/Rules/Ui/Component/Listing/Column/RuleActions.php
<?php namespace Vendor\Rules\Ui\Component\Listing\Column; class RuleActions extends \Magento\Ui\Component\Listing\Columns\Column { /** * Url path to edit * * @var string */ const URL_PATH_EDIT = 'vendor_rules/example_rule/edit'; /** * Url path to delete * * @var string */ const URL_PATH_DELETE = 'vendor_rules/example_rule/delete'; /** * URL builder * * @var \Magento\Framework\UrlInterface */ protected $urlBuilder; /** * Constructor * * @param \Magento\Framework\UrlInterface $urlBuilder * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context * @param \Magento\Framework\View\Element\UiComponentFactory $uiComponentFactory * @param array $components * @param array $data */ public function __construct( \Magento\Framework\UrlInterface $urlBuilder, \Magento\Framework\View\Element\UiComponent\ContextInterface $context, \Magento\Framework\View\Element\UiComponentFactory $uiComponentFactory, array $components = [], array $data = [] ) { $this->urlBuilder = $urlBuilder; parent::__construct($context, $uiComponentFactory, $components, $data); } /** * Prepare Data Source * * @param array $dataSource * @return array */ public function prepareDataSource(array $dataSource) { if (!isset($dataSource['data']['items'])) { return $dataSource; } foreach ($dataSource['data']['items'] as &$item) { if (!isset($item['rule_id'])) { continue; } $item[$this->getData('name')] = [ 'edit' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_EDIT, [ 'id' => $item['rule_id'], ] ), 'label' => __('Edit'), ], 'delete' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_DELETE, [ 'id' => $item['rule_id'], ] ), 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete "${ $.$data.name }"'), 'message' => __('Are you sure you wan\'t to delete the rule "${ $.$data.name }" ?'), ], ], ]; } return $dataSource; } } ?>
This class is in charge of processing actions from the Grid. From here, it’s possible to change URLs or the name of the transmitted parameter. In our case, the key is rule_id (it’s transmitted under the name ‘id’, to ease the understanding of its value).
It looks like this:
If you’ve done everything the right way, your grid should look like this:
As we can see from the example, it quite easy to transform the existing standard Grid into UI. In addition to extending the standard Grid functionality, it also allows you to simplify working with this part of Magento 2 functionality.
P.S. In the next article of the series, we are going to describe the possibility of adding mass—actions and extra columns.
Stay tuned! 😉