Привет! Меня зовут Павел и я Magento 2 бэкенд-разработчик. В прошлой части саги о Magento 2 UI Components мы получили общие сведения о UI-компонентах, их разнообразии, строении и технологиях, лежащих в основе. Сегодня подробно коснемся их конфигурации: значения по умолчанию, XML-конфигурации, выражения в значениях конфигурации, замена шаблонов и JS-компонентов и пр. Погнали!

Прежде всего

Поскольку сегодня мы будем много говорить об XML конфигурациях компонентов, нужно понять одну очень важную вещь. XML файл при парсинге превращается в многомерный массив, который затем используется для конфигурации компонента (в том числе при JS инициализации, см. предыдущую статью) верхнего уровня и всех дочерних компонентов. Когда мы будем обсуждать элементы конфигурации, этот момент стоит держать в голове, - так многие вещи станут понятнее. 

Также следует знать, что М2 при парсинге всех XML конфигураций выполняет merge всех получившихся массивов для одноименных файлов конфигураций. Это значит, что мы можем переписать конфигурацию какого либо компонента в своем модуле без необходимости переписывать всю исходную конфигурацию (по тому же принципу, как это работает, например, для лейаутов). Порядок, в котором будут парситься файлы (более поздние будут затирать значения более ранних) можно определить путем указания sequence в файле module.xml.

Самая базовая конфигурация компонентов обозначена в файле <magento root>/vendor/magento/module-ui/view/base/ui_component/etc/definition.xml. Все отдельные файлы конфигураций перетирают значения, заданные в этом файле. Однако также стоит помнить, что значения по умолчанию для конфигурации содержатся также в JS классе компонента. 

Значения по умолчанию можно не указывать в XML конфигурации, и их объявление требуется только в случае, если необходимо задать значения, отличных от значений по умолчанию.

Как мы уже упоминали ранее, результатом обработки XML конфигураций будет конструкция вида

<script type="text/x-magento-init">{"*": {"Magento_Ui/js/core/app":{<JSON_configuration>}}}</script>

на целевой странице, которая будет включать конфигурацию для всех UI компонентов.

Конфигурация верхнего уровня

Для примера возьмем любой грид (компонент верхнего уровня listing) и последовательно рассмотрим его конфигурацию, постепенно спускаясь от верхнего уровня к нижнему.

Дата-провайдер

Первое, о чем хотелось бы поговорить - дата-провайдер компонента listing. Объявляется он примерно так:

<argument name="data" xsi:type="array">
    <item name="js_config" xsi:type="array">
        <item name="provider" xsi:type="string">white_rabbit_grid_listing.white_rabbit_grid_listing_data_source</item>
    </item>
</argument>

Результатом парсинга этого элемента будет такой элемент конфигурации:

[‘data’][‘js_config’][‘provider’] = white_rabbit_grid_listing.white_rabbit_grid_listing_data_source

Первая часть этого выражения - сам компонент верхнего уровня, в нашем случае - listing (она соответствует названию файла, white_rabbit_grid_listing.xml). Вторая часть - после точки - собственно, дата-провайдер.

Дата-провайдерами управляет класс-фабрика Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory, содержащий в переменной $collections массив, где ключами являются имена дата-провайдеров, а значениями - референсы их классов . Дата-провайдер добавляется через механизм DI в файле di.xml, например:

<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
    <arguments>
        <argument name="collections" xsi:type="array">
            <item name="white_rabbit_listing_data_source"
                  xsi:type="string">RSHB\WhiteRabbit\Model\ResourceModel\Rabbit\Grid\Collection</item>
        </argument>
    </arguments>
</type>

<virtualType name="RSHB\WhiteRabbit\Model\ResourceModel\Rabbit\Grid\Collection"
             type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
    <arguments>
        <argument name="mainTable" xsi:type="string">rshb_white_rabbit</argument>
        <argument name="resourceModel" xsi:type="string">RSHB\WhiteRabbit\Model\ResourceModel\Rabbit</argument>
    </arguments>
</virtualType>

Таким образом дата-провайдер узнает, откуда брать коллекцию элементов для нашего грида.

Далее привязываем дата-провайдер в качестве источника данных для нашего грида:

<dataSource name="white_rabbit_listing_data_source" component="Magento_Ui/js/grid/provider">
    <settings>
        <storageConfig>
            <param name="indexField" xsi:type="string">id</param>
        </storageConfig>
        <updateUrl path="mui/index/render"/>
    </settings>
    <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="white_rabbit_grid_listing_data_source">
        <settings>
            <requestFieldName>id</requestFieldName>
            <primaryFieldName>id</primaryFieldName>
        </settings>
    </dataProvider>
