Add a Form 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 form in Magento 2, using the UI component functionality. This will also follow on from the Step by Step Guide to Adding a Grid.

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. Create an Add.php controller which will forward it the edit action.

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

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

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

class Add extends Entity
{
    /**
     * Forward to edit
     */
    public function execute()
    {
        $resultForward = $this->resultForwardFactory->create();
        return $resultForward->forward('edit');
    }
}

2. Create the Edit.php class.

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

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

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

class Edit extends Entity
{
    /**
     * @return \Magento\Framework\View\Result\Page
     */
    public function execute()
    {
        $entityId = $this->getRequest()->getParam('entity_id');
        $resultPage = $this->resultPageFactory->create();
        $resultPage->setActiveMenu('[Vendor]_[Module]::entity')
            ->addBreadcrumb(__('Entity'), __('Entity'))
            ->addBreadcrumb(__('Manage Entity'), __('Manage Entity'));

        if ($entityId === null) {
            $resultPage->addBreadcrumb(__('New Entity'), __('New Entity'));
            $resultPage->getConfig()->getTitle()->prepend(__('New Entity'));
        } else {
            $resultPage->addBreadcrumb(__('Edit Entity'), __('Edit Entity'));
            $resultPage->getConfig()->getTitle()->prepend(
                $this->entityRepository->getById($entityId)->getName()
            );
        }
        return $resultPage;
    }
}

3. Modify the parent Entity.php controller class and add in the $entityRepository as a dependency.

// 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;
use [Vendor]\[Module]\Api\EntityRepositoryInterface;

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;

    /**
     * Entity Repository Interface
     *
     * @var EntityRepositoryInterface
     */
    protected $entityRepository;

    /**
     * Entity constructor.
     *
     * @param Registry $registry
     * @param PageFactory $resultPageFactory
     * @param ForwardFactory $resultForwardFactory
     * @param EntityRepositoryInterface $entityRepository
     * @param Context $context
     */
    public function __construct(
        Registry $registry,
        PageFactory $resultPageFactory,
        ForwardFactory $resultForwardFactory,
        EntityRepositoryInterface $entityRepository,
        Context $context
    ) {
        $this->coreRegistry         = $registry;
        $this->resultPageFactory    = $resultPageFactory;
        $this->resultForwardFactory = $resultForwardFactory;
        $this->entityRepository     = $entityRepository;
        parent::__construct($context);
    }
}

3. Add the adminhtml layout file for the edit action to include the UI component.

// app/code/[Vendor]/[Module]/view/adminhtml/layout/[module]_entity_edit.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_form" />
        </referenceContainer>
    </body>
</page>

4. Add the form UI component.

// app/code/[Vendor]/[Module]/view/adminhtml/ui_component/[vendor]_entity_form.xml

