Magento 2 custom widget

In one of our previous articles we learned how to create a widget. Now we will see how we can create a custom one, or even better, how to extend the core one. For this example I picked default catalog product listing widget that I will extend with sorting fields for better customization of this widget.

Module setup

First we need to create a new module. This requires namespace and module folders with registration.php and etc/module.xml inside module folder. For this example I am going to use Inchoo for namespace and CatalogWidget for module name. If you are unfamiliar with development preparations and creation of custom modules in magento 2, we have a great article to get you started.

registration.php

<?php
MagentoFrameworkComponentComponentRegistrar::register(
    MagentoFrameworkComponentComponentRegistrar::MODULE,
    'Inchoo_CatalogWidget',
    __DIR__
);

etc/module.xml

<?xml version="1.0"?>
<config xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi_noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Inchoo_CatalogWidget" setup_version="1.0.0" >
        <sequence>
            <module name="Magento_CatalogWidget"/>
        </sequence>
    </module>
</config>

Custom code

First we will create widget configuration which is almost identical to one used in extended widget. We do this by creating widget.xml file in etc folder. Apart from label and description nodes changes, there are two extra parameters collection_sort_by and collection_sort_order that we will later use to sort our product collection. Parameter type is select, and they are using our custom source models we will create in the following steps. Other differences are container nodes that I will explain later on, and options for template parameter. I added our own template option with name top_products. This will be our own custom template to render products list just how we want to. Last but not least important is custom widget placeholder image. We will place it at view/adminhtml/web/images/inchoo_widget_block.png.

etc/widget.xml

<?xml version="1.0" encoding="UTF-8"?>
<widgets xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi_noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
    <widget id="inchoo_products_list" class="InchooCatalogWidgetBlockProductProductsList"
            placeholder_image="Inchoo_CatalogWidget::images/inchoo_widget_block.png">
        <label translate="true">Inchoo Catalog Products List</label>
        <description>Inchoo - Extended Catalog Products List</description>
        <parameters>
            <parameter name="title" xsi_type="text" required="false" visible="true">
                <label translate="true">Title</label>
            </parameter>
            <parameter name="collection_sort_by" xsi_type="select" visible="true"
                       source_model="InchooCatalogWidgetModelConfigSourceSortBy">
                <label translate="true">Sort Collection By</label>
            </parameter>
            <parameter name="collection_sort_order" xsi_type="select" visible="true"
                       source_model="InchooCatalogWidgetModelConfigSourceSortOrder">
                <label translate="true">Sort Collection Order</label></parameter>
            <parameter name="show_pager" xsi_type="select" visible="true"
                       source_model="MagentoConfigModelConfigSourceYesno">
                <label translate="true">Display Page Control</label>
            </parameter>
            <parameter name="products_per_page" xsi_type="text" required="true" visible="true">
                <label translate="true">Number of Products per Page</label>
                <depends>
                    <parameter name="show_pager" value="1" />
                </depends>
                <value>5</value>
            </parameter>
            <parameter name="products_count" xsi_type="text" required="true" visible="true">
                <label translate="true">Number of Products to Display</label>
                <value>10</value>
            </parameter>
            <parameter name="template" xsi_type="select" required="true" visible="true">
                <label translate="true">Template</label>
                <options>
                    <option name="default" value="Magento_CatalogWidget::product/widget/content/grid.phtml" selected="true">
                        <label translate="true">Products Grid Template</label>
                    </option>
                    <option name="top_products" value="Inchoo_CatalogWidget::product/widget/content/top_products.phtml">
                        <label translate="true">Top Products Template</label>
                    </option>
                </options>
            </parameter>
            <parameter name="cache_lifetime" xsi_type="text" visible="true">
                <label translate="true">Cache Lifetime (Seconds)</label>
                <description translate="true">86400 by default, if not set. To refresh instantly, clear the Blocks HTML Output cache.</description>
            </parameter>
            <parameter name="condition" xsi_type="conditions" visible="true" required="true" sort_order="10"
                       class="MagentoCatalogWidgetBlockProductWidgetConditions">
                <label translate="true">Conditions</label>
            </parameter>
        </parameters>
        <containers>
            <container name="content">
                <template name="grid" value="default" />
                <template name="top-products" value="top_products" />
            </container>
            <container name="content.top">
                <template name="grid" value="default" />
            </container>
        </containers>
    </widget>
