Magento 2 Create Custom Category Widget.

In Magento 2, There are many default widgets available in native.
You can check the list of the widget from Magento Opensource by Admin panel.

Go To Content -> Elements -> Widgets

Name of widgets which are an available default,
CMS Page link, CMS Static Block, Catalog Category Link, Catalog New Products List, Catalog Product Link, Catalog Products List, Orders and Returns, Recently Compared Products, Recently Viewed Products

We are going to create a new custom widget for display Category grid. In which you have to select the category from widget admin panel and based on the selected category you can get a grid of category with Category Image and name at a specific location.

For creating a custom widget you need to create a custom module.
For Declare module we need to create registration.php and module.xml file,
Let’s start with custom module Packagename is Rbj and Module name is CategoryWidget.

Create file, app/code/Rbj/CategoryWidget/registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Rbj_CategoryWidget',
    __DIR__
);

module.xml file, app/code/Rbj/CategoryWidget/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="Rbj_CategoryWidget"/>
</config>

For create a Custom widget, We need to create a widget.xml file under the, etc folder.
Using widget.xml You can define your custom widget to the admin panel.

File Path, app/code/Rbj/CategoryWidget/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="catalog_category_list"
            class="Rbj\CategoryWidget\Block\Category\Widget\CategoryList">
        <label translate="true">Catalog Category Grid</label>
        <description translate="true">Category Grid</description>
        <parameters>
            <parameter name="page_size" xsi:type="text" required="true" visible="true">
                <label translate="true">Number of Category to display</label>
                <value>5</value>
            </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>
            <parameter name="template" xsi:type="select" required="true" visible="true">
                <label>Template</label>
                <options>
                    <option name="grid" value="category/widget/grid.phtml" selected="true">
                        <label translate="true">Category Grid Template</label>
                    </option>
                    <option name="sidebar" value="category/widget/sidebar.phtml">
                        <label translate="true">Category Sidebar Template</label>
                    </option>
                </options>
            </parameter>
        </parameters>
        <containers>
            <container name="sidebar.main">
                <template name="default" value="sidebar" />
            </container>
            <container name="content">
                <template name="grid" value="grid" />
                <template name="list" value="list" />
            </container>
            <container name="sidebar.additional">
                <template name="default" value="sidebar" />
            </container>
        </containers>
    </widget>
</widgets>

For Define our custom logic, We need to create block class in the above file,
Our custom block file is Rbj\CategoryWidget\Block\Category\Widget\CategoryList and they implement BlockInterface from \Magento\Widget\Block\BlockInterface.

In the above XML file, We have defined two fields for no. of a category to display in a grid and the second field is for conditions in which you need to select categories for display in the widget.

Now we need to define our block file of a widget,

Create CategoryList.php file,
app/code/Rbj/CategoryWidget/Block/Widget/CategoryList.php

<?php
namespace Rbj\CategoryWidget\Block\Category\Widget;

use Magento\Framework\View\Element\Template;
use Magento\Framework\App\Filesystem\DirectoryList;

/**
 * Grid of Category component
 */