<?xml version="1.0" encoding="UTF-8"?>
<form 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_form.[vendor]_entity_form_data_source</item>
            <item name="deps" xsi:type="string">[module]_entity_form.[vendor]_entity_form_data_source</item>
        </item>
        <item name="label" xsi:type="string" translate="true">Entity Information</item>
        <item name="config" xsi:type="array">
            <item name="dataScope" xsi:type="string">data</item>
            <item name="namespace" xsi:type="string">[module]_form</item>
        </item>
        <item name="template" xsi:type="string">templates/form/collapsible</item>
        <item name="buttons" xsi:type="array">
            <item name="back" xsi:type="string">[Vendor]\[Module]\Block\Adminhtml\Entity\Edit\BackButton</item>
            <item name="delete" xsi:type="string">[Vendor]\[Module]\Block\Adminhtml\Entity\Edit\DeleteButton</item>
            <item name="reset" xsi:type="string">[Vendor]\[Module]\Block\Adminhtml\Entity\Edit\ResetButton</item>
            <item name="save" xsi:type="string">[Vendor]\[Module]\Block\Adminhtml\Entity\Edit\SaveButton</item>
            <item name="save_and_continue" xsi:type="string">[Vendor]\[Module]\Block\Adminhtml\Entity\Edit\SaveAndContinueButton</item>
        </item>
    </argument>
    <dataSource name="[module]_entity_form_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <argument name="class" xsi:type="string">[Vendor]\[Module]\Model\DataProvider</argument>
            <argument name="name" xsi:type="string">[module]_entity_form_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="submit_url" xsi:type="url" path="[module]/entity/save"/>
                </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/form/provider</item>
            </item>
        </argument>
    </dataSource>
    <fieldset name="entity_details">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="label" xsi:type="string" translate="true">Entity Details</item>
            </item>
        </argument>
        <field name="entity_id">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="visible" xsi:type="boolean">false</item>
                    <item name="dataType" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Entity ID</item>
                    <item name="formElement" xsi:type="string">input</item>
                    <item name="source" xsi:type="string">data</item>
                    <item name="dataScope" xsi:type="string">entity_id</item>
                </item>
            </argument>
        </field>
        <field name="name">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="dataType" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Name</item>
                    <item name="formElement" xsi:type="string">input</item>
                    <item name="source" xsi:type="string">data</item>
                    <item name="dataScope" xsi:type="string">name</item>
                </item>
            </argument>
        </field>
        <field name="description">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="formElement" xsi:type="string">wysiwyg</item>
                    <item name="source" xsi:type="string">data</item>
                    <item name="label" xsi:type="string" translate="true">Description</item>
                    <item name="dataScope" xsi:type="string">description</item>
                    <item name="validation" xsi:type="array">
                        <item name="required-entry" xsi:type="boolean">true</item>
                    </item>
                </item>
            </argument>
        </field>
        <field name="is_active">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="dataType" xsi:type="string">boolean</item>
                    <item name="label" xsi:type="string" translate="true">Enabled</item>
                    <item name="formElement" xsi:type="string">checkbox</item>
                    <item name="source" xsi:type="string">block</item>
                    <item name="dataScope" xsi:type="string">is_active</item>
                    <item name="prefer" xsi:type="string">toggle</item>
                    <item name="valueMap" xsi:type="array">
                        <item name="true" xsi:type="number">1</item>
                        <item name="false" xsi:type="number">0</item>
                    </item>
                    <item name="default" xsi:type="number">1</item>
                </item>
            </argument>
        </field>
    </fieldset>
<form>

5. Create the button block classes.

// app/code/[Vendor]/[Module]/Block/Adminhtml/Entity/Edit/Generic.php

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

use Magento\Backend\Block\Widget\Context;
use Magento\Framework\Exception\NoSuchEntityException;
use [Vendor]\[Module]\Api\EntityRepositoryInterface;

class Generic
{
    /**
     * @var Context
     */
    protected $context;

    /**
     * @var EntityRepositoryInterface
     */
    protected $entityRepository;

    /**
     * Generic constructor.
     *
     * @param Context $context
     * @param EntityRepositoryInterface $entityRepository
     */
    public function __construct(
        Context $context,
        EntityRepositoryInterface $entityRepository
    ) {
        $this->context = $context;
        $this->entityRepository = $entityRepository;
    }

    /**
     * Return Entity ID
     *
     * @return int|null
     */
    public function getEntityId()
    {
        try {
            return $this->entityRepository->getById(
                $this->context->getRequest()->getParam('entity_id')
            )->getId();
        } catch (NoSuchEntityException $e) {
            return null;
        }
    }

    /**
     * Generate url by route and parameters
     *
     * @param   string $route
     * @param   array $params
     * @return  string
     */
    public function getUrl($route = '', $params = [])
    {
        return $this->context->getUrlBuilder()->getUrl($route, $params);
    }
}
// app/code/[Vendor]/[Module]/Block/Adminhtml/Entity/Edit/BackButton.php

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

