Custom products grid – Base XML

Welcome to the second part of my article series about Magento Custom Products Grid. In the previous part, we:

  • made some assumptions about the structure of the database,
  • created the controller and the layout,
  • added the virtual types for the filters and the data providers.

Now, let’s continue creating our custom products grid view using the UI Component. In this part, we will create a base XML file which will define the UI Component and show all of its benefits.

UI Component XML

First of all, we have to prepare a new XML file which will define our UI Component. In order to do this, in view/adminhtml location, we have to create a new ui_component folder and a new file called: myproducts_listing.xml. This name needs to be the same as we defined in the my_products_index_index.xml layout file

// file: Magently/MyUiComponent/view/adminhtml/ui_component/myproducts_listing.xml

<?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="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">
                myproducts_listing.myproducts_listing_data_source
            </item>
        </item>
    </argument>

</listing>

Now, we will define the listing (grid) component. Everything we add here will be inside <listing></listing> tags.
Then we have to define a provider. The value myproducts_listing is the name of our UI Component and myproducts_listing_data_source which we defined in etc/di.xml file.

1. Settings block

Now, we have to define our component settings inside <settings> tag:

<settings>
</settings>

Here we can add buttons which will be displayed at the top of the view:

// file: Magently/MyUiComponent/view/adminhtml/ui_component/myproducts_listing.xml

<?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="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">
                myproducts_listing.myproducts_listing_data_source
            </item>
        </item>
    </argument>

</listing>

