Magento 2.1 – Load Listing Component for Custom Model on Product Edit Page

gridlisting-componentmagento-2.1magento2

As the title suggests, I'm trying to add a listing component for a custom model on the catalog/product/edit page. But I'm currently stuck, although I have the feeling I'm almost there!

The problem is that I see the headers of the grid, but the message 'We couldn't find any records' remains and the spinner is not going away (giving me the impression that I missed some configuration somewhere).

My setup:

I've added a modifier to the catalog product form modifier pool using etc/adminhtml/di.xml like so:

<!--
    We add something to the Catalog Product pool like so:
-->
<virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool"
             type="Magento\Ui\DataProvider\Modifier\Pool">
    <arguments>
        <argument name="modifiers" xsi:type="array">
            <!--
                Add a UI component
            -->
            <item name="attachments" xsi:type="array">
                <item name="class" xsi:type="string">Vendor\ProductAttachment\Ui\DataProvider\Product\Form\Modifier\ProductAttachment</item>
                <item name="sortOrder" xsi:type="number">115</item>
            </item>
        </argument>
    </arguments>
</virtualType>

(it's a product attachment module)

The is ProductAttachment.php:modifyMeta():

/**
 * Modify the metadata
 *
 * @param array $meta
 * @return array
 */
public function modifyMeta(array $meta)
{
    $meta = array_replace_recursive(
        $meta,
        [
            // Key = group name
            'product_attachment' => [
                // Array with children:
                'children' => [
                    // Include button that opens grid in new modal:
                    'product_attachment' => [
                        // Set the children:
                        'children' => [
                            // The button:
                            'button_set' => [
                                'arguments' => [
                                    'data' => [
                                        'config' => [
                                            'formElement' => 'container',
                                            'componentType' => 'container',
                                            'label' => false,
                                            'content' => __('Manage Product Attachments'),
                                            'template' => 'ui/form/components/complex'
                                        ]
                                    ]
                                ],
                                'children' => [
                                    'button_product_attachment' => [
                                        'arguments' => [
                                            'data' => [
                                                'config' => [
                                                    'formElement' => 'container',
                                                    'componentType' => 'container',
                                                    'component' => 'Magento_Ui/js/form/components/button',
                                                    'actions' => [
                                                        [
                                                            'targetName' => 'product_form.product_form.product_attachment.product_attachment.modal',
                                                            'actionName' => 'openModal'
                                                        ],
                                                        [
                                                            'targetName' => 'product_form.product_form.product_attachment.product_attachment.modal.listing',
                                                            'actionName' => 'render'
                                                        ]
                                                    ],
                                                    'title' => __('Show Product Attachments'),
                                                    'provider' => null
                                                ]
                                            ]
                                        ]
                                    ]
                                ]
                            ],
                            // The modal:
                            'modal' => [
                                'arguments' => [
                                    'data' => [
                                        'config' => [
                                            'componentType' => 'modal',
                                            'dataScope' => '',
                                            'options' => [
                                                'title' => __('Manage Product Attachments Modal'),
                                                'buttons' => [
                                                    // Cancel button:
                                                    [
                                                        'text' => __('Cancel'),
                                                        'actions' => [
                                                            'closeModal'
                                                        ]
                                                    ],
                                                    // Add button:
                                                    [
                                                        'text' => __('Link Attachments to Product'),
                                                        'class' => 'action-primary',
                                                        'actions' => [
                                                            // Save and close:
                                                            [
                                                                'targetName' => 'index = product_attachment_listing',
                                                                'actionName' => 'save'
                                                            ],
                                                            'closeModal'
                                                        ]
                                                    ]
                                                ]
                                            ]
                                        ]
                                    ]
                                ],
                                // This is where the grid / listing is rendered:
                                'children' => [
                                    'listing' => [
                                        'arguments' => [
                                            'data' => [
                                                'config' => [
                                                    'componentType' => 'insertListing',
                                                    'dataScope' => 'product_attachment_listing',
                                                    'ns' => 'product_attachment_listing',       // This is the namespace
                                                    'render_url' => $this->urlBuilder->getUrl('mui/index/render', []), // Explain
                                                ]
                                            ]
                                        ]
                                    ]
                                ]
                            ]
                        ],
                        // Configuration:
                        'arguments' => [
                            'data' => [
                                'config' => [
                                    'additionalClasses' => 'admin__fieldset-section',
                                    'label' => __('Manage Product Attachments'),
                                    'collapsible' => false,
                                    'componentType' => 'fieldset'
                                ]
                            ]
                        ]
                    ],
                ],
                // Arguments, information etc:
                'arguments' => [
                    'data' => [
                        'config' => [
                            'label' => __('Product Attachments'),
                            'collapsible' => true,
                            'componentType' => 'fieldset',
                            'dataScope' => ''
                        ]
                    ]
                ]
            ]
        ]
    );

    return $meta;
}

The above code works, since it adds an extra row below the product form, nicely with a button that when I click it opens a modal.

As you might have noticed, I point to product_attachment_listing. So let's take a look at that:

<?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="context" xsi:type="configurableObject">
        <argument name="class" xsi:type="string">Magento\Framework\View\Element\UiComponent\Context</argument>
        <argument name="namespace" xsi:type="string">product_attachment_listing</argument>
    </argument>
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="provider" xsi:type="string">product_attachment_listing.product_attachment_listing_data_source</item>
            </item>
            <item name="deps" xsi:type="string">product_attachment_listing.product_attachment_listing_data_source</item>
        </item>
        <item name="spinner" xsi:type="string">product_attachment_columns</item>
    </argument>
    <!--
        Set the datasource:
    -->
    <dataSource name="product_attachment_listing_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <argument name="class" xsi:type="string">Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider</argument>
            <argument name="name" xsi:type="string">product_attachment_listing_data_source</argument>
            <argument name="primaryFieldName" xsi:type="string">id</argument>
            <argument name="requestFieldName" xsi:type="string">id</argument>
            <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">id</item>
                    </item>
                </item>
            </argument>
        </argument>
    </dataSource>
    <!--
        The columns:
    -->
    <columns name="product_attachment_columns">
        <column name="id">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="label" xsi:type="string" translate="true">ID</item>
                </item>
            </argument>
        </column>
        <column name="filename">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="label" xsi:type="string" translate="true">File Name</item>
                </item>
            </argument>
        </column>
    </columns>
</listing>

The data source is defined in etc/di.xml:

<!--
    Add Product Attachments collection is a data provider:
-->
<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
    <arguments>
        <argument name="collections" xsi:type="array">
            <item name="product_attachment_listing_data_source" xsi:type="string">Vendor\ProductAttachment\Model\ResourceModel\Attachment\Grid\Collection</item>
        </argument>
    </arguments>
</type>
<!--
    Add parameters for the constructor of \Vendor\ProductAttachment\Model\ResourceModel\Attachment\Grid\Collection
-->
<type name="Vendor\ProductAttachment\Model\ResourceModel\Attachment\Grid\Collection">
    <arguments>
        <argument name="mainTable" xsi:type="string">productattachment</argument>
        <argument name="eventPrefix" xsi:type="string">productattachment_block_grid_collection</argument>
        <argument name="eventObject" xsi:type="string">productattachment_grid_collection</argument>
        <argument name="resourceModel" xsi:type="string">Vendor\ProductAttachment\Model\ResourceModel\Attachment</argument>
    </arguments>
</type>

And to show you the Grid Collection, it's pretty straight-forward:

namespace Vendor\ProductAttachment\Model\ResourceModel\Attachment\Grid;

use Magento\Framework\Api\Search\AggregationInterface;
use Magento\Framework\Api\Search\SearchResultInterface;
use Vendor\ProductAttachment\Model\ResourceModel\Attachment\Collection as AttachmentCollection;
use Magento\Framework\Api\SearchResultsInterface;

/**
 * Class Collection
 * This is the collection that is used in grids
 * @package Vendor\ProductAttachment\Model\ResourceModel\Attachment\Grid
 */
class Collection extends AttachmentCollection implements SearchResultInterface
{
    /**
     * Collection constructor.
     * @param \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory
     * @param \Psr\Log\LoggerInterface $logger
     * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
     * @param \Magento\Framework\Event\ManagerInterface $eventManager
     * @param \Magento\Framework\DB\Adapter\AdapterInterface $mainTable
     * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $eventPrefix
     * @param $eventObject
     * @param $resourceModel
     * @param string $model
     * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection
     * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb|null $resource
     */
    public function __construct(
        \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory,
        \Psr\Log\LoggerInterface $logger,
        \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
        \Magento\Framework\Event\ManagerInterface $eventManager,
        // The following 4 parameters are set in etc/di.xml
        $mainTable,
        $eventPrefix,
        $eventObject,
        $resourceModel,
        $model = 'Magento\Framework\View\Element\UiComponent\DataProvider\Document',
        \Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
        \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null
    ) {
        parent::__construct(
            $entityFactory,
            $logger,
            $fetchStrategy,
            $eventManager,
            $connection,
            $resource
        );

        $this->_eventPrefix = $eventPrefix;
        $this->_eventObject = $eventObject;
        $this->_init($model, $resourceModel);
        $this->setMainTable($mainTable);
    }

    /**
     * @var AggregationInterface
     */
    protected $aggregations;

    // The following methods are sort of stubs:

    /**
     * @return AggregationInterface
     */
    public function getAggregations()
    {
        return $this->aggregations;
    }

    /**
     * @param AggregationInterface $aggregations
     */
    public function setAggregations($aggregations)
    {
        $this->aggregations = $aggregations;
    }

    /**
     * @return null
     */
    public function getSearchCriteria()
    {
        return null;
    }

    /**
     * @param array|null $items
     * @return $this
     */
    public function setItems(array $items = null)
    {
        return $this;
    }

    /**
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return $this
     */
    public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria)
    {
        return $this;
    }

    /**
     * @return int
     */
    public function getTotalCount()
    {
        return $this->getSize();
    }

    /**
     * @param int $totalCount
     * @return $this
     */
    public function setTotalCount($totalCount)
    {
        return $this;
    }
}

