16

In Magento 2's UI Form Component configuration files, you'll often see a item attribute with the same of source -- <item name="source" xsi:type="string">block</item> below.

#File: vendor/magento/module-cms/view/adminhtml/ui_component/cms_block_form.xml
<field name="title">
    <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">Block Title</item>
            <item name="formElement" xsi:type="string">input</item>
            <item name="source" xsi:type="string">block</item>
            <item name="sortOrder" xsi:type="number">20</item>
            <item name="dataScope" xsi:type="string">title</item>
            <item name="validation" xsi:type="array">
                <item name="required-entry" xsi:type="boolean">true</item>
            </item>
        </item>
    </argument>
</field>    

What are these fields for? I ask because it seems like they're not necessary. For example, the module in this GitHub repository configures a working UI Component form, but doesn't use these name="source" items.

Does anyone know what these name="source" items are for? I'm aware of the UI Component mechanic that takes the XML and cofigures it as x-magento-init JSON

"block_id": {
    "type": "form.input",
    "name": "block_id",
    "dataScope": "block_id",
    "config": {
        "component": "Magento_Ui\/js\/form\/element\/abstract",
        "template": "ui\/form\/field",
        "visible": false,
        "dataType": "text",
        "formElement": "input",
        "source": "block"
    }
},

Which is fed into a uiElement based Knockout view model object. However, it's not clear how the nested tree of uiElement based Knockout view model objects use these field level source fields.

If I look at the uiElement's initModules method

    initModules: function () {
        _.each(this.modules, function (name, property) {
            if (name) {
                this[property] = this.requestModule(name);
            }
        }, this);

        if (!_.isFunction(this.source)) {
            this.source = registry.get(this.provider);
        }

        return this;
    },

I see the object references a source property, and if its not set, will reach into the registry for an object using the provider property as a string/key identifier. It seems like the value of these source items isn't used. However, its possible that they're used by the PHP code, or some other javascript code. Hence, my question.

7

The source is, or should be, the data provider. From what I can tell, however, the <item name="source"> node in the XML example you gave makes no measurable difference and can be removed without consequence.

Here's how I came to that: in the initModules() method of elements/element.js, there is a check to see if this.source is a callable function:

if (!_.isFunction(this.source)) {
    this.source = registry.get(this.provider);
}

If this.source is not a callable function, it overrides this.source with a UI Component from the registry using this.provider. Again, this is the provider, though, and not the source. As such, if the source is not a callable function at that point, it simply loads the provider and the original this.source goes the way of the wind.

this.source often is empty, but in the case of the cms_block_form, this.source would be 'block' to start with. Since that is a string and not a callable function, it is simply overridden.

Also note that a UI Component could easily add some logic to set this.source to a callable function, based off of a string from XML, before initModules() is run.


Now, why is this source there in the first place? I don't know why it's in the XML, but it serves a purpose in the Javascript. For an example, I pulled up grid/columns/column.js. In defaults: {}, the following is there:

modules: {
    source: '${ $.provider }'
}

Back in elements/element.js, this is evaluated in initModules():

_.each(this.modules, function (name, property) {
    if (name) {
        this[property] = this.requestModule(name);
    }
}, this);

Here is the requestModule() method:

requestModule: function (name) {
    var requested = this._requesetd;
    if (!requested[name]) {
        requested[name] = registry.async(name);
    }
    return requested[name];
},

The async() method is returned from the registry, and in initModules(), assigned to the property given. In this case, this.source is set to be the async() method from the registry. This would happen for anything within modules:{}, not just source, but sheds light on what happens with the source in some components. The async() function returned from is - not surprisingly - a callable function. As a result, this evaluates to false and is skipped:

initModules: function () {
    ...

    if (!_.isFunction(this.source)) {
        this.source = registry.get(this.provider);
    }

    return this;
}, 

Back in grid/columns/column.js, source is used to change the sorting of the grid.

exportSorting: function () {
    ...
    this.source('set', 'params.sorting', {
        field: this.index,
        direction: this.sorting
    });
},

