Then we will see how to construct UI Components, introduced in Magento 2.0, and how to create definition xml, controllers, layouts and a new menu item. Finally we will describe how to create, edit, save and delete data belonging to the module.

This article will discuss the following topics:

1) Creating admin menu and grid

In the first step, we create the menu item in the admin area belonging to our module. We can have it in a new main menu, but it may be better to place it under a relevant main menu item. Here we place it under the Content main menu. For this, we need a new file. We create the menu in the app/code/Aion/Test/etc/adminhtml/ directory, in the menu.xml. The file contains the following:

<?xml version="1.0"?> <!-- /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> <add id="Aion_Test::content_elements" title="Test Extension" module="Magento_Backend" sortOrder="10" parent="Magento_Backend::content" resource="Magento_Backend::content_elements"/> <add id="Aion_Test::aion_test" title="Manage Items" module="Aion_Test" sortOrder="10" parent="Aion_Test::content_elements" action="test/test" resource="Aion_Test::test"/> </menu> </config>

First we define a main element in the file, namely the id=Aion_Test::content_elements and then place the menu item after this element in such a way that we define the parent of this element (id=Aion_Test::aion_test) as the main element. If everything has been executed properly, we can see our sub-menu item under the main menu Content in the admin area. Here it is important to mention the Action=”test/test” parameter, which will define the path of the adminhtml controller to be created later. Next we need to create the adminhtml controller and the layout file that will be responsible for displaying the grid. But, before that, we create an abstract controller class in order to be able to manage backend user roles in one place. We create the abstract controller class in the Test.php located in the app/code/Aion/Test/Controller/Adminhtml/ directory. The file contains the following:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml; /** * Aion manage items controller */ abstract class Test extends \Magento\Backend\App\Action { /** * Core registry * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry */ public function __construct(\Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry) { $this->_coreRegistry = $coreRegistry; parent::__construct($context); } /** * Init page * * @param \Magento\Backend\Model\View\Result\Page $resultPage * @return \Magento\Backend\Model\View\Result\Page */ protected function initPage($resultPage) { $resultPage->setActiveMenu('Aion_Test::aion_test') ->addBreadcrumb(__('Test'), __('Test')) ->addBreadcrumb(__('Items'), __('')); return $resultPage; } /** * Check the permission to run it * * @return boolean */ protected function _isAllowed() { return $this->_authorization->isAllowed('Aion_Test::test_menu'); } }

