Create Dynamic Custom Tabs on Product View Page

Create Dynamic Custom Tabs on Product View Page
()

Today I’m creating new post on request from one of my fellow. He asked me how can we achieve dynamic multi tabs at product view page using best practices and without interfering with the core.
So here I’m with the new post now.

Lets go a head and initialize our module:

 
<?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="Arsal_CustomTab" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Catalog" />
        </sequence>
    </module>
</config>

Register our module: (Arsal/Customtab/registration.php)

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

For demonstration purposes I’m creating only a Config model here to retrieve values quickly in the form of array, which will be further used to push to tabs.

So let go ahead and create a new Config model Arsal/CustomTab/Model/TabConfig.php:

<?php
namespace Arsal\CustomTab\Model;

class TabConfig
{
    /**
     * @var array $tabs
     */
    private $tabs = [
        'tabA' =>  [
            'title'         =>  'Custom Tab A',
            'description'   =>  'Custom Tab A is right here !',
            'sortOrder'     =>  50
        ],
        'tabB'  =>  [
            'title'         =>  'Recently Viewed',
            'type'          =>  'template',
            'data'          =>  [
                "type"      =>  "Magento\Reports\Block\Product\Widget\Viewed",
                "name"      =>  "custom.recently.view.products",
                "template"  =>  "Magento_Reports::widget/viewed/content/viewed_list.phtml"
            ],
            'description'   =>  '',
            'sortOrder'     =>  45
        ],
        'tabC'  => [
            'title'         =>  'Lorem Ipsum Tab',
            'type'          =>  'template',
            'data'          =>  [
                "type"      =>  "Magento\Framework\View\Element\Template",
                "name"      =>  "lorem.ipsum",
                "template"  =>  "Arsal_CustomTab::template_c.phtml"
            ],
            'description'   =>  '',
            'sortOrder'     =>  45
        ]
    ];

    /**
     * @return array
     */
    public function getTabs()
    {
        return $this->tabs;
    }

}

Next step is the injection of this data to block and adding these blocks will be treated as child of block product.info.details

To achieve this we need to observe the generation of layouts after event i.e `layout_generate_blocks_after`. So let’s move a head and create an event observer.

Create events.xml inside Arsal/CustomTab/etc/:

 
<?xml version="1.0"?>
<!--
/**
* @module: Arsal_CustomTab
* @author: Arsalan Ajmal
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="layout_generate_blocks_after">
        <observer name="arsal_layout_generate_blocks_after" instance="Arsal\CustomTab\Observer\NewTab" />
    </event>
</config>

Now create observer class NewTab.php inside Arsal/CustomTab/Observer:

<?php
/**
 * @author Arsalan Ajmal
 * @module Arsal_CustomTab
 */
namespace Arsal\CustomTab\Observer;

use Arsal\CustomTab\Model\TabConfig;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;

class NewTab implements ObserverInterface
{
    /**
     * @var string PARENT_BlOCK_NAME
     */
    const PARENT_BlOCK_NAME = 'product.info.details';

    /**
     * @var string RENDERING_TEMPLATE
     */
    const RENDERING_TEMPLATE = 'Arsal_CustomTab::tab_renderer.phtml';

    /**
     * @var TabConfig $tabs
     */
    private $tabs;

    /**
     * NewTab constructor.
     * @param TabConfig $tabs
     */
    public function __construct(TabConfig $tabs)
    {
        $this->tabs = $tabs;
    }

    /**
     * @param Observer $observer
     */
    public function execute(Observer $observer)
    {
        /** @var \Magento\Framework\View\Layout $layout */
        $layout = $observer->getLayout();
        $blocks = $layout->getAllBlocks();

        foreach ($blocks as $key => $block) {
            /** @var \Magento\Framework\View\Element\Template $block */
            if ($block->getNameInLayout() == self::PARENT_BlOCK_NAME) {

                foreach ($this->tabs->getTabs() as $key => $tab) {
                    $block->addChild(
                        $key,
                        \Magento\Catalog\Block\Product\View::class,
                        [
                            'template' => self::RENDERING_TEMPLATE,
                            'title'     =>  $tab['title'],
                            'jsLayout'      =>  [
                                $tab
                            ]
                        ]
                    );
                }
            }
        }
    }
}

Now create phtml rendering template: Arsal/CustomTab/view/frontend/templates/tab_renderer.phtml:

<?php
/**
 * @var \Magento\Catalog\Block\Product\View $block
 */