</dataSource>

Как мы видим, за источник данных отвечает компонент DataSource

Здесь стоит обратить внимание на то, как задаются primary поля для сущности, а также поля, которые будут индексными для грида (в некоторых случаях бывает полезно использовать разные поля для этих нужд).

Настройки (settings)

Общий для всех компонентов раздел, который позволяет задать множество параметров компонента. Рассмотрим на примере родительского компонента listing:

<settings>
    <spinner>entity_grid_columns</spinner>
    <deps>
        <dep>entity_grid_listing.entity_grid_listing_data_source</dep>
    </deps>
    <buttons>
        <button name="add">
            <url path="*/*/new"/>
            <class>primary</class>
            <label translate="true">Add New Referral</label>
        </button>
    </buttons>
</settings>

Обратим внимание на несколько элементов. Первое: указана зависимость (<dep>) от дата-провайдера. Второе: компонент включает настройки для кнопок <buttons> и спиннера, который выводится при загрузке (указана группа компонентов, для которых этот спиннер будет появляться). 

Возникает вопрос: как узнать, что вообще мы можем писать в settings? Идем в определения компонентов, которые находятся по адресу <magento root>/vendor/magento/module-ui/view/base/ui_component/etc/definition, и смотрим в файл listing.xsd, среди прочего видим группу componentListingSettings, где обозначены описанные выше элементы. А элемент <dep> наследуется от определения более высокого уровня, из общего для всех настроек определения ui_settings.xsd. Однако, нет смысла каждый раз копаться в ядре, для абсолютного большинства компонентов доступные элементы раздела settings указаны на страницах самих компонентов (см. Предыдущую часть).

Прочие элементы базового компонента listing

Кратко рассмотрим прочие элементы базового компонента listing:

ListingToolbar является оберткой для набора компонентов управления гридом, например:

<listingToolbar name="listing_top">
    <settings>
        <sticky>true</sticky>
    </settings>
    <bookmark name="bookmarks"/>
    <columnsControls name="columns_controls"/>
    <filters name="listing_filters"/>
    <paging name="listing_paging"/>
    <exportButton name="export_button"/>
</listingToolbar>

Как мы видим, здесь задаются компоненты закладок, фильтров, пагинации п пр. Каждый из этих компонентов может быть сконфигурирован по тому же принципу, что и базовый компонент -  при помощи раздела settings. В примере выше для компонентов bookmark, filters, paging оставлены настройки по умолчанию, поэтому раздел settings просто не объявляется.

Наконец, одним из важнейших компонентов грида является компонент-обертка Columns, который включает в себя дочерние компоненты типа Column.

Конфигурация дочернего компонента

В целом, конфигурация дочернего компонента ничем не отличается от конфигурации базовых компонентов, мы также можем задавать конфигурацию в разделе <settings>:

<column name="order_id" class="Magento\Ui\Component\Listing\Columns\Column" sortOrder="20">
    <settings>
        <filter>text</filter>
        <sortable>true</sortable>
        <label translate="true">Order ID</label>
    </settings>
</column>

Поэтому коснемся аспектов, которые мы пропустили при рассмотрении базового компонента.

Класс

По своей сути это серверная часть компонента, которая занимается подготовкой данных перед передачей их js-виджету. По умолчанию у каждого компонента есть свой класс, однако мы можем подменить этот класс для получения поведения, которое нам необходимо. Рассмотрим пример.

Предположим, есть коллекция объектов, каждый из которых выглядит так:

{
  “fist_name”: “Rabbit”,
  “middle_name”: “Whitest”,
  “last_name”: “White”,
  …
}

И мы хотим вывести коллекцию в грид, но при этом есть задача вместо трех колонок first_name, middle_name и last_name выводить одну колонку name, где данные из трех колонок будут объединены в одну строку. Как мы можем этого добиться? 

Создаем класс для нашего компонента column (обратите внимание, что наш класс наследует базовый Magento\Ui\Component\Listing\Columns\Column):

<?php

namespace RSHB\WhiteRabbit\Ui\Component\Listing\Column\Rabbit;

use Magento\Ui\Component\Listing\Columns\Column;

/**
 * Class Name
 * @package RSHB\WhiteRabbit\Ui\Component\Listing\Column\Rabbit
 */