The initPage() function of the abstract controller is responsible for the setting of the active menu item as well as defining the breadcrumb path. The other significant function is _isAllowed(), which checks and controls admin roles. Next we create the controller needed for managing the admin grid, which we will extend from the previously mentioned abstract controller. We create the controller class in the Index.php located in the app/code/Aion/Test/Controller/Adminhtml/Test directory. The file includes the following:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml\Test; class Index extends \Aion\Test\Controller\Adminhtml\Test { /** * @var \Magento\Framework\View\Result\PageFactory */ protected $resultPageFactory; /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Framework\View\Result\PageFactory $resultPageFactory ) { $this->resultPageFactory = $resultPageFactory; parent::__construct($context, $coreRegistry); } /** * Index action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); $this->initPage($resultPage)->getConfig()->getTitle()->prepend(__('Items')); return $resultPage; } }

The Index controller is responsible for displaying the admin grid. In Magento 2.0 every controller class (file) is an action indeed. This means that in our case, the IndexAction() function, known from Magento 1.x, is replaced by the execute() function. Consequently, for every controller action there is a separate controller file and one execute() function. This may immediately raise the question: what is it good for? Basically, thanks to this, the code of the whole module is much clearer than in the case of controllers in the Magento 1.x system, which often resulted in a lengthy script at the end of the development process. The $resultPage and $this->resultPageFactory replace the $this->loadLayout() and $this->renderLayout() calls, known from Magento 1.x. We need to define the path (or route) of the created adminhtml controllers in a separate file so that Magento 2.0 can “recognize” it. We define the path in the routes.xml in the app/code/Aion/Test/etc/adminhtml/ directory. The file contains the following:

<?xml version="1.0"?> <!-- /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> <router id="admin"> <route id="test" frontName="test"> <module name="Aion_Test" before="Magento_Backend" /> </route> </router> </config>

There are two important tags and parameters belonging to them in the file. The first one is <router id=”admin”>, which indicates that it is a backend path. The second one is <route id=”test” frontName=”test”>, where the frontName defines the main path of the created adminhtml controllers. Next, we need to create the collection, which will feed data to the admin grid mentioned previously. We create the collection in the Collection.php located in the app/code/Aion/Model/ResourceModel/Test/Grid/ directory. The file includes the following:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Model\ResourceModel\Test\Grid; use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Search\AggregationInterface; use Aion\Test\Model\ResourceModel\Test\Collection as TestCollection; /** * Collection for displaying grid of Aion Items */ class Collection extends TestCollection implements SearchResultInterface { /** * @var AggregationInterface */ protected $aggregations; /** * @param \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param string $mainTable * @param string $eventPrefix * @param string $eventObject * @param string $resourceModel * @param string $model * @param string|null $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory, \Psr\Log\LoggerInterface $logger, \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Store\Model\StoreManagerInterface $storeManager, $mainTable, $eventPrefix, $eventObject, $resourceModel, $model = 'Magento\Framework\View\Element\UiComponent\DataProvider\Document', $connection = null, \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null ) { parent::__construct( $entityFactory, $logger, $fetchStrategy, $eventManager, $storeManager, $connection, $resource ); $this->_eventPrefix = $eventPrefix; $this->_eventObject = $eventObject; $this->_init($model, $resourceModel); $this->setMainTable($mainTable); } /** * @return AggregationInterface */ public function getAggregations() { return $this->aggregations; } /** * @param 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(\Magento\Framework\Api\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; } }

The class defined in the file is responsible for adding the data in the table to be created, as well as implementing search and paging functions. Implementing the functions, mentioned above, is needed for the proper functioning of the UI Components to be described here. The other advantage of this is that the class we define here can be used in other locations easily, if we want to display the module’s data in an admin grid somewhere else, e.g. on a product or customer ajax tab in the admin panel. There is only one thing left, which is to create the layout file belonging to the Index controller. We create the layout file in the test_test_index.xml located in the app/code/Aion/Test/view/adminhtml/layout/ directory. The previously defined route is clearly visible in the file’s name: basic route route -> directory -> controller action. The file includes the following:

<?xml version="1.0"?> <!-- /** * Copyright © 2015 Magento. All rights reserved. * See COPYING.txt for license details. */ --> <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="test_test_listing"/> </referenceContainer> </body> </page>

The name of the previously mentioned UI component file is defined in the “content” reference container located in the layout file. This will be detailed in the next section.

2) New design of UI Components or admin grid

We can create admin grids much more easily by using UI components introduced in Magento 2.0. Furthermore, this opens up more possibilities for the administrator in terms of making searches, filtering and displaying columns in a custom manner in the tables. Additionally, we can save different designs or views. In order to make the UI components functional, we need to create several files and implement them properly. The most important file, which defines the operation and design of the admin grid, is an xml file. In our module, this is named as test_test_listing.xml (see previous section) and is located in the app/code/Aion/Test/view/adminhtml/ui_component directory. This file is quite lengthy so we show it in separate sections.

<?xml version="1.0" encoding="UTF-8"?> <!-- /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ --> <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">test_test_listing.test_test_listing_data_source</item> <item name="deps" xsi:type="string">test_test_listing.test_test_listing_data_source</item> </item> <item name="spinner" xsi:type="string">test_test_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 Item</item> <item name="class" xsi:type="string">primary</item> <item name="url" xsi:type="string">*/*/new</item> </item> </item> </argument> <dataSource name="test_test_listing_data_source"> <argument name="dataProvider" xsi:type="configurableObject"> <argument name="class" xsi:type="string">TestGridDataProvider</argument> <argument name="name" xsi:type="string">test_test_listing_data_source</argument> <argument name="primaryFieldName" xsi:type="string">test_id</argument> <argument name="requestFieldName" xsi:type="string">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> …

The following are defined in the first argument tag of the file:

data source name (test_test_listing_data_source), see: second dataSource tag: <dataSource name=”test_test_listing_data_source”>

colums tag name: test_test_columns, this will be needed later on

adding new element and defining other buttons, see: <item name=”buttons” xsi:type=”array”> tag

