There are times that you might want to give your client the ability to easily add or modify content on store front without using a full blown third-party extension. One common use case is a homepage slider or a simple content block in the sidebar with a catchy call to action. Using static blocks for that purpose is probably your first choice as it's quickest to implement, right? While it's undeniably the fastest way to to create an editable block it might not necessarily align with your client needs and expectations.

What's the problem

Imagine a situation that after you've created a nicely formatted html block, added a javascript snippet and made sure all is working and looking great, once your client starts fiddling with it often times you find yourself in a situation explaining them why something is "broken" after they've edited two lines of content. Usually it's because of something small like a misspelled class or a missing closing div, sometimes it's completely restructured html that the client unknowingly changed. Of course that's not because the client ignored your meticulously crafted instructions but because he's not as experienced a developer as you are and working with code is not something they shouldn't need to do anyway, however simple it might seem.

Widgets to the rescue

This is where widgets really shine. Creating a widget allows you separate your block structure, behavior and logic from the actual content that your client wants to present to their audience. It lets them add or edit content in the admin using simple forms with inputs, dropdown lists, image uploaders etc. while not even touching the code. It minimizes the risk of human error and allows you to be as flexible as you want with your implementation.

How to create a widget

While it's not as easy as creating a static block as it requires crafting custom module with a bunch of various php, phtml and xml files, you'll find that once you have a recipe for such a module you'll use it for majority of your custom front-end blocks.

Below are step by step instructions on how to create a sample widget that shows a list of featured products.

  1. Create registration.php for your custom module eg. app/code/RocketWeb/CusomWidget/registration.php
<?php
/**
* Custom Widget
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'RocketWeb_CustomWidget',
__DIR__
);
  1. Create module.xml in etc folder of of your custom module eg. app/code/RocketWeb/CustomWidget/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="RocketWeb_CustomWidget" setup_version="1.0.0">
        <sequence>
            <module name="Magento_CatalogWidget"/>
        </sequence>
    </module>
</config>
  1. Create widget.xml eg. app/code/RocketWeb/CustomWidget/etc/widget.xml
<?xml version="1.0" ?>
<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
    <widget class="RocketWeb\CustomWidget\Block\Product\ProductsList" id="eyebobs_customwidget_samplewidget">
        <label>RocketWeb Sample Widget</label>
        <description></description>
        <parameters>
            <parameter name="title" visible="true" xsi:type="text">
                <label>Title</label>
            </parameter>
            <parameter name="content" visible="true" xsi:type="block">
                <label>Content</label>
                <block class="RocketWeb\CustomWidget\Block\Adminhtml\Widget\TextField"/>
            </parameter>
            <parameter name="products_sort_by" xsi:type="select" visible="true" source_model="RocketWeb\CustomWidget\Model\SortBy">
                <label translate="true">Sort Products By</label>
            </parameter>
            <parameter name="layout" xsi:type="select" required="false" visible="true">
                <label translate="true">Layout</label>
                <options>
                    <option name="left" value="left" selected="true">
                        <label translate="true">Left-aligned</label>
                    </option>
                    <option name="right" value="right" selected="true">
                        <label translate="true">Right-aligned</label>
                    </option>
                </options>
            </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="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="custom" value="RocketWeb_CustomWidget::product/custom-grid.phtml" selected="true">
                        <label translate="true">Custom Grid 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 Outputcache.</description>
            </parameter>
            <parameter name="condition" xsi:type="conditions" visible="true" required="true" sort_order="10" class="Magento\CatalogWidget\Block\Product\Widget\Conditions">
                <label translate="true">Conditions</label>
            </parameter>
        </parameters>
    </widget>
</widgets>
  1. Create custom blocks eg. app/code/RocketWeb/CustomWidget/Adminhtml/Widget/TextField.php
<?php 
namespace Eyebobs\CustomWidget\Block\Adminhtml\Widget;
Class TextField extends \Magento\Backend\Block\Template{
    protected $_elementFactory;
    /**
     * @param \Magento\Backend\Block\Template\Context $context
    * @param \Magento\Framework\Data\Form\Element\Factory $elementFactory
    * @param array $data
    */
    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Framework\Data\Form\Element\Factory $elementFactory,
        array $data = []
    ) {
        $this->_elementFactory = $elementFactory;
        parent::__construct($context, $data);
    }
    /**
     * Prepare chooser element HTML
     *
     * @param \Magento\Framework\Data\Form\Element\AbstractElement $element Form Element
    * @return \Magento\Framework\Data\Form\Element\AbstractElement
    */
    public function prepareElementHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element)
    {
        $input = $this->_elementFactory->create("textarea", ['data' => $element->getData()]);
        $input->setId($element->getId());
        $input->setForm($element->getForm());
        $input->setClass("widget-option input-textarea admin__control-text");
        if ($element->getRequired()) {
            $input->addClass('required-entry');
        }

        $element->setData('after_element_html', $input->getElementHtml());
        return $element;
    }
}
  1. Create required source models eg. app/code/RocketWeb/CustomWidget/Model/SortBy.php
<?php
namespace RocketWeb\CustomWidget\Model;

class SortBy implements \Magento\Framework\Option\ArrayInterface
{
    public function toOptionArray()
    {
        return [
            ['value' => 'id', 'label' => __('Product ID')],
            ['value' => 'name', 'label' => __('Name')],
            ['value' => 'price', 'label' => __('Price')]
        ];
    }
}
  1. Create template files eg. app/code/RocketWeb/CustomWidget/view/frontend/templates/product/custom-grid.phtml
<div class="section">
        <h3 class="title"><?php echo $block->getData('title'); ?></h3>
        <p class="content" style="text-align:<?php echo ($block->getData('layout')) ? $block->getData('layout') : 'left'; ?>"><?php echo $block->getData('content'); ?></p
    </div>
    <?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())):?>
    <div class="products-grid">
    <?php
    $type = 'widget-product-grid';

    $mode = 'grid';

    $image = 'new_products_content_widget_grid';
    $title = $block->getTitle() ? __($block->getTitle()) : '';
    $items = $block->getProductCollection()->getItems();

    $showWishlist = true;
    $showCompare = true;
    $showCart = true;
    $templateType = \Magento\Catalog\Block\Product\ReviewRendererInterface::DEFAULT_VIEW;
    $description = false;
    $abstractProductBlock = $block->getLayout()->createBlock('\Magento\Catalog\Block\Product\AbstractProduct');
    ?>
            <?php $iterator = 1; ?>
            <?php foreach ($items as $_item): ?>
                <?php /* @escapeNotVerified */ echo($iterator++ == 1) ? '<div class="product-item custom">' : '</div><div class="product-item">' ?>
                <?php
                echo $this->getLayout()->createBlock("Magento\Framework\View\Element\Template")
                    ->setTemplate("Magento_Theme::html/catalog/product-item-info.phtml")
                    ->assign(['parentBlock' => $block, 'product' => $_item, 'image' => $image])
                    ->toHtml();
                ?>

                <?php echo($iterator == count($items)+1) ? '</li>' : '' ?>
            <?php endforeach ?>
    </div>
    <?php endif;?>
</div>

Conclusion

You've probably used widgets in Magento before but if you've never created one yourself now is a good time to pick it up. It will make your and your client's life much easier, I guarantee.