</widgets>

We have two custom parameters with two custom source models. I like to organize my modules just like core magento, so I will create models at Model/Config/Source.

Model/Config/Source/SortBy.php

<?php
namespace InchooCatalogWidgetModelConfigSource;
 
class SortBy implements MagentoFrameworkOptionArrayInterface
{
    public function toOptionArray()
    {
        return [
            ['value' => 'name', 'label' => __('Product Name')],
            ['value' => 'price', 'label' => __('Price')]
        ];
    }
}

Model/Config/Source/SortOrder.php

<?php
namespace InchooCatalogWidgetModelConfigSource;
 
class SortOrder implements MagentoFrameworkOptionArrayInterface
{
    public function toOptionArray()
    {
        return [
            ['value' => 'asc', 'label' => __('Ascending')],
            ['value' => 'desc', 'label' => __('Descending')]
        ];
    }
}

Next step is to create a custom block that our widget will use.To make things easier we will extend ProductList block from Magento_CatalogWidget. This will give us all existing features of catalog product listing widget that Magento prepared for us.
We are overriding createCollection method from ProductList to add sorting order on collection. I added “collection_sort_by” and “collection_sort_order” attribute getters with fallback on default values that are defined at the start of the class. This custom parameters are defined in widget.xml configuration.

Block/Product/ProductsList.php

<?php
namespace InchooCatalogWidgetBlockProduct;
 
class ProductsList extends MagentoCatalogWidgetBlockProductProductsList
{
    const DEFAULT_COLLECTION_SORT_BY = 'name';
    const DEFAULT_COLLECTION_ORDER = 'asc';
 
    public function createCollection()
    {
        /** @var $collection MagentoCatalogModelResourceModelProductCollection */
        $collection = $this->productCollectionFactory->create();
        $collection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds());
 
        $collection = $this->_addProductAttributesAndPrices($collection)
            ->addStoreFilter()
            ->setPageSize($this->getPageSize())
            ->setCurPage($this->getRequest()->getParam($this->getData('page_var_name'), 1))
            ->setOrder($this->getSortBy(), $this->getSortOrder());
 
        $conditions = $this->getConditions();
        $conditions->collectValidatedAttributes($collection);
        $this->sqlBuilder->attachConditionToCollection($collection, $conditions);
 
        return $collection;
    }
 
    public function getSortBy()
    {
        if (!$this->hasData('collection_sort_by')) {
            $this->setData('collection_sort_by', self::DEFAULT_COLLECTION_SORT_BY);
        }
        return $this->getData('collection_sort_by');
    }
 
    public function getSortOrder()
    {
        if (!$this->hasData('collection_sort_order')) {
            $this->setData('collection_sort_order', self::DEFAULT_COLLECTION_ORDER);
        }
        return $this->getData('collection_sort_order');
    }
}

Code is ready, it’s time for testing.picking_widget

Containers

While creating widgets we have layout update options where we can specify on which page and in which container will our widget be rendered at. To show its full potential I will create our custom template file. For this example I copied the normal product listing template. Again, trying to keep the consistency, I am mimicking the path of template creation to view/frontend/templates/product/widget/content/top_products.phtml. In our widget configuration we specified two containers, one named content, and the other one named content.top. In the first one I defined two templates, grid and top-products. Values of this nodes will show template options with same names. This gives us ability to allow certain custom templates to be available for rendering in only certain containers.

<container name="content">
    <template name="grid" value="default" />
    <template name="top-products" value="top_products" />
</container>
<container name="content.top">
    <template name="grid" value="default" />
</container>

Main Content Area – name="content", template select is available.
layout_updates_1
Main Content Top – name="content.top, template select is not available.
layout_updates_2edited
Source available on github.