… <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> </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">test_test_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">test_test_listing.test_test_listing.test_test_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> …

Working further on in the xml file, we define the functions placed above the table. These are the following:

we can save the “look” of the present table in different views, see: <bookmark name=”bookmarks”> tag

if there are too many columns, we can define which ones should be shown and we can save this at bookmarks mentioned before, see: <component name=”columns_controls”> tag

… <filterSearch name="fulltext"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="provider" xsi:type="string">test_test_listing.test_test_listing_data_source</item> <item name="chipsProvider" xsi:type="string">test_test_listing.test_test_listing.listing_top.listing_filters_chips</item> <item name="storageConfig" xsi:type="array"> <item name="provider" xsi:type="string">test_test_listing.test_test_listing.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">test_test_listing.test_test_listing.test_test_columns</item> <item name="storageConfig" xsi:type="array"> <item name="provider" xsi:type="string">test_test_listing.test_test_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">test_test_listing.test_test_listing.listing_top.listing_filters</item> <item name="imports" xsi:type="array"> <item name="visible" xsi:type="string">test_test_listing.test_test_listing.test_test_columns.${ $.index }:visible</item> </item> </item> </item> </argument> </filters> …

We add the text based search function and table filters. These are the following:

in varchar, text type columns, we can search within an input field, see: <filterSearch name=”fulltext”> tag

we can filter every single column according to different parameters view(Aion\Test\Ui\Component\Listing\Column\Test\Options), select, date, ID(range), text type filters, see: <filters name=”listing_filters”> tag

… <massaction name="listing_massaction"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="selectProvider" xsi:type="string">test_test_listing.test_test_listing.test_test_columns.ids</item> <item name="indexField" xsi:type="string">test_id</item> </item> </argument> <action name="delete"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="type" xsi:type="string">delete</item> <item name="label" xsi:type="string" translate="true">Delete</item> <item name="url" xsi:type="url" path="test/test/massDelete"/> <item name="confirm" xsi:type="array"> <item name="title" xsi:type="string" translate="true">Delete items</item> <item name="message" xsi:type="string" translate="true">Are you sure you wan't to delete selected items?</item> </item> </item> </argument> </action> <action name="disable"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="type" xsi:type="string">disable</item> <item name="label" xsi:type="string" translate="true">Disable</item> <item name="url" xsi:type="url" path="test/test/massDisable"/> </item> </argument> </action> <action name="enable"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="type" xsi:type="string">enable</item> <item name="label" xsi:type="string" translate="true">Enable</item> <item name="url" xsi:type="url" path="test/test/massEnable"/> </item> </argument> </action> </massaction> …

The functions that we have created, needed to change mass data, are added within the massaction tag. These are the following in our module:

mass delete, see: <action name=”delete”> tag

mass enable and disable, see: <action name=”disable”> and <action name=”enable”> tags. These modify the is_active data, which was created earlier in the database table of our module.

… <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">test_test_listing.test_test_listing.listing_top.bookmarks</item> <item name="namespace" xsi:type="string">current.paging</item> </item> <item name="selectProvider" xsi:type="string">test_test_listing.test_test_listing.test_test_columns.ids</item> </item> </argument> </paging> </container> …

The <paging name=”listing_paging”> tag implements paging and the selectablitiy of the number of the listed elements (select) in our table.

… <columns name="test_test_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">test_test_listing.test_test_listing.listing_top.bookmarks</item> <item name="namespace" xsi:type="string">current</item> </item> </item> <item name="childDefaults" xsi:type="array"> <item name="fieldAction" xsi:type="array"> <item name="provider" xsi:type="string">test_test_listing.test_test_listing.test_test_columns_editor</item> <item name="target" xsi:type="string">startEdit</item> <item name="params" xsi:type="array"> <item name="0" xsi:type="string">${ $.$data.rowIndex }</item> <item name="1" xsi:type="boolean">true</item> </item> </item> <item name="storageConfig" xsi:type="array"> <item name="provider" xsi:type="string">test_test_listing.test_test_listing.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="indexField" xsi:type="string">test_id</item> </item> </argument> </selectionsColumn> …

Now we define the columns of the table, see: <columns name=”test_test_columns”> tag. Its name was defined at the beginning of the file. The ID field, set with the mass actions, mentioned earlier, see: <selectionsColumn name=”ids”> tag.

