Create Dynamic Custom Tabs on Product Page – Part 2

This article is 2nd part of Create Dynamic Custom Tabs on Product View Page. To get good understanding you should read first article before.

In this article we will cover following points:

  • Display simple tab title and its description.
  • Display product attribute in custom tab.
  • Render a whole template in custom tab.

We will retrieve all this information from database collection and process it to display on frontend.

First thing first, let’s begin with creating database table. Create InstallSchema class:

Arsal/CustomTab/Setup/InstallSchema.php

<?php
namespace Arsal\CustomTab\Setup;

use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

/**
 * Class InstallSchema
 * @package Arsal\CustomTab\Setup
 */
class InstallSchema implements InstallSchemaInterface
{

    /**
     * @param SchemaSetupInterface $setup
     * @param ModuleContextInterface $context
     * @throws \Zend_Db_Exception
     */
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;

        $installer->startSetup();
        $table = $installer->getConnection()
            ->newTable($installer->getTable('customtab'))
            ->addColumn(
                'entity_id',
                Table::TYPE_INTEGER,
                null,
                [
                    'identity' => true,
                    'unsigned' => true,
                    'nullable' => false,
                    'primary' => true,
                ],
                'Entity id'
            )
            ->addColumn(
                'identifier',
                Table::TYPE_TEXT,
                254,
                [
                    'nullable'  => false,
                    'unique'    =>  true
                ],
                'Tab Title'
            )
            ->addColumn(
                'title',
                Table::TYPE_TEXT,
                254,
                [
                    'nullable' => false,
                    'default' => '',
                ],
                'Tab Title'
            )
            ->addColumn(
                'description',
                Table::TYPE_TEXT,
                '2M',
                [],
                'Tab Description'
            )
            ->addColumn(
                'type',
                Table::TYPE_INTEGER,
                null,
                [],
                'Tab Type'
            )
            ->addColumn(
                'custom_data',
                Table::TYPE_TEXT,
                '2M',
                [],
                'Custom Template Data'
            )
            ->addColumn(
                'sort_order',
                Table::TYPE_INTEGER,
                null,
                [],
                'Sort Order'
            )
            ->addColumn(
                'created_at',
                Table::TYPE_TIMESTAMP,
                null,
                [
                    'nullable' => false,
                    'default' => 'CURRENT_TIMESTAMP'
                ],
                'Date Created'
            )
            ->addColumn(
                'updated_at',
                Table::TYPE_TIMESTAMP,
                null,
                [
                    'nullable' => false,
                    'default' => 'CURRENT_TIMESTAMP'
                ],
                'Date Updated'
            )
            ->setComment('Custom Tabs')
            ->setOption('type', 'InnoDB')
            ->setOption('charset', 'utf8');

        // Create Table.
        $installer->getConnection()->createTable($table);

        $installer->startSetup();
    }
}

Columns description:

entity_id: Entity ID and primary key of entity.
identifier: Unique and used as tabs key.
title: Current Tab title.
description: Tab custom content.
Type: Integer type, could be 1 or 0. If it is set 1 custom template will be used as tab content else description will be used as tab content.
custom_data: customer data stored in json format which keeps information for template class, view file and its name.
sort_order: Display order of custom tabs.

Create customtab model:
Arsal/CustomTab/Model/Customtab.php

<?php
namespace Arsal\CustomTab\Model;

use Arsal\CustomTab\Model\ResourceModel\Customtab as ResourceModel;
use Magento\Framework\DataObject\IdentityInterface;
use Magento\Framework\Model\AbstractModel;

/**
 * Class Customtab
 * @package Arsal\CustomTab\Model
 */
class Customtab extends AbstractModel implements IdentityInterface
{
    const TYPE_CUSTOM_DESCRIPTION = 0; // Simple tab
    const TYPE_CUSTOM_DATA = 1; // If template is needed to be rendered
    const TYPE_ATTRIBUTES = 2; // If attribute needs to be displayed.

    /**
     * @var string
     */
    protected $_cacheTag = 'customtab';

    /**
     * @return string[]
     */
    public function getIdentities()
    {
        return ['customtab_' . $this->getId() . '_' . $this->getData('identifier')];
    }

    /**
     * @inheritDoc
     */
    protected function _construct()
    {
        $this->_init(ResourceModel::class);
    }
}

Now create resource model:
Arsal/CustomTab/Model/ResourceModel/Customtab.php

<?php
namespace Arsal\CustomTab\Model\ResourceModel;

use Arsal\CustomTab\Model\ResourceModel\Customtab\Collection;

class Customtab extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{

    /**
     * @inheritDoc
     */
    protected function _construct()
    {
        $this->_init('customtab', 'entity_id');
    }
}

Now create collection model: Arsal/CustomTab/Model/ResourceModel/Customtab/Collection.php

<?php
namespace Arsal\CustomTab\Model\ResourceModel\Customtab;

use Arsal\CustomTab\Model\ResourceModel\Customtab as ResourceModel;
use Arsal\CustomTab\Model\Customtab;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;

class Collection extends AbstractCollection
{
    public function _construct()
    {
        $this->_init(Customtab::class, ResourceModel::class);
    }
}

Now its the time to modify your TabConfig model and start retrieving data from collection: Arsal/CustomTab/Model/TabConfig.php

<?php
namespace Arsal\CustomTab\Model;

use Arsal\CustomTab\Model\ResourceModel\Customtab\Collection;
use Arsal\CustomTab\Model\ResourceModel\Customtab\CollectionFactory;
use Magento\Catalog\Model\Product;
use Magento\Framework\Registry;

/**
 * Class TabConfig
 * @package Arsal\CustomTab\Model
 */