?>
<?php
if (!empty($block->getJsLayout())) {
    $jsLayout = \Zend_Json::decode($block->getJsLayout());
    foreach ($jsLayout as $layout) {
        if (isset($layout['type']) && 'template' === $layout['type'] && isset($layout['data'])){
            echo $this->getLayout()->createBlock($layout['data']['type'])
                ->setDisplayType($layout['data']['name'])
                ->setTemplate($layout['data']['template'])->toHtml();
        } else {
            ?>
            <h1><?= $layout['title']; ?></h1>
            <div><?= $layout['description']; ?></div>
            <?php
        }
    }
}

We are step behind rendering the blocks. To render these blocks we need to add block names to grouped child data. The best way we can do with it is to add these blocks name via interceptor (plugin) to grouped data.

First create plugin configuration: Arsal/CustomTab/etc/frontend/di.xml:

 
<?xml version="1.0"?>
<!--
/**
* @module: Arsal_CustomTab
* @author: Arsalan Ajmal
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Catalog\Block\Product\View\Details">
        <plugin name="arsal_product_view_description" type="Arsal\CustomTab\Plugin\Description" />
    </type>

</config>

Create Plugin instance class: Arsal/CustomTab/Plugin/Description.php:

<?php
namespace Arsal\CustomTab\Plugin;

use Arsal\CustomTab\Model\TabConfig;

class Description
{
    /**
     * @var TabConfig $tabs
     */
    private $tabs;

    /**
     * Description constructor.
     * @param TabConfig $tabs
     */
    public function __construct(
        TabConfig $tabs
    ) {
        $this->tabs = $tabs;
    }

    /**
     * @param \Magento\Catalog\Block\Product\View\Details $subject
     * @param array $result
     * @return array
     */
    public function afterGetGroupSortedChildNames(
        \Magento\Catalog\Block\Product\View\Details $subject,
        $result
    ) {
        if (!empty($this->tabs->getTabs())) {
            foreach ($this->tabs->getTabs() as $key => $tab) {
                $sortOrder = isset($tab['sortOrder']) ? $tab['sortOrder'] : 45;
                $result = array_merge($result, [ $sortOrder => 'product.info.details.' . $key]);
            }
        }
        return $result;
    }
}

Now create template c renderer template:

<?php
/**
 * @var \Magento\Framework\View\Element\Template $block
 */
?>
<h3>Custom Block</h3>
<div>
    <h4>What is Lorem Ipsum?</h4>
    <p>
        Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
    </p>
</div>

That’s all. Your custom dynamic tabs are there on product view page now 🙂

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

How useful was this post?

Click on a star to rate it!

Average rating / 5. Vote count:

No votes so far! Be the first to rate this post.

As you found this post useful...

Follow us on social media!

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