… <column name="test_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">ID</item> </item> </argument> </column> <column name="name"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <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="filter" xsi:type="string">text</item> <item name="label" xsi:type="string" translate="true">Name</item> </item> </argument> </column> <column name="email"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <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="filter" xsi:type="string">text</item> <item name="label" xsi:type="string" translate="true">Email</item> </item> </argument> </column> <column name="creation_time" class="Magento\Ui\Component\Listing\Columns\Date"> <argument name="data" xsi:type="array"> <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</item> </item> </argument> </column> <column name="update_time" class="Magento\Ui\Component\Listing\Columns\Date"> <argument name="data" xsi:type="array"> <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">Modified</item> </item> </argument> </column> <column name="sort_order"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <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="filter" xsi:type="string">text</item> <item name="label" xsi:type="string" translate="true">Sort Order</item> </item> </argument> </column> <column name="is_active"> <argument name="data" xsi:type="array"> <item name="options" xsi:type="array"> <item name="disable" xsi:type="array"> <item name="value" xsi:type="string">0</item> <item name="label" xsi:type="string" translate="true">Disabled</item> </item> <item name="enable" xsi:type="array"> <item name="value" xsi:type="string">1</item> <item name="label" xsi:type="string" translate="true">Enabled</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">Status</item> </item> </argument> </column> <actionsColumn name="actions" class="Aion\Test\Ui\Component\Listing\Column\TestActions"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="indexField" xsi:type="string">test_id</item> </item> </argument> </actionsColumn> </columns> </listing>

Next we need to define the columns in the table. With each column we can set the type, e.g. text, select, textRange etc. The last column includes the basic actions, see: <actionsColumn name=”actions” class=”Aion\Test\Ui\Component\Listing\Column\TestActions”> tag Now we have finished with the xml defining the grid (test_test_listing.xml). Now we will take a look at some classes which are responsible for the actions located in the last column.

3) UI component classes

For the functioning of the action column located in the grid defining xml, created in the previous section, we need a class which assists in displaying and functioning. The first one is the TestActions class seen in the previous section tag, <actionsColumn name=”actions” class=”Aion\Test\Ui\Component\Listing\Column\TestActions”>. The file is named as TestActions.php located in the app/code/Aion/Test/Ui/Component/Listing/Column directory. The file contains the following:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Ui\Component\Listing\Column; use Magento\Framework\UrlInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Ui\Component\Listing\Columns\Column; /** * Class TestActions */ class TestActions extends Column { /** * Url path */ const URL_PATH_EDIT = 'test/test/edit'; const URL_PATH_DELETE = 'test/test/delete'; /** * @var UrlInterface */ protected $urlBuilder; /** * Constructor * * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param UrlInterface $urlBuilder * @param array $components * @param array $data */ public function __construct( ContextInterface $context, UiComponentFactory $uiComponentFactory, UrlInterface $urlBuilder, 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'])) { foreach ($dataSource['data']['items'] as & $item) { if (isset($item['test_id'])) { $item[$this->getData('name')] = [ 'edit' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_EDIT, [ 'test_id' => $item['test_id'] ] ), 'label' => __('Edit') ], 'delete' => [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_DELETE, [ 'test_id' => $item['test_id'] ] ), 'label' => __('Delete'), 'confirm' => [ 'title' => __('Delete "${ $.$data.name }"'), 'message' => __('Are you sure you wan\'t to delete a "${ $.$data.name }" record?') ] ] ]; } } } return $dataSource; } }

The class creates the array in the appropriate format, necessary for displaying the mass action. It is important to define the precise path with the constant values at the beginning of the file in order to direct to the proper adminhtml controllers.

4) Adminhtml controllers

For the complete functioning of the grid, a few controllers need to be created. Let’s see them one by one. For mass deletion, we use the massDelete controller. The file is named as MassDelete.php located in the app/code/Aion/Test/Controller/Adminhtml/Test/ directory. The file includes the following:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml\Test; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Aion\Test\Model\ResourceModel\Test\CollectionFactory; /** * Class MassDelete */ class MassDelete extends \Magento\Backend\App\Action { /** * @var Filter */ protected $filter; /** * @var CollectionFactory */ protected $collectionFactory; /** * @param Context $context * @param Filter $filter * @param CollectionFactory $collectionFactory */ public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory) { $this->filter = $filter; $this->collectionFactory = $collectionFactory; parent::__construct($context); } /** * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect * @throws \Magento\Framework\Exception\LocalizedException|\Exception */ public function execute() { $collection = $this->filter->getCollection($this->collectionFactory->create()); $collectionSize = $collection->getSize(); foreach ($collection as $item) { $item->delete(); } $this->messageManager->addSuccess(__('A total of %1 record(s) have been deleted.', $collectionSize)); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); return $resultRedirect->setPath('*/*/'); } }

