19

I've configured a grid component in Magento -- and the sorting behavior seems broken. Where can I debug this on the javascript level, and/or does anyone else have any idea why this might be happening?

If I sort the grid once, an ajax request is made, and everything sorts correctly.

enter image description here

However, the second sort, without an ajax request, renders the grid with all the same IDs.

enter image description here

The behavior is not repeated on Magento core grids, so I'm pretty sure this is something I'm doing. I just don't know the ui component system well enough to know where to start debugging this.

2 Answers 2

26

Alright, I can't pretend to understand why yet, but the problem was the data argument of my dataProvider argument.

<!-- ... -->
<argument name="dataProvider" xsi:type="configurableObject">
    <!-- ... --->
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="update_url" xsi:type="url" path="mui/index/render"/>
        </item>
    </argument>
    <!-- ... -->
</argument>
<!-- ... -->

When I compared this to a few of the core grids, the data argument was missing a storageConfig node, with an indexField sub-node with the primary key of my model.

<argument name="data" xsi:type="array">
    <item name="config" xsi:type="array">
        <item name="update_url" xsi:type="url" path="mui/index/render"/>
        <item name="storageConfig" xsi:type="array">
            <item name="indexField" xsi:type="string">pulsestorm_commercebug_log_id</item>
        </item>                    

    </item>                          
</argument>

When I added these nodes, sorting functionality was restored.

3
  • Just ran into the same problem, I imagine it is falling back or loading values from storage by the row index rather than data row id, although it makes no sense why the data is duplicated. Thank you for the answer. Jul 18, 2017 at 8:14
  • Thanks @alan , This worked for me. Mar 20, 2020 at 12:08
  • 1
    Thank @alan worked for me too.
    – thelettuce
    Aug 13, 2020 at 15:40
8

TL;DR

This is an interesting problem indeed.

Here's how I understood the system but I may not be 100% right.

As you have seen clicking the header column generates an AJAX request to the following route: /admin_key/mui/index/render with the following parameters:

  • filters[placeholder]
  • isAjax
  • namespace
  • paging[current]
  • paging[pageSize]
  • search
  • sorting[direction]
  • sorting[field]

The last one is the field on which you're sorting your grid.

This route is declared by default in app/code/Magento/Ui/view/base/ui_component/etc/definition.xml:

<insertListing class="Magento\Ui\Component\Container">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="component" xsi:type="string">Magento_Ui/js/form/components/insert-listing</item>
            <item name="update_url" xsi:type="url" path="mui/index/render"/>
            <item name="render_url" xsi:type="url" path="mui/index/render"/>
            <item name="autoRender" xsi:type="boolean">false</item>
            <item name="dataLinks" xsi:type="array">
                <item name="imports" xsi:type="boolean">true</item>
                <item name="exports" xsi:type="boolean">false</item>
            </item>
            <item name="realTimeLink" xsi:type="boolean">true</item>
        </item>
    </argument>
</insertListing>

But in a listing ui_component XML it is also declared:

        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
                <item name="update_url" xsi:type="url" path="mui/index/render"/>
                <item name="storageConfig" xsi:type="array">
                    <item name="indexField" xsi:type="string">page_id</item>
                </item>
            </item>
        </argument>

This route is handled by app/code/Magento/Ui/Controller/Adminhtml/Index/Render.php based on the namespace parameter (which is normally the name of your UI Component)

public function execute()
{
    if ($this->_request->getParam('namespace') === null) {
        $this->_redirect('admin/noroute');
        return;
    }

    $component = $this->factory->create($this->_request->getParam('namespace'));
    $this->prepareComponent($component);
    $this->_response->appendBody((string) $component->render());
}

Where the prepareComponent method is recursive on the child components:

protected function prepareComponent(UiComponentInterface $component)
{
    foreach ($component->getChildComponents() as $child) {
        $this->prepareComponent($child);
    }
    $component->prepare();
}

When the column component is prepared, the column sorting is handled by app/code/Magento/Ui/Component/Listing/Columns/Column.php:

public function prepare()
{
    $this->addFieldToSelect();

    $dataType = $this->getData('config/dataType');
    if ($dataType) {
        $this->wrappedComponent = $this->uiComponentFactory->create(
            $this->getName(),
            $dataType,
            array_merge(['context' => $this->getContext()], (array) $this->getData())
        );
        $this->wrappedComponent->prepare();
        $wrappedComponentConfig = $this->getJsConfig($this->wrappedComponent);
        // Merge JS configuration with wrapped component configuration
        $jsConfig = array_replace_recursive($wrappedComponentConfig, $this->getJsConfig($this));
        $this->setData('js_config', $jsConfig);

        $this->setData(
            'config',
            array_replace_recursive(
                (array)$this->wrappedComponent->getData('config'),
                (array)$this->getData('config')
            )
        );
    }

    $this->applySorting();

    parent::prepare();
}

Where the applySorting() method is based on the sorting parameter and it simply adds the order to the data provider:

protected function applySorting()
{
    $sorting = $this->getContext()->getRequestParam('sorting');
    $isSortable = $this->getData('config/sortable');
    if ($isSortable !== false
        && !empty($sorting['field'])
        && !empty($sorting['direction'])
        && $sorting['field'] === $this->getName()
    ) {
        $this->getContext()->getDataProvider()->addOrder(
            $this->getName(),
            strtoupper($sorting['direction'])
        );
    }
}

Once every component is prepared, the action class renders the component (again recursively) for the response:

$this->_response->appendBody((string) $component->render());

I reckon those are the important PHP steps of what happens during the sorting.

Now to the JS, the render and update URLs (declared in definition.xml above) are assigned to the element in app/code/Magento/Ui/view/base/web/js/form/components/insert.js:

return Element.extend({
    defaults: {
        content: '',
        template: 'ui/form/insert',
        showSpinner: true,
        loading: false,
        autoRender: true,
        visible: true,
        contentSelector: '${$.name}',
        externalData: [],
        params: {
            namespace: '${ $.ns }'
        },
        renderSettings: {
            url: '${ $.render_url }',
            dataType: 'html'
        },
        updateSettings: {
            url: '${ $.update_url }',
            dataType: 'json'
        },
        imports: {},
        exports: {},
        listens: {},
        links: {
            value: '${ $.provider }:${ $.dataScope}'
        },
        modules: {
            externalSource: '${ $.externalProvider }'
        }
    }

Still in this file, there's a requestData method that is used to retrieve the AJAX data:

    requestData: function (params, ajaxSettings) {
        var query = utils.copy(params);

        ajaxSettings = _.extend({
            url: this['update_url'],
            method: 'GET',
            data: query,
            dataType: 'json'
        }, ajaxSettings);

        this.loading(true);

        return $.ajax(ajaxSettings);
    }

You can see that this method is called when the render() method is called:

        $.async({
            component: this.name,
            ctx: '.' + this.contentSelector
        }, function (el) {
            self.contentEl = $(el);
            self.startRender = true;
            params = _.extend({}, self.params, params || {});
            request = self.requestData(params, self.renderSettings);
            request
                .done(self.onRender)
                .fail(self.onError);
        });

Once this is done, a callback method is called to apply the data. It is onRender() :

    onRender: function (data) {
        this.loading(false);
        this.set('content', data);
        this.isRendered = true;
        this.startRender = false;
    }

I reckon that's where the new content is being applied.

1
  • 2
    Code Columbo... Jul 18, 2017 at 8:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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