12 Comments

  • Great Work !!

  • in custom tab, can we create a button and can I give a link of other website.

    • Yes you can do it. All you have to do is to specify you custom html in description or may be you can create custom template and put your button right there.

  • Hi,
    wonderful article!
    but is it possible to use table collection instead of using static tab content in Tabconfig model.
    i tried but it doesn’t worked.
    Please help.
    Thank you in advance.

  • This article was helpful. But can i know how to get these custom tabs below? (I want them below details tab i.e in the next line. Not with other default tabs.)


    • Hi Asha,
      Thanks for your comment. Well in order to achieve that you have to do some customization. The code below will do that for you, but you might need further customization for this.
      Now inside your module first go to file ‘Arsal\CustomTab\etc\frontend\di.xml’ and replace with following code:

       
      <?xml version="1.0"?>
      <!--
      /**
      * @module: Arsal_CustomTab
      * @author: Arsalan Ajmal
      */
      -->
      <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
      
          <type name="Magento\Catalog\Block\Product\View\Description">
              <plugin name="arsal_product_after_description" type="Arsal\CustomTab\Plugin\DescriptionProductGetAfter" />
          </type>
      </config>
      


      Now create a Class ‘DescriptionProductGetAfter’ inside ‘PME\CustomTab\Plugin’:

      <?php
      
      namespace Arsal\CustomTab\Plugin;
      
      
      use Magento\Catalog\Api\ProductRepositoryInterface;
      use Magento\Framework\Exception\NoSuchEntityException;
      use Magento\Framework\View\LayoutInterface;
      
      class DescriptionProductGetAfter
      {
      
          private $layout;
      
          private $product;
      
          public function __construct(LayoutInterface $layout, ProductRepositoryInterface $product)
          {
              $this->layout = $layout;
              $this->product = $product;
          }
      
          /**
           * @param \Magento\Catalog\Block\Product\View\Description $description
           * @param \Magento\Catalog\Model\Product $product
           * @return \Magento\Catalog\Model\Product
           */
          public function afterGetProduct(
              \Magento\Catalog\Block\Product\View\Description $description,
              $product
          )
          {
              $id = $product->getData('entity_id');
              $defaultDescription = "";
              try {
                  $productModel = $this->product->getById($id);
                  $defaultDescription = $productModel->getDescription();
              } catch (NoSuchEntityException $e) {
              }
              $product->setData('description',
                  $this->getLayout()->createBlock("Arsal\CustomTab\Block\DescriptionTab")
                      ->setDisplayType("lorem.ipsum1")
                      -> setData('description', $defaultDescription)
                      ->setTemplate("Arsal_CustomTab::details.phtml")->toHtml());
      
              return $product;
          }
      
          protected function getLayout()
          {
              return $this->layout;
          }
      
      }
      


      Create ‘DescriptionTab.php’ inside ‘Arsal\CustomTab\Block’:

      <?php
      
      namespace Arsal\CustomTab\Block;
      
      
      use Arsal\CustomTab\Model\TabConfig;
      use Magento\Framework\View\Element\Template;
      
      class DescriptionTab extends Template
      {
          /**
           * @var TabConfig $tabConfig
           */
          private $tabConfig;
      
          /**
           * DescriptionTab constructor.
           * @param Template\Context $context
           * @param TabConfig $tabConfig
           * @param array $data
           */
          public function __construct(
              Template\Context $context,
              TabConfig $tabConfig,
              array $data = []
          )
          {
              $this->tabConfig = $tabConfig;
              parent::__construct($context, $data);
          }
      
          /**
           * @return array
           */
          public function getTabConfig()
          {
              return $this->tabConfig->getTabs();
          }
      
          /**
           * @return \Magento\Framework\View\LayoutInterface
           * @throws \Magento\Framework\Exception\LocalizedException
           */
          public function getLayout()
          {
              return parent::getLayout();
          }
      }
      


      Now create ‘details.phtml’ inside ‘PME\CustomTab\view\frontend\templates’:

      <?php
      
      // @codingStandardsIgnoreFile
      
      /**
       * @var \Arsal\CustomTab\Block\DescriptionTab $block
       */
      ?>
      <?php
          if ($this->getDescription() && !empty($this->getDescription())) {
              echo $this->getDescription();
          }
      ?>
      <?php if (!empty($block->getTabConfig())):?>
          <div class="product info detailed">
              <?php $layout = $block->getLayout(); ?>
                  <div id="inner-tabs">
                      <?php
                          $result = [];
                          foreach ($block->getTabConfig() as $key => $tab) {
                              $sortOrder = isset($tab['sortOrder']) ? $tab['sortOrder'] : 45;
                              $result = array_merge($result, [ $sortOrder => 'product.info.details.' . $key]);
                          }
                      ?>
                      <ul style="margin: 0; padding: 0;">
                          <?php foreach ($result as $name):?>
                          <?php
                              $html = $layout->renderElement($name);
                              if (!trim($html)) {
                                  continue;
                              }
                              $alias = $layout->getElementAlias($name);
                              if (isset($block->getTabConfig()[$alias]) && isset($block->getTabConfig()[$alias]['title'])) {
                                  $label = $block->getTabConfig()[$alias]['title'];
                              } else {
                                  $label = $alias;
                              }
                          ?>
                              <li class="product data items" style="list-style: none; margin: 0; display: inline-block; padding: 0 5px;">
                                  <div class="data item title active" id="tab-label-<?php /* @escapeNotVerified */ echo $alias;?>">
                                      <a  href="#<?php /* @escapeNotVerified */ echo $alias; ? rel="nofollow">"
                                          class="data switch switch-tabs"
                                          id="tab-label-<?php /* @escapeNotVerified */ echo $alias;?>-title">
                                          <?php /* @escapeNotVerified */ echo $label; ?>
                                      </a>
                                  </div>
                              </li>
      
                          <?php endforeach;?>
                          <li class="product data items">
                              <?php foreach ($result as $name):?>
                                  <?php
                                  $html = $layout->renderElement($name);
                                  if (!trim($html)) {
                                      continue;
                                  }
                                  $alias = $layout->getElementAlias($name);
                                  $label = $alias;
                                  ?>
                                  <div  id="<?php /* @escapeNotVerified */ echo $alias; ?>"  class="data item content">
                                      <?php /* @escapeNotVerified */ echo $html; ?>
                                  </div>
                              <?php endforeach; ?>
                          </li>
                      </ul>
                  </div>
      
          </div>
          <script>
              requirejs(
                  [
                      'jquery',
                      'jquery/ui'
                  ], function ($, tabs) {
                      'use strict';
      
                      $("#inner-tabs").tabs();
      
                  }
              )
          </script>
      <?php endif; ?>
      

      Tab Inside Details

Leave a Reply

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