The execute() function of the controller class (namely the action) is given a controller (from the \Magento\Ui\Component\MassAction\Filter class), deleting the items iterating through it. For modifying the mass status, we use the massEnable and massDisable controllers. The files are named as MassEnable.php and MassDisable.php located in the app/code/Aion/Test/Controller/Adminhtml/Test/ directory. The files contain the following:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml\Test; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Aion\Test\Model\ResourceModel\Test\CollectionFactory; /** * Class MassEnable */ class MassEnable extends \Magento\Backend\App\Action { /** * @var Filter */ protected $filter; /** * @var CollectionFactory */ protected $collectionFactory; /** * @param Context $context * @param Filter $filter * @param CollectionFactory $collectionFactory */ public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory) { $this->filter = $filter; $this->collectionFactory = $collectionFactory; parent::__construct($context); } /** * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect * @throws \Magento\Framework\Exception\LocalizedException|\Exception */ public function execute() { $collection = $this->filter->getCollection($this->collectionFactory->create()); foreach ($collection as $item) { $item->setIsActive(true); $item->save(); } $this->messageManager->addSuccess(__('A total of %1 record(s) have been enabled.', $collection->getSize())); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); return $resultRedirect->setPath('*/*/'); } }

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml\Test; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Ui\Component\MassAction\Filter; use Aion\Test\Model\ResourceModel\Test\CollectionFactory; /** * Class MassDisable */ class MassDisable extends \Magento\Backend\App\Action { /** * @var Filter */ protected $filter; /** * @var CollectionFactory */ protected $collectionFactory; /** * @param Context $context * @param Filter $filter * @param CollectionFactory $collectionFactory */ public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory) { $this->filter = $filter; $this->collectionFactory = $collectionFactory; parent::__construct($context); } /** * Execute action * * @return \Magento\Backend\Model\View\Result\Redirect * @throws \Magento\Framework\Exception\LocalizedException|\Exception */ public function execute() { $collection = $this->filter->getCollection($this->collectionFactory->create()); foreach ($collection as $item) { $item->setIsActive(false); $item->save(); } $this->messageManager->addSuccess(__('A total of %1 record(s) have been disabled.', $collection->getSize())); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); return $resultRedirect->setPath('*/*/'); } }

The two types of functioning of the two controllers are very similar. Both iterate through the collection provided by the Filter class and set the is_active data key to TRUE in case of massEnbale, and to FALSE in case of massDisable, and then save the elements of the collection.

5) Object manager configuration

For the proper functioning of the admin grid, we need to define the source data objects and filters. For this, we need a defining xml. The file is located in the app/code/Aion/Test/etc/ directory, named as di.xml. The file includes the following:

<?xml version="1.0"?> <!-- /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> <arguments> <argument name="collections" xsi:type="array"> <item name="test_test_listing_data_source" xsi:type="string">Aion\Test\Model\ResourceModel\Test\Grid\Collection</item> </argument> </arguments> </type> <type name="Aion\Test\Model\ResourceModel\Test\Grid\Collection"> <arguments> <argument name="mainTable" xsi:type="string">aion_test</argument> <argument name="eventPrefix" xsi:type="string">aion_test_grid_collection</argument> <argument name="eventObject" xsi:type="string">test_grid_collection</argument> <argument name="resourceModel" xsi:type="string">Aion\Test\Model\ResourceModel\Test</argument> </arguments> </type> <virtualType name="TestGirdFilterPool" 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> <virtualType name="TestGridDataProvider" type="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider"> <arguments> <argument name="collection" xsi:type="object" shared="false">Aion\Test\Model\ResourceModel\Test\Collection</argument> <argument name="filterPool" xsi:type="object" shared="false">TestGirdFilterPool</argument> </arguments> </virtualType> </config>