I think I've now shown you most of the code that I use to create a product listing in a modal on a product edit-page. Like I said, the modal pops out, and I see that a grid is loaded / generated (because I can see the headers for ID and filename), but then it just seems to stop working. The spinner never leaves and the grid is never populated.

I also don't have any JS errors in my console.

For what's it worth: when the modal is opened, a XHR-request is done to http://www.vendor.com/admin/mui/index/render/?namespace=product_attachment_listing&isAjax=true

The response of that request is as follows:

<!--
/**
 * Copyright &copy; 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<div class="admin__data-grid-outer-wrap" data-bind="scope: 'product_attachment_listing.product_attachment_listing'">
    <div data-role="spinner"
         data-component="product_attachment_listing.product_attachment_listing.product_attachment_columns"
         class="admin__data-grid-loading-mask">
        <div class="spinner">
            <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
        </div>
    </div>
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">{
        "*": {
            "Magento_Ui/js/core/app": {
                "types": {
                    "dataSource": [],
                    "text": {
                        "component": "Magento_Ui\/js\/form\/element\/text",
                        "extends": "product_attachment_listing"
                    },
                    "column.text": {
                        "component": "Magento_Ui\/js\/form\/element\/text",
                        "extends": "product_attachment_listing"
                    },
                    "columns": {
                        "extends": "product_attachment_listing"
                    },
                    "listing": {
                        "config": {
                            "provider": "product_attachment_listing.product_attachment_listing_data_source"
                        },
                        "deps": "product_attachment_listing.product_attachment_listing_data_source",
                        "extends": "product_attachment_listing"
                    },
                    "html_content": {
                        "component": "Magento_Ui\/js\/form\/components\/html",
                        "extends": "product_attachment_listing"
                    }
                },
                "components": {
                    "product_attachment_listing": {
                        "children": {
                            "product_attachment_listing": {
                                "type": "product_attachment_listing",
                                "name": "product_attachment_listing",
                                "children": {
                                    "product_attachment_columns": {
                                        "type": "columns",
                                        "name": "product_attachment_columns",
                                        "children": {
                                            "id": {
                                                "type": "column.text",
                                                "name": "id",
                                                "config": {
                                                    "component": "Magento_Ui\/js\/grid\/columns\/column",
                                                    "componentType": "column",
                                                    "dataType": "text",
                                                    "label": "ID"
                                                }
                                            },
                                            "filename": {
                                                "type": "column.text",
                                                "name": "filename",
                                                "config": {
                                                    "component": "Magento_Ui\/js\/grid\/columns\/column",
                                                    "componentType": "column",
                                                    "dataType": "text",
                                                    "label": "File Name"
                                                }
                                            }
                                        },
                                        "config": {
                                            "component": "Magento_Ui\/js\/grid\/listing",
                                            "componentType": "columns",
                                            "storageConfig": {
                                                "provider": "ns = ${ $.ns }, index = bookmarks",
                                                "namespace": "current"
                                            },
                                            "childDefaults": {
                                                "storageConfig": {
                                                    "provider": "ns = ${ $.ns }, index = bookmarks",
                                                    "root": "columns.${ $.index }",
                                                    "namespace": "current.${ $.storageConfig.root }"
                                                }
                                            }
                                        }
                                    }
                                },
                                "config": {
                                    "component": "uiComponent"
                                }
                            },
                            "product_attachment_listing_data_source": {
                                "type": "dataSource",
                                "name": "product_attachment_listing_data_source",
                                "dataScope": "product_attachment_listing",
                                "config": {
                                    "data": {
                                        "items": [
                                            {
                                                "id_field_name": "id",
                                                "id": "1",
                                                "filename": "\/attachments\/2\/0\/2015-10-20t06-48_transaction_747982541978651-1643286.pdf",
                                                "product_id": "26",
                                                "orig_data": null
                                            }
                                        ],
                                        "totalRecords": 1
                                    },
                                    "component": "Magento_Ui\/js\/grid\/provider",
                                    "update_url": "http:\/\/www.vendor.com\/admin\/mui\/index\/render\/",
                                    "storageConfig": {
                                        "indexField": "id"
                                    },
                                    "params": {
                                        "namespace": "product_attachment_listing"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }</script>
</div>

If anyone could help me with this I would be really thankful. I've been struggling with this for 2-3 days now …

Best Answer

Why is it that always when I post a question on Stack Exchange, I find out the answer myself a couple of moments later? Is it some sort of digital rubber ducking? Anyway...

My mistake was in product_attachment_listing.xml. I stated:

<argument name="data" xsi:type="array">
    <item name="js_config" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="provider" xsi:type="string">product_attachment_listing.product_attachment_listing_data_source</item>
        </item>
        <item name="deps" xsi:type="string">product_attachment_listing.product_attachment_listing_data_source</item>
    </item>
    <item name="spinner" xsi:type="string">product_attachment_columns</item>
</argument>

But this needed to be:

<argument name="data" xsi:type="array">
    <item name="js_config" xsi:type="array">
        <item name="provider" xsi:type="string">product_attachment_listing.product_attachment_listing_data_source</item>
        <item name="deps" xsi:type="string">product_attachment_listing.product_attachment_listing_data_source</item>
    </item>
    <item name="spinner" xsi:type="string">product_attachment_columns</item>
</argument>

I guess I made the mistake because I followed the official documentation (http://devdocs.magento.com/guides/v2.1/ui-components/ui-listing-grid.html) :-/

Hope this saves somebody some days smashing their head on their keyboard.