use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class BackButton extends Generic implements ButtonProviderInterface
{
    /**
     * Get button data
     *
     * @return array
     */
    public function getButtonData()
    {
        return [
            'label' => __('Back'),
            'on_click' => sprintf("location.href = '%s';", $this->getBackUrl()),
            'class' => 'back',
            'sort_order' => 10
        ];
    }

    /**
     * Get URL for back (reset) button
     *
     * @return string
     */
    public function getBackUrl()
    {
        return $this->getUrl('*/*/');
    }
}
// app/code/[Vendor]/[Module]/Block/Adminhtml/Entity/Edit/DeleteButton.php

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

use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class DeleteButton extends Generic implements ButtonProviderInterface
{
    /**
     * Get button data
     *
     * @return array
     */
    public function getButtonData()
    {
        $data = [];
        if ($this->getEntityId()) {
            $data = [
                'label' => __('Delete Entity'),
                'class' => 'delete',
                'on_click' => 'deleteConfirm(\'' . __(
                    'Are you sure you want to do this?'
                ) . '\', \'' . $this->getDeleteUrl() . '\')',
                'sort_order' => 20,
            ];
        }
        return $data;
    }

    /**
     * @return string
     */
    public function getDeleteUrl()
    {
        return $this->getUrl('*/*/delete', ['entity_id' => $this->getEntityId()]);
    }
}
// app/code/[Vendor]/[Module]/Block/Adminhtml/Entity/Edit/ResetButton.php

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

use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class ResetButton implements ButtonProviderInterface
{
    /**
     * Get button data
     *
     * @return array
     */
    public function getButtonData()
    {
        return [
            'label' => __('Reset'),
            'class' => 'reset',
            'on_click' => 'location.reload();',
            'sort_order' => 30
        ];
    }
}
// app/code/[Vendor]/[Module]/Block/Adminhtml/Entity/Edit/SaveButton.php

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

use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class SaveButton implements ButtonProviderInterface
{
    /**
     * Get button data
     *
     * @return array
     */
    public function getButtonData()
    {
        return [
            'label' => __('Save Entity'),
            'class' => 'save primary',
            'data_attribute' => [
                'mage-init' => ['button' => ['event' => 'save']],
                'form-role' => 'save',
            ],
            'sort_order' => 90,
        ];
    }
}
// app/code/[Vendor]/[Module]/Block/Adminhtml/Entity/Edit/SaveAndContinueButton.php

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

use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class SaveAndContinueButton implements ButtonProviderInterface
{

    /**
     * Get button data
     *
     * @return array
     */
    public function getButtonData()
    {
        return [
            'label' => __('Save and Continue Edit'),
            'class' => 'save',
            'data_attribute' => [
                'mage-init' => [
                    'button' => ['event' => 'saveAndContinueEdit'],
                ],
            ],
            'sort_order' => 80,
        ];
    }
}

6. Add the DataProvider class.

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

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

use Magento\Ui\DataProvider\AbstractDataProvider;
use Magento\Ui\DataProvider\Modifier\ModifierInterface;
use Magento\Ui\DataProvider\Modifier\PoolInterface;
use [Vendor]\[Module]\Model\ResourceModel\Entity\CollectionFactory;

class DataProvider extends AbstractDataProvider
{
    /**
     * @var PoolInterface
     */
    protected $pool;

    /**
     * @param string $name
     * @param string $primaryFieldName
     * @param string $requestFieldName
     * @param CollectionFactory $entityCollectionFactory
     * @param PoolInterface $pool
     * @param array $meta
     * @param array $data
     */
    public function __construct(
        $name,
        $primaryFieldName,
        $requestFieldName,
        CollectionFactory $entityCollectionFactory,
        PoolInterface $pool,
        array $meta = [],
        array $data = []
    ) {
        $this->collection   = $entityCollectionFactory->create();
        $this->pool         = $pool;
        parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
        $this->meta = $this->prepareMeta($this->meta);
    }

    /**
     * Prepares Meta
     *
     * @param array $meta
     * @return array
     */
    public function prepareMeta(array $meta)
    {
        return $meta;
    }