We add one button with a label “Add New”. The URL indicates */*/addNew controller, so we have to add a new class AddNew in Controller/Adminhtml/Index location to handle this button. I will not discuss it here, let’s focus on the UI Component. The “primary” class means, that the button will be highlighted with orange style (for default “Magento backend” theme). When we remove this class, then the button will be presented as a normal text. You can add as many buttons as you need in a similar way.

Next, we set a spinner and a dependency on a component initialization. For the <spinner> tag, we set some names which we will define later in this XML file by <columns> tag. For <deps> we set the same text as for the provider above.

2. DataSource block

The next step is to set up a data source:

// file: Magently/MyUiComponent/view/adminhtml/ui_component/myproducts_listing.xml

<?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="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">
                myproducts_listing.myproducts_listing_data_source
            </item>
        </item>
    </argument>

</listing>

Let’s go through this:

We have set a myproducts_listing_data_source identifier as a name of the Data Source. This name was used previously many times, so now we can see where it’s defined.

  • For <storageConfig>, we set indexField with “id” value. It’s a primary index column name from our database table.
  • For <dataProvider> tag we set the class as MyProductsGridDataProvider defined previously in the etc/di.xml file and with the same name as myproducts_listing_data_source.

Inside <dataProvider> tag we have to define two parameters requestFieldName and primaryFieldName. Those values will be passed to Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider class. The primaryFieldName is the name of the main index in our database table and requestFieldName is the name of the request parameter.

3. Toolbar block

Here, we define our toolbar’s components and behavior.

// file: Magently/MyUiComponent/view/adminhtml/ui_component/myproducts_listing.xml

<listing>
    <!-- ... other block of code -->
    <listingToolbar name="listing_top">
        <settings>
            <sticky>true</sticky>
        </settings>

        <bookmark name="bookmarks"/>

        <columnsControls name="columns_controls"/>

        <exportButton name="export_button"/>

        <filterSearch name="fulltext"/>

        <paging name="listing_paging"/>

        <filters name="listing_filters"/>

    </listingToolbar>

</listing>
  1. First, we set “sticky” to true. This is a great feature that makes the toolbar and headers of the custom products grid always visible even when you scroll down the page.
  2. We can add a UI Bookmark component. It’s useful for remembering the current state of view like applied filters, pagination, presented columns, etc. The data of UI Bookmarks is stored in the database in the “ui_bookmark” table.
  3. We can add the column controls. This component displays a special selector which allows the user to select which columns of our table should be presented or not.
  4. We can add an export button. This functionality allows a user to export current view as CSV or Excel XML file.
  5. We can add a full-text search. This enables the “search by keyword” functionality in the view.
  6. Here we should also add the <paging> tag. It adds the whole pagination functionality with a button to select how many items per page should be displayed. Without this paging, all items would be displayed on one page. It’s not good for the performance when we have hundreds of items, so don’t forget to add it.
  7. Finally, we can add filters for search by specific fields. Later, we will define which fields will be used in the filters by adding columns to our XML.

Obviously, most of the features presented above are optional, except the pagination, which should always be there.

4. Column

Finally, we add columns to our grid view:

// file: Magently/MyUiComponent/view/adminhtml/ui_component/myproducts_listing.xml

<listing>
    <!-- ... other block of code -->
    <columns name="myproducts_columns">
        <column name="id" sortOrder="10">
            <settings>
                <filter>textRange</filter>
                <label translate="true">ID</label>
                <sorting>asc</sorting>
            </settings>
        </column>

        <column name="name" sortOrder="20">
            <settings>
                <filter>text</filter>
                <label translate="true">Name</label>
            </settings>
        </column>

        <column name="price"
                class="Magento\Catalog\Ui\Component\Listing\Columns\Price"
                sortOrder="30">
            <settings>
                <filter>textRange</filter>
                <label translate="true">Price</label>
            </settings>
        </column>

        <column name="image" 
                class="Magently\MyUiComponent\Ui\Component\Listing\Columns\Image"
                component="Magento_Ui/js/grid/columns/thumbnail"
                sortOrder="40">
            <settings>
                <hasPreview>1</hasPreview>
                <sortable>false</sortable>
                <label translate="true">Image</label>
            </settings>
        </column>

        <column name="description" sortOrder="50">
            <settings>
                <label translate="true">Description</label>
                <visible>false</visible>
            </settings>
        </column>

        <column name="last_update"
                class="Magento\Ui\Component\Listing\Columns\Date" 
                component="Magento_Ui/js/grid/columns/date"
                sortOrder="60">
            <settings>
                <filter>dateRange</filter>
                <dataType>date</dataType>
                <label translate="true">Last Update</label>
            </settings>
        </column>

        <column name="is_valid"
                component="Magento_Ui/js/grid/columns/select"
                sortOrder="70">
            <settings>
                <options class="Magento\Config\Model\Config\Source\Yesno"/>
                <filter>select</filter>
                <dataType>select</dataType>
                <label translate="true">Is Valid</label>
            </settings>
        </column>

    </columns>

</listing>

In the code above we have added all columns with the same names as defined in the database table. All columns also have a “sortOrder” argument set so we can decide what order they will appear in.

Let’s describe each column:

  1. First, we have “id”.
    • We set this column <filter> tag to value “testRange” so the user can filter by id ranges (example: 1-100). Alternatively, you can set this field value to “text” if you want to filter by a single value. If you remove <filter> tag the ID field will not be presented in the filters.
    • In the <label> tag we put column’s name which will appear in the column header. The “translate” argument set to “true” means that Magento will be using it to change this text to a proper one depending on the used language and available translations.
    • By <sorting> tag, we can decide which column will be used for default sorting – in our case it’s ID. As a value, we can set “asc” or “desc”. When we omit this tag, the sorting will not be used and the data will be fetched in the order set in the database.
  2. Nothing exceptional for the “name” column. We only add this field to filter by plain text.
  3. For the “price” column, we use a class attribute with a value which indicates: Magento\Catalog\Ui\Component\Listing\Columns\Price. This Magento class is used to format the price value with the appropriate currency available in the current store. Of course, if you want, you can create your own class with another way to format the value.
  4. In the “image” column, we want to display the thumbnail image. Its name is stored in our table in the database.
    • We need to set the “class” attribute here with Magently\MyUiComponent\Ui\Component\Listing\Columns\Image. We’ll create that class in a moment. Next, we have a “component” attribute with a value set to JavaScript file: Magento_Ui/js/grid/columns/thumbnail. This component handles displaying the image and click events (modal popup) in the grid. To enable a modal with an image preview, we set “hasPreview” to “1”. Picking “0” disables the popup.
    • We have also this new <sortable> tag set to “false”. This means that it won’t be possible to sort the grid by this column, assuming that it makes no sense to sort by image.
      The file structure and its description can be found below.
  5. For the “description” column, we can use the new settings <visible> with value “false”. This means that by default this column will not be visible in the custom products grid. User can still make it visible by using column controls we mentioned earlier.
  6. The next column is “last_update”. It’s a date, so we use the default Magento class and component to render this value in the format “MMM d, YYYY h:mm:ss A”. If you want, you can prepare your own class and/or JavaScript to use another format of date and time. Note, that for the filter we use “dateRange” and set “dataType” as “date”. This will make the selector show up as a calendar.
  7. Last column (“is_valid”) is a boolean field. 
    1. Here we use Magento select component. It’s needed to display boolean values “Yes/No” rather than “1/0”. In settings, we have a new <options> tag with Magento\Config\Model\Config\Source\Yesno class. It’s a simple default Magento source for Yes/No selector. If you want different options, you can prepare your own class and put it here. 
    2. If we want to use this field in the filters, we set “filter” and “dataType” to “select”. With this option in the filter, by “Is Valid” label, select control values “Yes/No/Empty” will be displayed instead of a simple text field.

Now, the promised Image.php file:

// file: Magently/MyUiComponent/Ui/Component/Listing/Columns/Image.php

<?php

namespace Magently\MyUiComponent\Ui\Component\Listing\Columns;

/**
 * Class for displaying images in UI Component Grid view
 */
