Magento 2 UI Components are the backbone of the admin panel – every grid, form and mass-action uses them. They are also one of the most complex parts of the platform. I show how the data grid works, how to write a DataProvider, add custom columns, and configure everything through XML without touching the JavaScript layer.
UI Component architecture
A UI Component in the admin consists of three layers:
- Layout XML – declares which component to render on the page
- Component XML – configures columns, filters, mass actions, DataProvider
- DataProvider PHP – provides data to the grid
Layout XML – place the grid on the page
<!-- view/adminhtml/layout/vendor_module_subscription_index.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<uiComponent name="vendor_module_subscription_listing"/>
</referenceContainer>
</body>
</page>
Component XML – grid configuration
<!-- view/adminhtml/ui_component/vendor_module_subscription_listing.xml -->
<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">
vendor_module_subscription_listing.vendor_module_subscription_listing_data_source
</item>
</item>
</argument>
<!-- Data source -->
<dataSource name="vendor_module_subscription_listing_data_source"
component="Magento_Ui/js/grid/provider">
<settings>
<storageConfig>
<param name="indexField" xsi:type="string">subscription_id</param>
</storageConfig>
<updateUrl path="mui/index/render"/>
</settings>
<aclResource>Vendor_Module::subscriptions</aclResource>
<dataProvider class="Vendor\Module\Ui\DataProvider\SubscriptionDataProvider"
name="vendor_module_subscription_listing_data_source">
<settings>
<requestFieldName>id</requestFieldName>
<primaryFieldName>subscription_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
<!-- Toolbar with search and filters -->
<listingToolbar name="listing_top">
<settings>
<sticky>true</sticky>
</settings>
<bookmark name="bookmarks"/>
<columnsControls name="columns_controls"/>
<filterSearch name="fulltext"/>
<filters name="listing_filters">
<filterSelect name="status" provider="${ $.parentName }">
<settings>
<caption translate="true">-- All Statuses --</caption>
<dataScope>status</dataScope>
<label translate="true">Status</label>
<options class="Vendor\Module\Model\Source\SubscriptionStatus"/>
</settings>
</filterSelect>
</filters>
<massaction name="listing_massaction">
<action name="delete">
<settings>
<confirm>
<message translate="true">Delete selected items?</message>
<title translate="true">Delete items</title>
</confirm>
<url path="vendor_module/subscription/massDelete"/>
<type>delete</type>
<label translate="true">Delete</label>
</settings>
</action>
</massaction>
<paging name="listing_paging"/>
</listingToolbar>
<!-- Columns -->
<columns name="vendor_module_subscription_columns">
<selectionsColumn name="ids">
<settings><indexField>subscription_id</indexField></settings>
</selectionsColumn>
<column name="subscription_id">
<settings>
<filter>textRange</filter>
<label translate="true">ID</label>
<sorting>asc</sorting>
</settings>
</column>
<column name="email">
<settings>
<filter>text</filter>
<label translate="true">Email</label>
</settings>
</column>
<column name="status" component="Magento_Ui/js/grid/columns/select">
<settings>
<options class="Vendor\Module\Model\Source\SubscriptionStatus"/>
<filter>select</filter>
<dataType>select</dataType>
<label translate="true">Status</label>
</settings>
</column>
<column name="created_at" class="Magento\Ui\Component\Listing\Columns\Date"
component="Magento_Ui/js/grid/columns/date">
<settings>
<filter>dateRange</filter>
<dataType>date</dataType>
<label translate="true">Created At</label>
</settings>
</column>
<actionsColumn name="actions" class="Vendor\Module\Ui\Component\Listing\Column\SubscriptionActions">
<settings>
<indexField>subscription_id</indexField>
<label translate="true">Actions</label>
</settings>
</actionsColumn>
</columns>
</listing>
DataProvider PHP class
<?php
declare(strict_types=1);
namespace Vendor\Module\Ui\DataProvider;
use Magento\Ui\DataProvider\AbstractDataProvider;
use Vendor\Module\Model\ResourceModel\Subscription\CollectionFactory;
class SubscriptionDataProvider extends AbstractDataProvider
{
public function __construct(
string $name,
string $primaryFieldName,
string $requestFieldName,
CollectionFactory $collectionFactory,
array $meta = [],
array $data = []
) {
$this->collection = $collectionFactory->create();
parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
}
public function getData(): array
{
if (isset($this->loadedData)) {
return $this->loadedData;
}
$items = $this->collection->getItems();
$this->loadedData = [];
foreach ($items as $item) {
$this->loadedData[$item->getId()] = $item->getData();
}
return $this->loadedData;
}
}
Summary
UI Components have a steep XML learning curve but are consistent once you understand the pattern. DataProvider drives the data, the XML component file drives the configuration, and the layout XML places everything on the page. For custom columns – extend AbstractColumn and override prepareDataSource(). For actions column – define the URL and label in your custom column class.