    /**
     * Get data
     *
     * @return array
     */
       /**
     * Get data
     *
     * @return array
     */
    public function getData()
    {
        if (isset($this->loadedData)) {
            return $this->loadedData;
        }
        $items = $this->collection->getItems();
        foreach ($items as $page) {
            $this->loadedData[$page->getId()] = $page->getData();
        }
        $data = $this->dataPersistor->get('module_messages');
        if (!empty($data)) {
            $page = $this->collection->getNewEmptyItem();
            $page->setData($data);
            $this->loadedData[$page->getId()] = $page->getData();
            $this->dataPersistor->clear('module_messages');
        }
        return $this->loadedData;
    }
}

7. Within the di.xml (note you can create another one in the adminhtml directory), add the $modifiers arguments.

// app/code/[Vendor]/[Module]/etc/adminhtml/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">
    <virtualType name="[Vendor][Module]UiDataProviderEntityFormModifierPool" type="Magento\Ui\DataProvider\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="entity_data" xsi:type="array">
                    <item name="class" xsi:type="string">[Vendor]\[Module]\Ui\DataProvider\Entity\Form\Modifier\EntityData</item>
                    <item name="sortOrder" xsi:type="number">10</item>
                </item>
            </argument>
        </arguments>
    </virtualType>
    <type name="[Vendor]\[Module]\Model\DataProvider">
        <arguments>
            <argument name="pool" xsi:type="object">[Vendor][Module]UiDataProviderEntityFormModifierPool</argument>
        </arguments>
    </type>
</config>

8. Add the modifier class.

// app/code/[Vendor]/[Module]/Ui/DataProvider/Entity/Form/Modifier/EntityData.php

<?php
namespace [Vendor]\[Module]\Ui\DataProvider\Entity\Form\Modifier;

use Magento\Ui\DataProvider\Modifier\ModifierInterface;
use [Vendor]\[Module]\Model\ResourceModel\Entity\CollectionFactory;

class EntityData implements ModifierInterface
{
    /**
     * @var \[Vendor]\[Module]\Model\ResourceModel\Entity\Collection
     */
    protected $collection;

    /**
     * @param CollectionFactory $entityCollectionFactory
     */
    public function __construct(
        CollectionFactory $entityCollectionFactory
    ) {
        $this->collection = $entityCollectionFactory->create();
    }

    /**
     * @param array $meta
     * @return array
     */
    public function modifyMeta(array $meta)
    {
        return $meta;
    }

    /**
     * @param array $data
     * @return array
     */
    public function modifyData(array $data)
    {
        $items = $this->collection->getItems();
        /** @var $entity \[Vendor]\[Module]\Model\Entity */
        foreach ($items as $entity) {
            $_data = $entity->getData();
            $entity->setData($_data);
            $data[$entity->getId()] = $_data;
        }
        return $data;
    }
}

If you’ve made it this far, your form will successfully load!

Add a Form in Magento 2

9. Add a Save.php controller class so that the form data can be saved.

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

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

use Magento\Framework\Registry;
use Magento\Framework\View\Result\PageFactory;
use Magento\Backend\Model\View\Result\ForwardFactory;
use Magento\Backend\App\Action\Context;
use Magento\Framework\Message\Manager;
use Magento\Framework\Api\DataObjectHelper;
use [Vendor]\[Module]\Api\EntityRepositoryInterface;
use [Vendor]\[Module]\Api\Data\EntityInterface;
use [Vendor]\[Module]\Api\Data\EntityInterfaceFactory;
use [Vendor]\[Module]\Controller\Adminhtml\Entity;

class Save extends Entity
{
    /**
     * @var Manager
     */
    protected $messageManager;

    /**
     * @var EntityRepositoryInterface
     */
    protected $entityRepository;

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

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