class Image extends \Magento\Ui\Component\Listing\Columns\Column
{
    const ALT_FIELD = 'No image';
    const THUMB_PREFIX = 't_';

    /**
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    private $storeManager;

    /**
     * @var \Magento\Catalog\Helper\Image
     */
    private $helperImage;

    /**
     * @param \Magento\Framework\View\Element\UiComponent\ContextInterface $context
     * @param \Magento\Framework\View\Element\UiComponentFactory $uiComponentFactory
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Catalog\Helper\Image $helperImage
     * @param array $components
     * @param array $data
     */
    public function __construct(
        \Magento\Framework\View\Element\UiComponent\ContextInterface $context,
        \Magento\Framework\View\Element\UiComponentFactory $uiComponentFactory,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Catalog\Helper\Image $helperImage,
        array $components = [],
        array $data = []
    ) {
        parent::__construct($context, $uiComponentFactory, $components, $data);
        $this->storeManager = $storeManager;
        $this->helperImage = $helperImage;
    }

    /**
     * Prepare Data Source
     *
     * @param array $dataSource
     * @return array
     */
    public function prepareDataSource(array $dataSource)
    {
        if (isset($dataSource['data']['items'])) {
            // get URL to media directory, e.g. http://localhost/magento/media/
            $urlMedia = $this->storeManager
                ->getStore()
                ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA);

            // get field name, in our case it's "image"
            $fieldName = $this->getData('name');

            foreach ($dataSource['data']['items'] as & $item) {
                $import = new \Magento\Framework\DataObject($item);
                $image = $import->getImage();

                $imageUrls = $this->getImageUrls($image, $urlMedia);

                $item[$fieldName . '_src'] = $imageUrls['thumb'];
                $item[$fieldName . '_alt'] = $image ?: self::ALT_FIELD;
                $item[$fieldName . '_link'] = $imageUrls['image'];
                $item[$fieldName . '_orig_src'] = $imageUrls['image'];
            }
        }

        return $dataSource;
    }

    /**
     * Get URLs to large and thumbail image
     *
     * @param string|null $filename
     * @param string $urlMedia
     * @return array
     */
    private function getImageUrls($filename, string $urlMedia)
    {
        if (empty($filename)) {
            // if no filename then get URLs to placeholders
            return [
                'image' => $this->helperImage->getDefaultPlaceholderUrl('image'),
                'thumb' => $this->helperImage->getDefaultPlaceholderUrl('thumbnail'),
            ];
        }

        // prepare link to preview/big image
        $image = $urlMedia . $filename;

        // prepare link to thumbnail
        $thumb =  $urlMedia .
                  self::THUMB_PREFIX .
                  $filename;

        return [
            'image' => $image,
            'thumb' => $thumb,
        ];
    }
}

To change the column rendering, we have to extend the Magento\Ui\Component\Listing\Columns\Column class and override the prepareDataSource function.
What we want to do here, is to iterate all the custom products grid items and prepare each item’s data like this:

  • “image_src” with a URL to a thumbnail image;
  • “image_alt” with the value for “alt” image attribute, in our case, it’s a file name stored in the database;
  • “image_orig_src” and “image_link” with a URL to the original big image. The “image_link” opens “Go To Details Page” which is presented in a popup preview. In our case, this click action will open the image, however, you can use something more practical like redirect to edit page.

The assumptions for our code are:

  • The images are located directly in the media folder.
  • Database stores filenames of “big” images (with extensions)
  • All thumbnails have a “t_” prefix in the filename, e.g.: “t_some_image.jpg”.

Why my XML changes are not applied?

Please note, that when you change something in the UI Component XML file, after flushing the cache, the changes may still not be applied. The reason is a default UI Bookmark which is stored in the database and affects our view. To fix this, go to database and remove rows from “ui_bookmark” table where “namespace” is “myproducts_listing”. Refresh the page again to see your changes.

Custom products grid – Summary

The basic UI component listing is ready and should be working properly with all the features like:

  1. Display grid with our data stored in the database
  2. Pagination
  3. Sticky headers
  4. Filters
  5. Export data to files
  6. Fulltext search
  7. Bookmarks
  8. Columns controls

In this article you learned how to:

  • hide columns by default,
  • set default sorting of columns in the grid 
  • disable column from sorting,
  • manage the filters (simple text, ranges, selection, data picker),
  • display images in the grid,
  • display price, datetime values and boolean options,
  • add buttons at the top of the view.

But it’s not all that a UI component can do. It also allows us to improve the custom products grid view further by adding more advanced functionalities, like mass action, columns action, inline edit. I will describe it in part 3 of this article.