First you will need to create a module. Create the directory structure. In our case it is going to be app/code/Techflarestudio/Recently

First you will need to register the module:

<?php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Techflarestudio_Recently',
    __DIR__
);
registration.php
<?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="Techflarestudio_Recently" setup_version="0.1.0">
        <sequence>
            <module name="Magento_Catalog"/>
        </sequence>
    </module>
</config>
etc/module.xml


    {
    "name": "techflarestudio/recently",
    "description": "",
    "require": {
        "php": "~5.5.0|~5.6.0|~7.0.0",
        "magento/module-catalog": "null",

        "magento/magento-composer-installer": "*"
    },
    "suggest": {

    },
    "type": "magento2-module",
    "version": "0.1.0",
    "license": [

    ],
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Techflarestudio\\Recently\\": ""
        }
    },
    "extra": {
        "map": [
            [
                "*",
                "Techflarestudio/Recently"
            ]
        ]
    }
}

   
composer.json

In our example we have a custom product attribute ingredients and we are going to include this attribute to be loaded in the recently viewed widget list view. So lets start by including the attribute in the widget options:

<?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_recently_viewed">
        <parameters>
            <parameter name="show_attributes" xsi:type="multiselect" required="true" visible="true">
                <options>
                    <option name="ingredients" value="ingredients">
                        <label translate="true">Ingredients</label>
                    </option>
                </options>
            </parameter>
        </parameters>
    </widget>
</widgets>

Once added you will be able to choose the option in Widgets->Choose Widget->Widget Options->Product attributes to show

This will make sure that the functionality can be enabled/disabled by the store admin.

If we inspect how the core listing widget works it is rendering the description area in vendor/magento/module-catalog/view/base/web/template/product/list/listing.html

<div if="getRegion('description-area')().length"
       class="product-item-description">
       <fastForEach args="data: getRegion('description-area'), as: '$col'" >
            <render args="$col.getBody()"/>
       </fastForEach>
</div>

As you can see the knockout.js template is fetching the description-area nodes and rendering each of them. The nodes are defined in vendor/magento/module-catalog/view/frontend/ui_component/widget_recently_viewed.xml

So the next step is for us to include our attribute as column in the widget definition.

<?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">
    <columns name="widget_columns">
        <column name="ingredients" component="Techflarestudio_Recently/js/product/ingredients" displayArea="description-area">
            <settings>
                <label translate="true">ingredients</label>
                <bodyTmpl>Techflarestudio_Recently/product/ingredients</bodyTmpl>
            </settings>
        </column>
    </columns>
</listing>
view/frontend/ui_component/widget_recently_viewed.xml

Couple of things to note here.

  • we define our column name
  • we define the component that will be used to render the element - Techflarestudio_Recently/js/product/ingredients
  • we define the template for the column - Techflarestudio_Recently/product/ingredients

Once we have defined this the next step is to create the template and the component:

<strong if="isAllowed($row())" class="product product-item-ingredients ingredients">
    <span class="label" text="label" />
    <span class="value" text="getValue($row())" />
</strong>
view/frontend/web/template/product/ingredients.html
define([
    'Magento_Ui/js/grid/columns/column',
    'Magento_Catalog/js/product/list/column-status-validator'
], function (Column, columnStatusValidator) {
    'use strict';

    return Column.extend({

        /**
         * @param row
         * @returns {boolean}
         */
        hasValue: function (row) {
            return "ingredients" in row['extension_attributes'];
        },

        /**
         * @param row
         * @returns {*}
         */
        getValue: function (row) {
            return row['extension_attributes']['ingredients'];
        },

        /**
         * @param row
         * @returns {*|boolean}
         */
        isAllowed: function (row) {
            return (columnStatusValidator.isValid(this.source(), 'ingredients', 'show_attributes') && this.hasValue(row) );
        }

    });
});
view/frontend/web/js/product/ingredients.js

As you can in the component the value is being fetched from extension_attributes array. So before this can work we need to make sure that the data is loaded in the object. To do this define your extension attribute in xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Magento\Catalog\Api\Data\ProductRenderInterface">
        <attribute code="ingredients" type="string"/>
    </extension_attributes>
</config>
etc/di.xml

Next pass in the data as an argument for productProviders:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Catalog\Ui\DataProvider\Product\ProductRenderCollectorComposite">
        <arguments>
            <argument name="productProviders" xsi:type="array">
                <item name="ingredients" xsi:type="object">\Techflarestudio\Recently\Ui\DataProvider\Product\Listing\Collector\Ingredients</item>
            </argument>
        </arguments>
    </type>
</config>
etc/di.xml

You can see that we have defined a new class for DataProvider. We will use this to pass in the custom attribute data.

<?php

namespace Techflarestudio\Recently\Ui\DataProvider\Product\Listing\Collector;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\Data\ProductRenderExtensionFactory;
use Magento\Catalog\Api\Data\ProductRenderInterface;
use Magento\Catalog\Ui\DataProvider\Product\ProductRenderCollectorInterface;

/**
 * Class Ingredients
 * @package Techflarestudio\Recently\Ui\DataProvider\Product\Listing\Collector
 */
class Ingredients implements ProductRenderCollectorInterface
{
    const KEY = "ingredients";

    /**
     * @var ProductRenderExtensionFactory
     */
    private $productRenderExtensionFactory;

    /**
     * Sku constructor.
     * @param ProductRenderExtensionFactory $productRenderExtensionFactory
     */
    public function __construct(
        ProductRenderExtensionFactory $productRenderExtensionFactory
    ) {
        $this->productRenderExtensionFactory = $productRenderExtensionFactory;
    }

    /**
     * @param ProductInterface $product
     * @param ProductRenderInterface $productRender
     */
    public function collect(ProductInterface $product, ProductRenderInterface $productRender)
    {
        $extensionAttributes = $productRender->getExtensionAttributes();

        if (!$extensionAttributes) {
            $extensionAttributes = $this->productRenderExtensionFactory->create();
        }

        if($ingredients = $product->getIngredients()) {
            $extensionAttributes->setIngredients($ingredients);
        }

        $productRender->setExtensionAttributes($extensionAttributes);
    }
}
Ui/DataProvider/Product/Listing/Collector/Ingredients.php

In the collect function we are first fetching an array of the existing extension attributes, then checking if our product attribute is set and pass it in to product renderer class. We are using core classes for ProductRender factories and making sure that the data is passed in correctly.

That is it. Make sure to

  • enable the new widget option
  • refresh generated content
  • clear cache

If everything went well you should be able to see the newly added attribute in the recently viewed widget product description area.