    /**
     * Save constructor.
     *
     * @param Registry $registry
     * @param EntityRepositoryInterface $entityRepository
     * @param PageFactory $resultPageFactory
     * @param ForwardFactory $resultForwardFactory
     * @param Manager $messageManager
     * @param EntityInterfaceFactory $entityFactory
     * @param DataObjectHelper $dataObjectHelper
     * @param Context $context
     */
    public function __construct(
        Registry $registry,
        EntityRepositoryInterface $entityRepository,
        PageFactory $resultPageFactory,
        ForwardFactory $resultForwardFactory,
        Manager $messageManager,
        EntityInterfaceFactory $entityFactory,
        DataObjectHelper $dataObjectHelper,
        Context $context
    ) {
        parent::__construct($registry, $resultPageFactory, $resultForwardFactory, $entityRepository, $context);
        $this->messageManager    = $messageManager;
        $this->entityFactory     = $entityFactory;
        $this->entityRepository  = $entityRepository;
        $this->dataObjectHelper  = $dataObjectHelper;
    }

    /**
     * Save action
     *
     * @return \Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        $data = $this->getRequest()->getPostValue();

        $resultRedirect = $this->resultRedirectFactory->create();
        if ($data) {
            $id = $this->getRequest()->getParam('entity_id');
            if ($id) {
                $model = $this->entityRepository->getById($id);
            } else {
                unset($data['entity_id']);
                $model = $this->entityFactory->create();
            }

            try {
                $this->dataObjectHelper->populateWithArray($model, $data, EntityInterface::class);
                $this->entityRepository->save($model);
                $this->messageManager->addSuccessMessage(__('You saved this data.'));
                $this->_getSession()->setFormData(false);
                if ($this->getRequest()->getParam('back')) {
                    return $resultRedirect->setPath('*/*/edit', ['entity_id' => $model->getId(), '_current' => true]);
                }
                return $resultRedirect->setPath('*/*/');
            } catch (\Magento\Framework\Exception\LocalizedException $e) {
                $this->messageManager->addErrorMessage($e->getMessage());
            } catch (\RuntimeException $e) {
                $this->messageManager->addErrorMessage($e->getMessage());
            } catch (\Exception $e) {
                $this->messageManager->addException($e, __('Something went wrong while saving the entity.'));
            }

            $this->_getSession()->setFormData($data);
            return $resultRedirect->setPath('*/*/edit', [
                'entity_id' => $this->getRequest()->getParam('entity_id')
            ]);
        }
        return $resultRedirect->setPath('*/*/');
    }
}

10. Modify the [module]_entity_grid.xml file and add in <actionsColumn> configuration.

// app/code/[Vendor]/[Module]/view/adminhtml/layout/[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">

    ....

    <columns>

        ....

        <actionsColumn name="actions" class="[Vendor]\[Module]\Ui\Component\Listing\Column\EntityActions">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="indexField" xsi:type="string">entity_id</item>
                    <item name="urlEntityParamName" xsi:type="string">entity_id</item>
                </item>
            </argument>
        </actionsColumn>
    </columns>
</listing>

11. Add the EntityActions.php class.

 // app/code/[Vendor]/[Module]/Ui/Component/Listing/Column/EntityActions.php

<?php
namespace [Vendor]\[Module]\Ui\Component\Listing\Column;

use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Ui\Component\Listing\Columns\Column;
use Magento\Framework\UrlInterface;

class EntityActions extends Column
{
    const URL_PATH_EDIT = '[module]/entity/edit';
    const URL_PATH_DELETE = '[module]/entity/delete';

    /**
     * URL builder
     *
     * @var \Magento\Framework\UrlInterface
     */
    protected $urlBuilder;

