Skip to content
intermediate Phase 52 · UI Components

UI Components Deep Dive

Component architecture, XML configuration, JavaScript components, and data providers in Magento 2

1h
0 problems
Topic Progress 0%

Component Architecture

UI Component Tree

<!-- Full UI component structure -->
<listing>
    <settings>
        <!-- Spinner component for loading state -->
        <spinner>listingColumns</spinner>

        <!-- Component dependencies -->
        <deps>
            <dep>listing.listing_columns</dep>
            <dep>listing.listing_filters</dep>
        </deps>

        <!-- External component deps -->
        <externalDeps>
            <dep>listing.data_source</dep>
        </externalDeps>
    </settings>

    <!-- Data source -->
    <dataSource name="listing_data_source">
        <argument name="data" xsi:type="array">
            <item name="js_config" xsi:type="array">
                <item name="component" xsi:type="string">Magento_Ui/js/data/provider</item>
            </item>
        </argument>
        <settings>
            <updateUrl path="mui/index/render"/>
        </settings>
        <dataProvider class="Magento\Ui\Model\DataProvider" name="listing_data_source">
            <settings>
                <requestFieldName>id</requestFieldName>
                <updateRequestFieldName>ids</updateRequestFieldName>
            </settings>
        </dataProvider>
    </dataSource>

    <!-- Listing columns -->
    <listingColumns name="listing_columns">
        <settings>
            <childDefaults>
                <provider>listing_data_source</provider>
                <dataType>text</dataType>
            </childDefaults>
        </settings>
    </listingColumns>
</listing>

Component Lifecycle

1. Initialization: defaults applied → initObservable → initialize
2. Data loading: dataSource.getData() → components receive data
3. Rendering: template rendered with KnockoutJS bindings
4. Interaction: user actions → observables update → UI re-renders
5. Destruction: component cleanup

Advanced XML Configuration

Component Arguments

<!-- Pass arguments to components -->
<column name="custom">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="filter" xsi:type="string">text</item>
            <item name="dataType" xsi:type="string">text</item>
            <item name="label" xsi:type="string" translate="true">Custom Column</item>
            <item name="sortOrder" xsi:type="string">100</item>
            <item name="visible" xsi:type="boolean">true</item>
            <item name="sortable" xsi:type="boolean">true</item>
            <item name="editable" xsi:type="boolean">false</item>
        </item>
    </argument>
</column>

Data Mapping

<!-- Map data between components -->
<listing>
    <settings>
        <dataMapping>
            <item name="id" xsi:type="string">entity_id</item>
            <item name="label" xsi:type="string">name</item>
        </dataMapping>
    </settings>
</listing>

Component Listeners

<!-- Listen to other component changes -->
<column name="dependent_column">
    <settings>
        <imports>
            <link name="value">
                <link>source.independent_column:value</link>
            </link>
        </imports>
        <exports>
            <link name="value">
                <link>target_component:value</link>
            </link>
        </exports>
        <listens>
            <link name="value">
                <link>source_column:value</link>
            </link>
        </listens>
    </settings>
</column>

JavaScript UI Components

Component Class

// app/code/Vendor/Module/view/adminhtml/web/js/component/custom
define([
    'Magento_Ui/js/form/element/abstract',
    'ko'
], function (Abstract, ko) {
    'use strict';

    return Abstract.extend({
        defaults: {
            template: 'Vendor_Module/component/custom',
            imports: {
                externalValue: '${externalComponent}:value'
            },
            exports: {
                value: '${targetComponent}:value'
            },
            listens: {
                value: 'onValueChange'
            }
        },

        initObservable: function () {
            this._super()
                .observe({
                    value: null,
                    customProp: false
                });

            return this;
        },

        initialize: function () {
            this._super();
            this.value.subscribe(this.onChange.bind(this));
            return this;
        },

        onValueChange: function (newValue) {
            console.log('Value changed:', newValue);
        },

        getValue: function () {
            return this.value();
        },

        setValue: function (val) {
            this.value(val);
        }
    });
});

Component Template

<!-- Vendor/Module/view/adminhtml/web/template/component/custom.html -->
<div class="custom-component" data-bind="visible: visible()">
    <label data-bind="text: label"></label>
    <input type="text" data-bind="value: value, attr: { placeholder: placeholder }">
    <span data-bind="text: value, visible: displayValue()"></span>
</div>

Data Providers

Custom Data Provider

namespace Vendor\Module\Ui\DataProvider;

class CustomDataProvider extends \Magento\Ui\Model\DataProvider
{
    public function __construct(
        string $name,
        string $primaryFieldName,
        string $requestFieldName,
        \Magento\Framework\Db\Select $select,
        array $meta = [],
        array $data = []
    ) {
        parent::__construct($name, $primaryFieldName, $requestFieldName, [], $meta, $data);
        $this->setSelect($select);
    }

    public function getData(): array
    {
        $items = $this->getCollection()->getItems();

        $result = [];
        foreach ($items as $item) {
            $result[$item->getId()] = $item->getData();
        }

        return [
            'totalRecords' => $this->getCollection()->getSize(),
            'items' => $result,
        ];
    }

    public function getMeta(): array
    {
        $meta = parent::getMeta();

        $meta['listing_columns']['children']['custom_field']['arguments']['data']['config'] = [
            'dataType' => 'text',
            'formElement' => 'input',
            'component' => 'Magento_Ui/js/form/element/text',
            'label' => __('Custom Field'),
        ];

        return $meta;
    }
}

Data Provider Configuration

<dataSource name="custom_source">
    <settings>
        <deps>
            <dep>custom_source</dep>
        </deps>
        <updateUrl path="mui/index/render"/>
    </settings>
    <dataProvider class="Vendor\Module\Ui\DataProvider\CustomDataProvider" name="custom_source">
        <settings>
            <requestFieldName>id</requestFieldName>
            <updateRequestFieldName>ids</updateRequestFieldName>
            <filterUrlParams>
                <param name="id">entity_id</param>
            </filterUrlParams>
        </settings>
    </dataProvider>
</dataSource>

Quiz

1. What is the UI component lifecycle?

Question 1 options

2. What do imports/exports do in XML config?

Question 2 options

3. What must a data provider implement?

Question 3 options

Flashcards

Question

UI component lifecycle?

Answer

defaults → initObservable → initialize → load data → render

Question

imports/exports purpose?

Answer

Auto-sync data between UI components

Question

Data provider getData() returns?

Answer

['totalRecords' => n, 'items' => [...]]

Question

initObservable() purpose?

Answer

Initialize observable properties for reactive UI

Revision Notes

Key Takeaways

  • 1. UI components follow a defined lifecycle with initialization and data loading
  • 2. XML configuration supports args, imports, exports, and listeners
  • 3. JavaScript components extend abstract classes and use KnockoutJS observables
  • 4. Data providers supply data via getData() with totalRecords and items
  • 5. Components can sync data automatically via imports/exports

Interview Tips

  • Explain the full component lifecycle from XML to rendered UI
  • Describe how components communicate via imports/exports
  • Discuss data provider architecture and getData() contract

Cheat Sheet

UI Components:
  XML: <listing>/<form> root
  Config: args, imports, exports, listeners
  JS: extend abstract, initObservable
  Data: getData() → totalRecords + items

Lifecycle:
  defaults → initObservable → initialize → render

Sync:
  imports: receive from other components
  exports: send to other components
  listens: react to changes