Here we define the collection needed for the grid (see: <item name=”test_test_listing_data_source” xsi:type=”string”>Aion\Test\Model\ResourceModel\Test\Grid\Collection</item>), the filter and data provider that are necessary for the proper functioning of the UI component. We describe editing, saving and deleting of the different elements in the following sections.

6) Creating admin blocks necessary for editing

In order to be able to create the data, in the admin panel, belonging to the module and also be able to edit them, we need the appropriate classes. First, we need to create the container class, which will contain the form later on. We create the class in the Test.php file located in the Aion/Test/Block/Adminhtml/ directory:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Block\Adminhtml; /** * Adminhtml Aion items content block */ class Test extends \Magento\Backend\Block\Widget\Grid\Container { /** * @return void */ protected function _construct() { $this->_blockGroup = 'Aion_Test'; $this->_controller = 'adminhtml_test'; $this->_headerText = __('Items'); $this->_addButtonLabel = __('Add New Item'); parent::_construct(); } }

It is important to define the proper blockGroup and controller. We now need the form container class. Here we define the title of the admin page of the edited object and can add or remove custom buttons to it besides the “basic” buttons. We create the class in the Edit.php file located in the Aion/Test/Block/Adminhtml/Test directory:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Block\Adminhtml\Test; /** * Aion item edit form container */ class Edit extends \Magento\Backend\Block\Widget\Form\Container { /** * Core registry * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; /** * @param \Magento\Backend\Block\Widget\Context $context * @param \Magento\Framework\Registry $registry * @param array $data */ public function __construct( \Magento\Backend\Block\Widget\Context $context, \Magento\Framework\Registry $registry, array $data = [] ) { $this->_coreRegistry = $registry; parent::__construct($context, $data); } /** * @return void */ protected function _construct() { $this->_objectId = 'test_id'; $this->_blockGroup = 'Aion_Test'; $this->_controller = 'adminhtml_test'; parent::_construct(); $this->buttonList->update('save', 'label', __('Save Item')); $this->buttonList->update('delete', 'label', __('Delete Item')); $this->buttonList->add( 'saveandcontinue', [ 'label' => __('Save and Continue Edit'), 'class' => 'save', 'data_attribute' => [ 'mage-init' => ['button' => ['event' => 'saveAndContinueEdit', 'target' => '#edit_form']], ] ], -100 ); } /** * Get edit form container header text * * @return \Magento\Framework\Phrase */ public function getHeaderText() { if ($this->_coreRegistry->registry('test_item')->getId()) { return __("Edit Block '%1'", $this->escapeHtml($this->_coreRegistry->registry('test_item')->getName())); } else { return __('New Item'); } } }

If we want to use WYSWYG editor with textarea type fields for example, then it needs to be placed in the _construct() function or in the prepareLayout() function. The title value of the admin page is defined by the getHeaderText() function in the class. The last block that needs to be created displays and manages the form. We create the class in the Form.php file located in the Aion/Test/Block/Adminhtml/Test/Edit directory:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Block\Adminhtml\Test\Edit; /** * Adminhtml Aion item edit form */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { /** * @var \Magento\Cms\Model\Wysiwyg\Config */ protected $_wysiwygConfig; /** * @var \Magento\Store\Model\System\Store */ protected $_systemStore; /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Data\FormFactory $formFactory * @param \Magento\Cms\Model\Wysiwyg\Config $wysiwygConfig * @param \Magento\Store\Model\System\Store $systemStore * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Registry $registry, \Magento\Framework\Data\FormFactory $formFactory, \Magento\Cms\Model\Wysiwyg\Config $wysiwygConfig, \Magento\Store\Model\System\Store $systemStore, array $data = [] ) { $this->_wysiwygConfig = $wysiwygConfig; $this->_systemStore = $systemStore; parent::__construct($context, $registry, $formFactory, $data); } /** * Init form * * @return void */ protected function _construct() { parent::_construct(); $this->setId('test_form'); $this->setTitle(__('Item Information')); } /** * Prepare form * * @return $this */ protected function _prepareForm() { $model = $this->_coreRegistry->registry('test_item'); /** @var \Magento\Framework\Data\Form $form */ $form = $this->_formFactory->create( ['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']] ); $form->setHtmlIdPrefix('item_'); $fieldset = $form->addFieldset( 'base_fieldset', ['legend' => __('General Information'), 'class' => 'fieldset-wide'] ); if ($model->getId()) { $fieldset->addField('test_id', 'hidden', ['name' => 'test_id']); } $fieldset->addField( 'name', 'text', [ 'name' => 'name', 'label' => __('Name'), 'title' => __('Name'), 'required' => true ] ); $fieldset->addField( 'email', 'text', [ 'name' => 'email', 'label' => __('Email'), 'title' => __('Email'), 'required' => true, 'class' => 'validate-email' ] ); $fieldset->addField( 'is_active', 'select', [ 'label' => __('Status'), 'title' => __('Status'), 'name' => 'is_active', 'required' => true, 'options' => ['1' => __('Enabled'), '0' => __('Disabled')] ] ); if (!$model->getId()) { $model->setData('is_active', '1'); } $fieldset->addField( 'sort_order', 'text', [ 'name' => 'sort_order', 'label' => __('Sort Order'), 'title' => __('Sort Order'), 'required' => false ] ); $form->setValues($model->getData()); $form->setUseContainer(true); $this->setForm($form); return parent::_prepareForm(); } }