    /**
     * @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 = []
    ) {
        parent::__construct($context, $uiComponentFactory, $components, $data);
        $this->urlBuilder = $urlBuilder;
    }

    /**
     * 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['entity_id'])) {
                    $item[$this->getData('name')] = [
                        'edit' => [
                            'href' => $this->urlBuilder->getUrl(
                                static::URL_PATH_EDIT,
                                [
                                    'entity_id' => $item['entity_id']
                                ]
                            ),
                            'label' => __('Edit')
                        ],
                        'delete' => [
                            'href' => $this->urlBuilder->getUrl(
                                static::URL_PATH_DELETE,
                                [
                                    'entity_id' => $item['entity_id']
                                ]
                            ),
                            'label' => __('Delete'),
                            'confirm' => [
                                'title' => __('Delete "${ $.$data.name }"'),
                                'message' => __('Are you sure you want to delete the Entity: "${ $.$data.name }"?')
                            ]
                        ]
                    ];
                }
            }
        }
        return $dataSource;
    }
}

12. Add the Delete.php controller.

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

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

use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use [Vendor]\[Module]\Controller\Adminhtml\Entity;

class Delete extends Entity
{
    /**
     * @return \Magento\Framework\Controller\Result\Redirect
     */
    public function execute()
    {
        $resultRedirect = $this->resultRedirectFactory->create();
        $entityId = $this->getRequest()->getParam('entity_id');
        if ($entityId) {
            try {
                $this->entityRepository->deleteById($entityId);
                $this->messageManager->addSuccessMessage(__('The entity has been deleted.'));
                $resultRedirect->setPath('[module]/entity/index');
                return $resultRedirect;
            } catch (NoSuchEntityException $e) {
                $this->messageManager->addErrorMessage(__('The entity no longer exists.'));
                return $resultRedirect->setPath('[module]/entity/index');
            } catch (LocalizedException $e) {
                $this->messageManager->addErrorMessage($e->getMessage());
                return $resultRedirect->setPath('[module]/entity/index', ['entity_id' => $entityId]);
            } catch (\Exception $e) {
                $this->messageManager->addErrorMessage(__('There was a problem deleting the data'));
                return $resultRedirect->setPath('[module]/entity/edit', ['entity_id' => $entityId]);
            }
        }
        $this->messageManager->addErrorMessage(__('We can\'t find the entity to delete.'));
        $resultRedirect->setPath('[module]/entity/index');
        return $resultRedirect;
    }
}

13. Modify the [module]_entity_grid.xml file to include a mass actions dropdown.

 // 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">

    ....

    <container name="listing_top">

        ....

        <massaction name="listing_massaction">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="selectProvider" xsi:type="string">[module]_entity_grid.[module]_entity_grid.[module]_entity_grid_columns.ids</item>
                    <item name="indexField" xsi:type="string">entity_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="[module]/entity/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 want 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="[module]/entity/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="[module]/entity/massEnable"/>
                    </item>
                </argument>
            </action>
        </massaction>
    </container>

    ....

</listing>

14. Add the mass action related classes.

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

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

use Magento\Backend\App\Action\Context;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Registry;
use Magento\Framework\View\Result\PageFactory;
use Magento\Backend\Model\View\Result\ForwardFactory;
use Magento\Ui\Component\MassAction\Filter;
use [Vendor]\[Module]\Api\EntityRepositoryInterface;
use [Vendor]\[Module]\Controller\Adminhtml\Entity;
use [Vendor]\[Module]\Model\Entity as EntityModel;
use [Vendor]\[Module]\Model\ResourceModel\Entity\CollectionFactory;

abstract class MassAction extends Entity
{
    /**
     * @var Filter
     */
    protected $filter;

    /**
     * @var CollectionFactory
     */
    protected $collectionFactory;

    /**
     * @var EntityRepositoryInterface
     */
    protected $entityRepository;

    /**
     * @var ForwardFactory
     */
    protected $resultForwardFactory;

    /**
     * @var string
     */
    protected $successMessage;

    /**
     * @var string
     */
    protected $errorMessage;

