UI Components to jeden z bardziej zagadkowych elementów Magento 2. Siatki danych w panelu admina, formularze edycji, filtry – wszystko to jest zbudowane na UI Components. Dokumentacja Magento jest tu wyjątkowo skąpa, a debugowanie bywa frustrujące. Pokazuję jak to działa od środka i jak napisać własną siatkę danych z filtrowaniem.
Czym są UI Components?
UI Components to system renderowania interfejsu oparty na knockout.js po stronie frontendu i konfiguracji XML po stronie PHP. Każdy komponent ma trzy warstwy:
- XML – deklaracja struktury i konfiguracji komponentu
- PHP DataProvider – dostarcza dane do komponentu
- JavaScript ViewModel – logika po stronie klienta (knockout.js)
Magento ładuje konfigurację XML, buduje na jej podstawie JSON z konfiguracją komponentu i przekazuje do knockout.js który renderuje widok. Stąd charakterystyczny fragment w źródle strony:
// To co widzisz w źródle admina - konfiguracja UI Components jako JSON
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"vendor_module_listing": {
"children": { ... }
}
}
}
}
}
</script>
Własna siatka danych – struktura plików
Vendor/Module/
Ui/
Component/
Listing/
Column/
Status.php <- własna kolumna z renderowaniem
DataProvider/
SubscriptionDataProvider.php
view/
adminhtml/
layout/
vendor_module_index.xml <- layout strony
ui_component/
vendor_module_listing.xml <- konfiguracja siatki
Layout strony admina
<!-- view/adminhtml/layout/vendor_module_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_listing"/>
</referenceContainer>
</body>
</page>
Konfiguracja siatki – ui_component XML
<!-- view/adminhtml/ui_component/vendor_module_listing.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<argument name="data" xsi:type="array">
<item name="js_config" xsi:type="array">
<item name="provider" xsi:type="string">vendor_module_listing.vendor_module_listing_data_source</item>
</item>
</argument>
<settings>
<buttons>
<button name="add">
<url path="*/*/new"/>
<class>primary</class>
<label translate="true">Dodaj nowy</label>
</button>
</buttons>
<spinner>vendor_module_listing_columns</spinner>
<deps>
<dep>vendor_module_listing.vendor_module_listing_data_source</dep>
</deps>
</settings>
<!-- DataSource - połączenie z DataProviderem PHP -->
<dataSource name="vendor_module_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_listing_data_source">
<settings>
<requestFieldName>id</requestFieldName>
<primaryFieldName>subscription_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
<!-- Pasek narzędzi z filtrowaniem i paginacją -->
<listingToolbar name="listing_top">
<settings>
<sticky>true</sticky>
</settings>
<bookmark name="bookmarks"/>
<columnsControls name="columns_controls"/>
<filterSearch name="fulltext"/>
<filters name="listing_filters"/>
<paging name="listing_paging"/>
</listingToolbar>
<!-- Definicja kolumn -->
<columns name="vendor_module_listing_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>
<!-- Własna kolumna z customowym renderowaniem -->
<column name="status" class="Vendor\Module\Ui\Component\Listing\Column\Status">
<settings>
<filter>select</filter>
<label translate="true">Status</label>
</settings>
</column>
<column name="created_at" class="Magento\Ui\Component\Listing\Columns\Date">
<settings>
<filter>dateRange</filter>
<label translate="true">Data utworzenia</label>
</settings>
</column>
<actionsColumn name="actions" class="Vendor\Module\Ui\Component\Listing\Column\Actions">
<settings>
<indexField>subscription_id</indexField>
<label translate="true">Akcje</label>
</settings>
</actionsColumn>
</columns>
</listing>
DataProvider – PHP
<?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 = []
) {
parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
$this->collection = $collectionFactory->create();
}
public function getData(): array
{
if (!$this->getCollection()->isLoaded()) {
$this->getCollection()->load();
}
$items = [];
foreach ($this->getCollection() as $item) {
$items[] = $item->getData();
}
return [
'totalRecords' => $this->getCollection()->getSize(),
'items' => $items,
];
}
}
Własna kolumna z renderowaniem
<?php
declare(strict_types=1);
namespace Vendor\Module\Ui\Component\Listing\Column;
use Magento\Ui\Component\Listing\Columns\Column;
class Status extends Column
{
private array $statusLabels = [
1 => '<span class="grid-severity-notice">Aktywna</span>',
0 => '<span class="grid-severity-critical">Nieaktywna</span>',
];
public function prepareDataSource(array $dataSource): array
{
if (!isset($dataSource['data']['items'])) {
return $dataSource;
}
foreach ($dataSource['data']['items'] as &$item) {
$status = (int) ($item[$this->getData('name')] ?? 0);
$item[$this->getData('name')] = $this->statusLabels[$status]
?? '<span class="grid-severity-minor">Nieznany</span>';
}
return $dataSource;
}
}
Podsumowanie
UI Components mają stromą krzywą uczenia i kiepską dokumentację, ale gdy już rozumiesz strukturę XML - DataProvider - knockout.js, pisanie kolejnych siatek i formularzy idzie sprawnie przez kopiowanie i modyfikowanie istniejących konfiguracji. Warto przejrzeć kod natywnych modułów Magento jak Magento_Customer czy Magento_Sales - to najlepszy wzorzec do naśladowania.