class Name extends Column
{
    /**
     * @param array $dataSource
     * @return array
     */
    public function prepareDataSource(array $dataSource)
    {
        if (isset($dataSource['data']['items'])) {
            foreach ($dataSource['data']['items'] as & $item) {
                $nameArray = [
                    $item['first_name'] ?: '',
                    $item['middle_name'] ?: '',
                    $item['last_name'] ?: ''
                ];

                $name = implode(' ', array_filter($nameArray));

                $item['name'] = $name;
            }
        }
        
        return $dataSource;
    }
}

И далее применяем его к нашей конфигурации:

<column name="name" class="RSHB\WhiteRabbit\Ui\Component\Listing\Column\Rabbit\Name" sortOrder="20">
    <settings>
        <filter>text</filter>
        <sortable>true</sortable>
        <label translate="true">Name</label>
    </settings>
</column>

Мы получаем в гриде колонку name которая отсутствует в изначальном объекте, при этом колонка объединяет три описанных выше колонки в одну строку.

Компоненты и шаблоны

Аналогичным образом можно подменить js-компонент и шаблон вместо тех, что используются по умолчанию:

<column name="name" class="RSHB\WhiteRabbit\Ui\Component\Listing\Column\Rabbit\Name" sortOrder="20">
    <settings>
        <filter>text</filter>
        <sortable>true</sortable>
        <label translate="true">Name</label>
        <component>RSHB_WhiteRabbit/js/listing/column</component>
        <template>ui/column/templates/name</template>
    </settings>
</column>

Выражения

Последней на сегодня темой я хотел бы выбрать выражения, которые встречаются в качестве значений некоторых параметров в settings секции. При первом знакомстве с выражениями у многих взрывается мозг в попытке понять, что это такое и как оно работает. Поскольку выражения чаще всего используются для работы с разного рода полями (field), наследниками базового компонента form, рассмотрим пример из конфигурации формы. Иногда можно встретить конструкции такого вида:

<item name="config" xsi:type="array">
...
<item name="filterBy" xsi:type="array">
    <item name="target" xsi:type="string">${ $.provider }:${ $.parentScope }.white</item>
    <item name="field" xsi:type="string">rabbit_id</item>
</item>
...
</item>

Обратим внимание на item с именем target. Здесь выражение всего лишь ссылается на выбранный элемент другого компонента с именем white. Когда это может быть полезным?

Например, когда необходимо сделать два зависимых списка, чтобы значения второго списка зависели от значения, выбранного в первом. Это можно реализовать путем фильтрации второго списка по атрибуту rabbit_id выбранного элемента первого списка. Вот как это будет выглядеть:

<field name="white" sortOrder="60">
     <argument name="data" xsi:type="array">
         <item name="options" xsi:type="object">RSHB\WhiteRabbit\Model\Config\Source\White</item>
         <item name="config" xsi:type="array">
             <item name="label" xsi:type="string" translate="true">White</item>
             <item name="dataType" xsi:type="string">text</item>
             <item name="formElement" xsi:type="string">select</item>
             <item name="source" xsi:type="string">rabbit</item>
             <item name="dataScope" xsi:type="string">white</item>
             <item name="validation" xsi:type="array">
                 <item name="required-entry" xsi:type="boolean">true</item>
             </item>
         </item>
     </argument>
 </field>

 <field name="rabbit" sortOrder="70">
     <argument name="data" xsi:type="array">
         <item name="options" xsi:type="object">RSHB\WhiteRabbit\Model\Config\Source\Rabbit</item>
         <item name="config" xsi:type="array">
             <item name="dataType" xsi:type="string">text</item>
             <item name="label" xsi:type="string" translate="true">Rabbit</item>
             <item name="formElement" xsi:type="string">multiselect</item>
             <item name="source" xsi:type="string">rabbit</item>
             <item name="dataScope" xsi:type="string">rabbit</item>
             <item name="filterBy" xsi:type="array">
                 <item name="target" xsi:type="string">${ $.provider }:${ $.parentScope }.white</item>
                 <item name="field" xsi:type="string">rabbit_id</item>
             </item>
         </item>
     </argument>
 </field>

Теперь в multiselect “rabbit” будут отображаться только элементы, у которых атрибут rabbit_id будет соответствовать атрибуту value выбранного в white элемента. 

Вы можете использовать выражения и в других случаях, когда нужно обратиться к динамическому значению другого компонента.

Заключение

В рамках одной статьи невозможно рассмотреть абсолютно все аспекты конфигурации Ui-компонентов. Я не ставил себе задачу описать буквально все, однако хотел дать базовое понимание предмета и основу для дальнейшего самостоятельного изучения и экспериментов. Надеюсь, описанное выше поможет читателю разобраться в этой непростой области разработки под М2.

На этом все. Пишите код с удовольствием. До встречи!