class TabConfig
{
    private $tabs = [];
    /**
     * @var null|Product
     */
    private $product = null;

    /**
     * Core registry
     *
     * @var Registry
     */
    private $coreRegistry;

    /**
     * @var ResourceModel\Customtab\CollectionFactory
     */
    private $collection;

    /**
     * TabConfig constructor.
     * @param CollectionFactory $collection
     * @param Registry $registry
     */
    public function __construct(
        CollectionFactory $collection,
        Registry $registry
    ) {
        $this->collection = $collection;
        $this->coreRegistry = $registry;
    }

    /**
     * @return array
     */
    public function getTabs()
    {
        /**
         * Return if data already fetched.
         */
        if (!empty($this->tabs)) {
            return $this->tabs;
        }

        $collection = $this->getCollection()->setOrder('sort_order', Collection::SORT_ORDER_ASC);
        if (empty($collection)) {
            return [];
        }

        /**
         * Collect Tabs
         */
        foreach ($collection as $item) {
            $this->tabs[$item->getIdentifier()] = [
                'title'         =>  $item->getTitle(),
                'description'   =>  $item->getDescription(),
                'sortOrder'     =>  (int)$item->getSortOrder()
            ];
            if ($item->getType() == Customtab::TYPE_CUSTOM_DESCRIPTION || !$item->getCustomData()) {
                continue;
            }

            if ($item->getType() == Customtab::TYPE_ATTRIBUTES) {
                if (!empty($item->getCustomData()) && $this->getProduct()) {
                    // Process Product attributes
                    $this->processAttributes($item);
                }
                continue;
            }
            // Create Template
            $this->tabs[$item->getIdentifier()]['type'] = 'template';
            $this->tabs[$item->getIdentifier()]['data'] = \Zend_Json::decode($item->getCustomData());
        }

        return $this->tabs;
    }

    /**
     * @param Customtab $item
     */
    private function processAttributes(Customtab $item)
    {
        $attributes = explode(',', $item->getCustomData());
        if (!empty($attributes)) {
            foreach ($attributes as $attributeCode) {
                if (!isset($this->getProduct()->getAttributes()[trim($attributeCode)]) || !$this->getProduct()->getCustomAttribute(trim($attributeCode))) {
                    continue;
                }

                $this->tabs[$item->getIdentifier()]['description']  .=  "<div class='attribute-information'>"
                        . "<strong>{$this->getProduct()->getAttributes()[trim($attributeCode)]->getDefaultFrontendLabel()}</strong>: "
                        . " {$this->getProduct()->getCustomAttribute(trim($attributeCode))->getValue()}"
                    . "</div>";
            }
        }
    }

    /**
     * @return Product
     */
    public function getProduct()
    {
        if (!$this->product) {
            $this->product = $this->coreRegistry->registry('product');
        }
        return $this->product;
    }

    /**
     * @return Collection
     */
    public function getCollection()
    {
        return $this->collection->create();
    }
}

As you can see from code above, the identifier is being used as tabs key will differentiate the tabs. Using condition with type to identify is very useful to know the content we need to render.

If Type value is equal to 2, the product attributes would be rendered. You have to provide product attributes as a string in custom_data column. Using product attribute information you can compare the existance of attribute.

If type column value is set to 1, custom template data can be rendered by decoding JSON data from custom_data column.

To take quick test, try inserting following record into your customtab table:

INSERT INTO `customtab` (`entity_id`, `identifier`, `title`, `description`, `type`, `custom_data`, `sort_order`, `created_at`, `updated_at`) VALUES
(1, 'Tab A', 'Tab Custom A', NULL, 1, '{\r\n   \"type\": \"Magento\\\\Framework\\\\View\\\\Element\\\\Template\",\r\n    \"name\": \"lorem.ipsum.1\",\r\n    \"template\": \"Arsal_CustomTab::template_c.phtml\"\r\n}', 0, '2020-06-19 06:29:29', '2020-06-19 06:25:34'),
(2, 'TabB', 'Reviewed Products Listing', 'Reviewed products', 1, '{\r\n        \"type\": \"Magento\\\\Reports\\\\Block\\\\Product\\\\Widget\\\\Viewed\",\r\n        \"name\": \"custom.recently.view.products\",\r\n        \"template\": \"Magento_Reports::widget/viewed/content/viewed_list.phtml\"\r\n      }', 2, '2020-06-19 06:38:00', '2020-06-19 06:32:38'),
(4, 'Lorem ipsum', 'Lorem Ipsum Custom Tab', NULL, 1, '{\r\n      \"type\": \"Magento\\\\Framework\\\\View\\\\Element\\\\Template\",\r\n      \"name\": \"lorem.ipsum\",\r\n      \"template\": \"Arsal_CustomTab::template_c.phtml\"\r\n    }', 1, '2020-06-19 06:43:13', '2020-06-19 06:42:11'),
(5, 'TabA', 'My Tab A', '<h2>Why do we use it?</h2>\r\n<p>It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using \'Content here, content here\', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for \'lorem ipsum\' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>', 1, NULL, -1, '2020-06-20 08:35:11', '2020-06-19 06:44:17'),
(6, 'custom-attributes-tab', 'Product Attributes', '<h2>What is Lorem Ipsum?</h2>\r\n<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry.</p>', 2, 'climate, color, size, weight, description', NULL, '2020-06-20 10:22:19', '2020-06-20 08:30:29');

That’s all, now you have your dynamic custom tabs populating from database. Read first part from here: Create Dynamic Custom Tabs on Product View Page

Complete module Available on Github: https://github.com/arsalanworld/custom-tab

Leave a Reply

Your email address will not be published. Required fields are marked *