The async() method handles the functionality, but here, it is calling the set() method on this.source(). The source, or, dataProvider, is grid/provider.js and it does not have a set() method. It's parent, element/element.js does, though:

set: function (path, value) {
    var data = this.get(path),
        diffs;

    diffs = !_.isFunction(data) && !this.isTracked(path) ?
        utils.compare(data, value, path) :
        false;

    utils.nested(this, path, value);

    if (diffs) {
        this._notifyChanges(diffs);
    }

    return this;
},

The concept with set() is somewhat simple in that it updates values and notifies subscribers. So, as a result of columns.js declaring a source, it has direct access to execute methods on it's dataProvider.


Conclusion: the source appears to be what is used, at least in Javascript classes, as the data provider. If a source is set in a Javascript class, and is a callable function, it can be used to execute methods directly on the dataProvider.

This leaves me with a few questions still, though:

  • Is it possible to use source in XML in order to proxy a dataProvider class?
  • Was it supposed to serve a purpose in XML but get deprecated at some point?
  • Are there any core classes that look at this.source (from XML) and do something interesting with it before initModules() is run?
  • 1
    +1 for useful information, but end up with the same question I have -- what's source doing in those XML files :) – Alan Storm Feb 16 '17 at 16:08
6

Went to "the source" (groan) for this one and it looks like these <item name="source"/> nodes are, indeed, redundant. Or, the Magento engineer currently in charge of them thinks they're redundant, so that's as close to truth as we'll get.

3

The source is the key using which the ui component can read the data provided by "DataProvider" class. It is very useful when there are multiple tabs and fieldsets.

For example : refer module-customer/view/base/ui_component/customer_form.xml

<fieldset name="customer">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="label" xsi:type="string" translate="true">Account Information</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="formElement" xsi:type="string">input</item>

                **<item name="source" xsi:type="string">customer</item>**

            </item>
        </argument>
    </field>
. 
. 
.

<fieldset name="address">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="is_collection" xsi:type="boolean">true</item>
            <item name="label" xsi:type="string" translate="true">Addresses</item>
            <item name="removeMessage" xsi:type="string" translate="true">Are you sure you want to delete this item?</item>
        </item>
    </argument>
    <field name="parent_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">number</item>
                <item name="formElement" xsi:type="string">input</item>

                **<item name="source" xsi:type="string">address</item>**

            </item>
        </argument>
    </field>

The getData() method in DataProvider class will return an array with keys 'customer' and 'address' and the corresponding fields in the field-sets will be mapped from it. The screen-shot displays the result of getData() method.

Output of getData() method of DataProvider class

Afterwards when the getDataSourceData() method in Magento\Ui\Component\Form is called it processes the above data.

public function getDataSourceData()
{
    $dataSource = [];

    $id = $this->getContext()->getRequestParam($this->getContext()->getDataProvider()->getRequestFieldName(), null);
    $filter = $this->filterBuilder->setField($this->getContext()->getDataProvider()->getPrimaryFieldName())
        ->setValue($id)
        ->create();
    $this->getContext()->getDataProvider()
        ->addFilter($filter);

    $data = $this->getContext()->getDataProvider()->getData();

    if (isset($data[$id])) {
        $dataSource = [
            'data' => $data[$id]
        ];
    } elseif (isset($data['items'])) {
        foreach ($data['items'] as $item) {
            if ($item[$item['id_field_name']] == $id) {
                **$dataSource = ['data' => ['general' => $item]];**
            }
        }
    }
    return $dataSource;
}
  • Thank you for answering. However, are you sure about this? I'm not sure you're correct. Yes, on the customer form the JSON data has a key named customer, and that key coincidentally uses the name name as the <item name="source node. However, I don't see any PHP code that references the data in the source node. Also, the CMS Page form has a <item name="source" xsi:type="string">page</item> node and its data source data does not have a page key. Finally, my research indicates its name="dataScope" that determines where a field gets its values. – Alan Storm Feb 16 '17 at 15:45
  • 1
    yes, you are right Alan. During debugging I also saw the same thing (about dataScope). Thanks for the clarification. If I get anything more about "source" I will post. – Pankaj Bhope Feb 18 '17 at 14:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.