class CategoryList extends Template implements \Magento\Widget\Block\BlockInterface
{
    const PAGE_SIZE = 5;

    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        \Magento\Catalog\Helper\Category $category,
        \Magento\Catalog\Model\CategoryRepository $categoryRepository,
        \Magento\Widget\Helper\Conditions $conditionsHelper,
        \Magento\Framework\View\Asset\Repository $assetRepos,
        \Magento\Catalog\Helper\ImageFactory $helperImageFactory,
        \Magento\Framework\Image\AdapterFactory $imageFactory,
        \Magento\Framework\Filesystem $filesystem,
        array $data = []
    ) {
        $this->storeManager = $context->getStoreManager();
        $this->category = $category;
        $this->categoryRepository = $categoryRepository;
        $this->conditionsHelper = $conditionsHelper;
        $this->assetRepos = $assetRepos;
        $this->helperImageFactory = $helperImageFactory;
        $this->imageFactory = $imageFactory;
        $this->_filesystem = $filesystem;
        $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA);
        parent::__construct(
            $context,
            $data
        );
    }

    /**
     * Get small place holder image
     *
     * @return string
     */
    public function getPlaceHolderImage()
    {
        /** @var \Magento\Catalog\Helper\Image $helper */
        $imagePlaceholder = $this->helperImageFactory->create();
        return $this->assetRepos->getUrl($imagePlaceholder->getPlaceholder('small_image'));
    }
    /**
     * @param array $categoryIds
     * @return \Magento\Framework\Data\Collection
     */
    public function getCategory()
    {
        $categoryIds = $this->getCategoryIds();
        if(!empty($categoryIds)) {
            $ids = array_slice(explode(',', $categoryIds), 0, $this->getCategoryToDisplay());
            $category = [];
            foreach($ids as $id) {
                $category[] = $this->categoryRepository->get($id);
            }
            return $category;
        }
        return '';
    }

    /**
     * @param $imageName string imagename only
     * @param $width
     * @param $height
     */
    public function getResize($imageName,$width = 258,$height = 200)
    {
        $realPath = $this->_filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath('catalog/category/'.$imageName);
        if (!$this->_directory->isFile($realPath) || !$this->_directory->isExist($realPath)) {
            return false;
        }
        $targetDir = $this->_filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath('resized/'.$width.'x'.$height);
        $pathTargetDir = $this->_directory->getRelativePath($targetDir);
        if (!$this->_directory->isExist($pathTargetDir)) {
            $this->_directory->create($pathTargetDir);
        }
        if (!$this->_directory->isExist($pathTargetDir)) {
            return false;
        }

        $image = $this->imageFactory->create();
        $image->open($realPath);
        $image->keepAspectRatio(true);
        $image->resize($width,$height);
        $dest = $targetDir . '/' . pathinfo($realPath, PATHINFO_BASENAME);
        $image->save($dest);
        if ($this->_directory->isFile($this->_directory->getRelativePath($dest))) {
            return $this->storeManager->getStore()->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA).'resized/'.$width.'x'.$height.'/'.$imageName;
        }
        return false;
    }

    /**
     * Retrieve how many category should be displayed
     *
     * @return int
     */
    public function getCategoryToDisplay()
    {
        if (!$this->hasData('page_size')) {
            $this->setData('page_size', self::PAGE_SIZE);
        }
        return $this->getData('page_size');
    }

    /**
     * Retrieve category ids from widget
     *
     * @return string
     */
    public function getCategoryIds()
    {
        $conditions = $this->getData('conditions')
            ? $this->getData('conditions')
            : $this->getData('conditions_encoded');

        if ($conditions) {
            $conditions = $this->conditionsHelper->decode($conditions);
        }

        foreach ($conditions as $key => $condition) {
            if (!empty($condition['attribute']) && $condition['attribute'] == 'category_ids')    {
                return $condition['value'];
            }
        }
        return '';
    }
}

Now we need to create a template file which we have assigned to widget.xml file for display in the frontend.

<parameter name="template" xsi:type="select" required="true" visible="true">
    <label>Template</label>
    <options>
        <option name="grid" value="category/widget/grid.phtml" selected="true">
            <label translate="true">Category Grid Template</label>
        </option>
        <option name="sidebar" value="category/widget/sidebar.phtml">
            <label translate="true">Category Sidebar Template</label>
        </option>
    </options>
</parameter>

In the above code from the widget.xml file,

We have called two phtml files in a widget,
Create a phtml file for the Main content area,

app/code/Rbj/CategoryWidget/view/frontend/templates/category/widget/grid.phtml

<?php
/**
 * @var $block \Rbj\CategoryWidget\Block\Category\Widget\CategoryList
 */
?>
<div class="category-widget-main">
    <div class="category">
        <?php $_category = $block->getCategory(); ?>
        <?php foreach($_category as $category): ?>
            <div class="category-list">
                <a href="<?php echo $category->getUrl(); ?>">
                    <?php if($category->getImageUrl()): ?>
                        <?php $imageResize = $block->getResize($category->getImage());?>
                        <img src="<?php echo $imageResize; ?>" alt="<?php echo $category->getName(); ?>" />
                    <?php else: ?>
                        <img src="<?php echo $block->getPlaceHolderImage(); ?>" alt="<?php echo $category->getName(); ?>" />
                    <?php endif; ?>
                    <span><?php echo $category->getName(); ?></span>
                </a>
            </div>
        <?php endforeach; ?>
    </div>
</div>

Create the phtml file for sidebar area,

app/code/Rbj/CategoryWidget/view/frontend/templates/category/widget/sidebar.phtml

<?php
/**
 * @var $block \Rbj\CategoryWidget\Block\Category\Widget\CategoryList
 */
?>
<div class="category-widget-sidebar">
    <div class="category-sidebar">
        <?php $_category = $block->getCategory(); ?>
        <?php foreach($_category as $category): ?>
            <div class="category-list">
                <a href="<?php echo $category->getUrl(); ?>">
                    <?php if($category->getImageUrl()): ?>
                        <?php $imageResize = $block->getResize($category->getImage());?>
                        <img src="<?php echo $imageResize; ?>" alt="<?php echo $category->getName(); ?>" />
                    <?php else: ?>
                        <img src="<?php echo $block->getPlaceHolderImage(); ?>" alt="<?php echo $category->getName(); ?>" />
                    <?php endif; ?>
                    <span><?php echo $category->getName(); ?></span>
                </a>
            </div>
        <?php endforeach; ?>
    </div>
</div>

Run command to install our module,

php bin/magento setup:upgrade
php bin/magento cache:flush

Now you can go to widget section of Magento backend by refer below steps,
Go To Content -> Elements -> Widgets.
Click on Add Widget.

 

Custom widget
Custom widget in Magento 2

Now you need to set category from conditions tab,

category widget
select category from the category tree