    /**
     * MassAction constructor.
     *
     * @param Filter $filter
     * @param Registry $registry
     * @param EntityRepositoryInterface $entityRepository
     * @param PageFactory $resultPageFactory
     * @param Context $context
     * @param CollectionFactory $collectionFactory
     * @param ForwardFactory $resultForwardFactory
     * @param $successMessage
     * @param $errorMessage
     */
    public function __construct(
        Filter $filter,
        Registry $registry,
        EntityRepositoryInterface $entityRepository,
        PageFactory $resultPageFactory,
        Context $context,
        CollectionFactory $collectionFactory,
        ForwardFactory $resultForwardFactory,
        $successMessage,
        $errorMessage
    ) {
        parent::__construct($registry, $resultPageFactory, $resultForwardFactory, $entityRepository, $context);
        $this->filter               = $filter;
        $this->entityRepository     = $entityRepository;
        $this->collectionFactory    = $collectionFactory;
        $this->resultForwardFactory = $resultForwardFactory;
        $this->successMessage       = $successMessage;
        $this->errorMessage         = $errorMessage;
    }

    /**
     * @param EntityModel $entity
     * @return mixed
     */
    abstract protected function massAction(EntityModel $entity);

    /**
     * @return \Magento\Framework\Controller\Result\Redirect
     */
    public function execute()
    {
        try {
            $collection = $this->filter->getCollection($this->collectionFactory->create());
            $collectionSize = $collection->getSize();
            foreach ($collection as $entity) {
                $this->massAction($entity);
            }
            $this->messageManager->addSuccessMessage(__($this->successMessage, $collectionSize));
        } catch (LocalizedException $e) {
            $this->messageManager->addErrorMessage($e->getMessage());
        } catch (\Exception $e) {
            $this->messageManager->addExceptionMessage($e, __($this->errorMessage));
        }
        $redirectResult = $this->resultRedirectFactory->create();
        $redirectResult->setPath('[module]/entity/index');
        return $redirectResult;
    }
}
// app/code/[Vendor]/[Module]/Controller/Adminhtml/Entity/MassEnable.php

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

use [Vendor]\[Module]\Model\Entity;

class MassEnable extends MassAction
{
    /**
     * @param Entity $entity
     * @return $this
     */
    protected function massAction(Entity $entity)
    {
        $entity->setIsActive(true);
        $this->entityRepository->save($entity);
        return $this;
    }
}
// app/code/[Vendor]/[Module]/Controller/Adminhtml/Entity/MassEnable.php

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

use [Vendor]\[Module]\Model\Entity;

class MassDisable extends MassAction
{
    /**
     * @param Entity $entity
     * @return $this
     */
    protected function massAction(Entity $entity)
    {
        $entity->setIsActive(false);
        $this->entityRepository->save($entity);
        return $this;
    }
}
 // app/code/[Vendor]/[Module]/Controller/Adminhtml/Entity/MassDelete.php

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

use [Vendor]\[Module]\Model\Entity;

class MassDelete extends MassAction
{
    /**
     * @param Entity $entity
     * @return $this
     */
    protected function massAction(Entity $entity)
    {
        $this->entityRepository->delete($entity);
        return $this;
    }
}

15. Add the success and error messages for the mass actions within di.xml.

 // app/code/[Vendor]/[Module]/etc/adminhtml/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">
    
    ....

    <type name="[Vendor]\[Module]\Controller\Adminhtml\Entity\MassDelete">
        <arguments>
            <argument name="successMessage" xsi:type="string" translate="true">A total of %1 record(s) have been deleted.</argument>
            <argument name="errorMessage" xsi:type="string" translate="true">An error occurred while deleting record(s).</argument>
        </arguments>
    </type>
    <type name="[Vendor]\[Module]\Controller\Adminhtml\Entity\MassDisable">
        <arguments>
            <argument name="successMessage" xsi:type="string" translate="true">A total of %1 record(s) have been disabled.</argument>
            <argument name="errorMessage" xsi:type="string" translate="true">An error occurred while disabling selected record(s).</argument>
        </arguments>
    </type>
    <type name="[Vendor]\[Module]\Controller\Adminhtml\Entity\MassEnable">
        <arguments>
            <argument name="successMessage" xsi:type="string" translate="true">A total of %1 record(s) have been enabled.</argument>
            <argument name="errorMessage" xsi:type="string" translate="true">An error occurred while enabling record(s).</argument>
        </arguments>
    </type>
</config>

Enjoy your new Grid and Form UI Components!

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