We add the fields we want to edit in the _prepareForm() function of the class. These, in our case, are the name, email and sort_order fields. Additionally, there is the store_id field (important for multistore management) and the is_active field, which is of select type at present and is used for setting the status of the element which is being edited. Having finished with the above three classes, we have created the files necessary for the editing functions in the admin panel.

7) Creating controllers and layout

Apart from the classes mentioned previously, we still need the appropriate controller classes and layout files for the editing process. We create the first class in the NewAction.php file in the Aion/Test/Controller/Adminhtml/Test/ directory.

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml\Test; class NewAction extends \Aion\Test\Controller\Adminhtml\Test { /** * @var \Magento\Backend\Model\View\Result\ForwardFactory */ protected $resultForwardFactory; /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory ) { $this->resultForwardFactory = $resultForwardFactory; parent::__construct($context, $coreRegistry); } /** * Create new item * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { /** @var \Magento\Framework\Controller\Result\Forward $resultForward */ $resultForward = $this->resultForwardFactory->create(); return $resultForward->forward('edit'); } }

The class serves the creation of new elements and basically the function of the action (execute()) redirects to the Edit controller class. Next we create the controller needed for editing. We create the class in the Edit.php file located in the Aion/Test/Controller/Adminhtml/Test/ directory:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml\Test; class Edit extends \Aion\Test\Controller\Adminhtml\Test { /** * @var \Magento\Framework\View\Result\PageFactory */ protected $resultPageFactory; /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry, \Magento\Framework\View\Result\PageFactory $resultPageFactory ) { $this->resultPageFactory = $resultPageFactory; parent::__construct($context, $coreRegistry); } /** * Edit item * * @return \Magento\Framework\Controller\ResultInterface * @SuppressWarnings(PHPMD.NPathComplexity) */ public function execute() { // 1. Get ID and create model $id = $this->getRequest()->getParam('test_id'); $model = $this->_objectManager->create('Aion\Test\Model\Test'); // 2. Initial checking if ($id) { $model->load($id); if (!$model->getId()) { $this->messageManager->addError(__('This item no longer exists.')); /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); return $resultRedirect->setPath('*/*/'); } } // 3. Set entered data if was error when we do save $data = $this->_objectManager->get('Magento\Backend\Model\Session')->getFormData(true); if (!empty($data)) { $model->setData($data); } // 4. Register model to use later in blocks $this->_coreRegistry->register('test_item', $model); /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); // 5. Build edit form $this->initPage($resultPage)->addBreadcrumb( $id ? __('Edit Item') : __('New Item'), $id ? __('Edit Item') : __('New Item') ); $resultPage->getConfig()->getTitle()->prepend(__('Items')); $resultPage->getConfig()->getTitle()->prepend($model->getId() ? $model->getName() : __('New Item')); return $resultPage; } }

As a first step, the edit action(execute() function) makes a query for the test_id parameter. Then it initializes the Aion/Test/Model/Test model class. If the test_id parameter has a value, it attempts to load the model with the id just described. In case of a failure, we get an error message and we are redirected. In case of success, it stores the loaded model in the registry ($this->_coreRegistry->register(’test_item’, $model)). This is called and used by the form container class, mentioned above, from the registry. Finally, it creates the page ($resultPage), and then it sets the title parameter and breadcrumb for the page. We create the layout file belonging to the controller in the test_test_edit.xml file located in the Aion/Test/view/adminhtml/layout/ directory:

<?xml version="1.0"?> <!-- /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="editor"/> <body> <referenceContainer name="content"> <block class="Aion\Test\Block\Adminhtml\Test\Edit" name="test_test_edit"/> </referenceContainer> </body> </page>

Next, we set up the saving process. We create the class in the Save.php file located in the Aion/Test/Controller/Adminhtml/Test/ directory:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml\Test; class Save extends \Aion\Test\Controller\Adminhtml\Test { /** * Save action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); // check if data sent $data = $this->getRequest()->getPostValue(); if ($data) { $id = $this->getRequest()->getParam('test_id'); $model = $this->_objectManager->create('Aion\Test\Model\Test')->load($id); if (!$model->getId() && $id) { $this->messageManager->addError(__('This item no longer exists.')); return $resultRedirect->setPath('*/*/'); } // init model and set data $model->setData($data); // try to save it try { // save the data $model->save(); // display success message $this->messageManager->addSuccess(__('You saved the item.')); // clear previously saved data from session $this->_objectManager->get('Magento\Backend\Model\Session')->setFormData(false); // check if 'Save and Continue' if ($this->getRequest()->getParam('back')) { return $resultRedirect->setPath('*/*/edit', ['test_id' => $model->getId()]); } // go to grid return $resultRedirect->setPath('*/*/'); } catch (\Exception $e) { // display error message $this->messageManager->addError($e->getMessage()); // save data in session $this->_objectManager->get('Magento\Backend\Model\Session')->setFormData($data); // redirect to edit form return $resultRedirect->setPath('*/*/edit', ['test_id' => $this->getRequest()->getParam('test_id')]); } } return $resultRedirect->setPath('*/*/'); } }

In the first step, the controller class receives the data “posted” by the form created earlier ($data = $this->getRequest()->getPostValue();). If it is not an empty array, it initializes the Aion/Test/Model/Test model class, and if the test_id exists, received as a parameter (i.e. we are not saving a new object), then it loads it with the corresponding id. Then it sets the data received in the post and saves the model. When we have finished with this, we can add new objects from the admin grid, created earlier, and then save and edit these as well. One more significant controller needs to be created, which is responsible for deletion. We create the class in the Delete.php file located in the Aion/Test/Controller/Adminhtml/Test/ directory:

<?php /** * Copyright © 2016 AionNext Ltd. All rights reserved. * See COPYING.txt for license details. */ namespace Aion\Test\Controller\Adminhtml\Test; class Delete extends \Aion\Test\Controller\Adminhtml\Test { /** * Delete action * * @return \Magento\Framework\Controller\ResultInterface */ public function execute() { /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); // check if we know what should be deleted $id = $this->getRequest()->getParam('test_id'); if ($id) { try { // init model and delete $model = $this->_objectManager->create('Aion\Test\Model\Test'); $model->load($id); $model->delete(); // display success message $this->messageManager->addSuccess(__('You deleted the item.')); // go to grid return $resultRedirect->setPath('*/*/'); } catch (\Exception $e) { // display error message $this->messageManager->addError($e->getMessage()); // go back to edit form return $resultRedirect->setPath('*/*/edit', ['test_id' => $id]); } } // display error message $this->messageManager->addError(__('We can\'t find the item to delete.')); // go to grid return $resultRedirect->setPath('*/*/'); } }

The delete action (execute() function) makes a query for the test_id parameter first. Then it initializes the Aion/Test/Model/Test model class. If the test_id parameter has a value, it attempts to load the model with the aforementioned id and then it executes deletion. I really hope that I have managed to describe thoroughly how you can create your own module in the Magento 2 system and also how you can set and edit different items or files belonging to it, e.g. database tables, models, collections, blocks, admin grids, layout etc.

You can read Part 1 of this article here: Magento 2 module development – A comprehensive guide – Part 1

See also Part 3 (observers) and Part 4 (Knockout JS).