Magento 2:  How to Change Configurable Product Description Dynamically Based on Swatches

How to Change Configurable Product Description Dynamically Based on Swatches M2

Hello Magento Friends,

In this current Magento 2 blog, I will explain How to Change Configurable Product Description Dynamically Based on Swatches.

In Magento 2, configurable products are single products with variations in color selection, sizes, etc. You can Create Configurable Product using one of the two ways:

By default, the product description remains the same for all swatches in configurable products. We can change product descriptions based on configurable product’s swatches selection. Displaying dynamic and optimized product descriptions will help enhance the customer shopping experience with your store and aid in Google rankings.

Let’s learn How to Change Configurable Product Description Dynamically Based on Swatches in Magento 2.

Steps to Change Configurable Product Description Dynamically Based on Swatches in Magento 2:

Step 1: Create a di.xml file in the given path

{{magento_root}}/app/code/Vendor/Extension/etc/frontend/.di.xml

And then add the below code

<?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\ConfigurableProduct\Block\Product\View\Type\Configurable">
        <plugin name="Vendor_Extension_product_view" type="Vendor\Extension\Plugin\ConfigurableProduct\Product\View\Type\ConfigurablePlugin" sortOrder="1" />
    </type>
    <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable">
        <plugin name="Vendor_Extension_product_type" type="Vendor\Extension\Plugin\ConfigurableProduct\Product\Type\ConfigurablePlugin" sortOrder="1" />
    </type>
</config>

Step 2: Create plugin file ConfigurablePlugin.php at the following path

{{magento_root}}/app/code/Vendor/Extension/Plugin/ConfigurableProduct/Product/Type/ConfigurablePlugin.php

Now add the below-mentioned code

<?php 
namespace Vendor\Extension\Plugin\ConfigurableProduct\Product\Type;

class ConfigurablePlugin
{
    public function afterGetUsedProductCollection(\Magento\ConfigurableProduct\Model\Product\Type\Configurable $subject, $result)
   {
       $result->addAttributeToSelect('description');
       return $result;
   }
}

Step 3: Now, create a plugin file ConfigurablePlugin.php at the given path

{{magento_root}}/app/code/Vendor/Extension/Plugin/ConfigurableProduct/Product/View/Type/ConfigurablePlugin.php

And include the below piece of code

<?php 
namespace Vendor\Extension\Plugin\ConfigurableProduct\Product\View\Type; 
class ConfigurablePlugin { 
     protected $jsonEncoder; 
     protected $jsonDecoder; 

     public function __construct( 
        \Magento\Framework\Json\DecoderInterface $jsonDecoder, 
        \Magento\Framework\Json\EncoderInterface $jsonEncoder 
    ){ 
        $this->jsonEncoder = $jsonEncoder;
        $this->jsonDecoder = $jsonDecoder;
    }

    public function afterGetJsonConfig(\Magento\ConfigurableProduct\Block\Product\View\Type\Configurable $subject, $result)
    {
        $result = $this->jsonDecoder->decode($result);
        $currentProduct = $subject->getProduct();

        if ($currentProduct->getName()) {
            $result['productName'] = $currentProduct->getName();
        }

        if ($currentProduct->getSku()) {
            $result['productSku'] = $currentProduct->getSku();
       }

       if ($currentProduct->getDescription()) {
            $result['productDescription'] = $currentProduct->getDescription();
       }
        foreach ($subject->getAllowProducts() as $product) {
            $result['names'][$product->getId()] = $product->getName();
            $result['skus'][$product->getId()] = $product->getSku();
            $result['desc'][$product->getId()] = $product->getDescription();
        }
        return $this->jsonEncoder->encode($result);
    }
}

Step 4: After that, create requirejs-config.js file at the below file path

{{magento_root}}/app/code/Vendor/Extension/view/frontend/requirejs-config.js

Now add the following code fragment

var config = {
    map: {
       '*': {
          'Magento_Swatches/js/swatch-renderer':'Vendor_Extension/js/swatch-renderer'
       }
    }
 };

Step 5: Now create swatch-renderer.js file at the below path

{{magento_root}}/app/code/Vendor/Extension/view/frontend/web/js/swatch-renderer.js

Now add the following code snippet

/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

define([
   'jquery',
   'underscore',
   'mage/template',
   'mage/smart-keyboard-handler',
   'mage/translate',
   'priceUtils',
   'jquery-ui-modules/widget',
   'jquery/jquery.parsequery',
   'mage/validation/validation'
], function ($, _, mageTemplate, keyboardHandler, $t, priceUtils) {
   'use strict';

   /**
    * Extend form validation to support swatch accessibility
    */
   $.widget('mage.validation', $.mage.validation, {
       /**
        * Handle form with swatches validation. Focus on first invalid swatch block.
        *
        * @param {jQuery.Event} event
        * @param {Object} validation
        */
       listenFormValidateHandler: function (event, validation) {
           var swatchWrapper, firstActive, swatches, swatch, successList, errorList, firstSwatch;

           this._superApply(arguments);

           swatchWrapper = '.swatch-attribute-options';
           swatches = $(event.target).find(swatchWrapper);

           if (!swatches.length) {
               return;
           }

           swatch = '.swatch-attribute';
           firstActive = $(validation.errorList[0].element || []);
           successList = validation.successList;
           errorList = validation.errorList;
           firstSwatch = $(firstActive).parent(swatch).find(swatchWrapper);

           keyboardHandler.focus(swatches);

           $.each(successList, function (index, item) {
               $(item).parent(swatch).find(swatchWrapper).attr('aria-invalid', false);
           });

           $.each(errorList, function (index, item) {
               $(item.element).parent(swatch).find(swatchWrapper).attr('aria-invalid', true);
           });

           if (firstSwatch.length) {
               $(firstSwatch).trigger('focus');
           }
       }
   });

   /**
    * Render tooltips by attributes (only to up).
    * Required element attributes:
    *  - data-option-type (integer, 0-3)
    *  - data-option-label (string)
    *  - data-option-tooltip-thumb
    *  - data-option-tooltip-value
    *  - data-thumb-width
    *  - data-thumb-height
    */
   $.widget('mage.SwatchRendererTooltip', {
       options: {
           delay: 200,                             //how much ms before tooltip to show
           tooltipClass: 'swatch-option-tooltip'  //configurable, but remember about css
       },

       /**
        * @private
        */
       _init: function () {
           var $widget = this,
               $this = this.element,
               $element = $('.' + $widget.options.tooltipClass),
               timer,
               type = parseInt($this.data('option-type'), 10),
               label = $this.data('option-label'),
               thumb = $this.data('option-tooltip-thumb'),
               value = $this.data('option-tooltip-value'),
               width = $this.data('thumb-width'),
               height = $this.data('thumb-height'),
               $image,
               $title,
               $corner;

           if (!$element.length) {
               $element = $('<div class="' +
                   $widget.options.tooltipClass +
                   '"><div class="image"></div><div class="title"></div><div class="corner"></div></div>'
               );
               $('body').append($element);
           }

           $image = $element.find('.image');
           $title = $element.find('.title');
           $corner = $element.find('.corner');

           $this.on('mouseenter', function () {
               if (!$this.hasClass('disabled')) {
                   timer = setTimeout(
                       function () {
                           var leftOpt = null,
                               leftCorner = 0,
                               left,
                               $window;

                           if (type === 2) {
                               // Image
                               $image.css({
                                   'background': 'url("' + thumb + '") no-repeat center', //Background case
                                   'background-size': 'initial',
                                   'width': width + 'px',
                                   'height': height + 'px'
                               });
                               $image.show();
                           } else if (type === 1) {
                               // Color
                               $image.css({
                                   background: value
                               });
                               $image.show();
                           } else if (type === 0 || type === 3) {
                               // Default
                               $image.hide();
                           }

                           $title.text(label);

                           leftOpt = $this.offset().left;
                           left = leftOpt + $this.width() / 2 - $element.width() / 2;
                           $window = $(window);

                           // the numbers (5 and 5) is magick constants for offset from left or right page
                           if (left < 0) {
                               left = 5;
                           } else if (left + $element.width() > $window.width()) {
                               left = $window.width() - $element.width() - 5;
                           }

                           // the numbers (6,  3 and 18) is magick constants for offset tooltip
                           leftCorner = 0;

                           if ($element.width() < $this.width()) {
                               leftCorner = $element.width() / 2 - 3;
                           } else {
                               leftCorner = (leftOpt > left ? leftOpt - left : left - leftOpt) + $this.width() / 2 - 6;
                           }

                           $corner.css({
                               left: leftCorner
                           });
                           $element.css({
                               left: left,
                               top: $this.offset().top - $element.height() - $corner.height() - 18
                           }).show();
                       },
                       $widget.options.delay
                   );
               }
           });

           $this.on('mouseleave', function () {
               $element.hide();
               clearTimeout(timer);
           });

           $(document).on('tap', function () {
               $element.hide();
               clearTimeout(timer);
           });

           $this.on('tap', function (event) {
               event.stopPropagation();
           });
       }
   });

   /**
    * Render swatch controls with options and use tooltips.
    * Required two json:
    *  - jsonConfig (magento's option config)
    *  - jsonSwatchConfig (swatch's option config)
    *
    *  Tuning:
    *  - numberToShow (show "more" button if options are more)
    *  - onlySwatches (hide selectboxes)
    *  - moreButtonText (text for "more" button)
    *  - selectorProduct (selector for product container)
    *  - selectorProductPrice (selector for change price)
    */
   $.widget('mage.SwatchRenderer', {
       options: {
           classes: {
               attributeClass: 'swatch-attribute',
               attributeLabelClass: 'swatch-attribute-label',
               attributeSelectedOptionLabelClass: 'swatch-attribute-selected-option',
               attributeOptionsWrapper: 'swatch-attribute-options',
               attributeInput: 'swatch-input',
               optionClass: 'swatch-option',
               selectClass: 'swatch-select',
               moreButton: 'swatch-more',
               loader: 'swatch-option-loading'
           },
           // option's json config
           jsonConfig: {},

           // swatch's json config
           jsonSwatchConfig: {},

           // selector of parental block of prices and swatches (need to know where to seek for price block)
           selectorProduct: '.product-info-main',

           // selector of price wrapper (need to know where set price)
           selectorProductPrice: '[data-role=priceBox]',

           //selector of product images gallery wrapper
           mediaGallerySelector: '[data-gallery-role=gallery-placeholder]',

           // selector of category product tile wrapper
           selectorProductTile: '.product-item',

           // number of controls to show (false or zero = show all)
           numberToShow: false,

           // show only swatch controls
           onlySwatches: false,

           // enable label for control
           enableControlLabel: true,

           // control label id
           controlLabelId: '',

           // text for more button
           moreButtonText: $t('More'),

           // Callback url for media
           mediaCallback: '',

           // Local media cache
           mediaCache: {},

           // Cache for BaseProduct images. Needed when option unset
           mediaGalleryInitial: [{}],

           // Use ajax to get image data
           useAjax: false,

           /**
            * Defines the mechanism of how images of a gallery should be
            * updated when user switches between configurations of a product.
            *
            * As for now value of this option can be either 'replace' or 'prepend'.
            *
            * @type {String}
            */
           gallerySwitchStrategy: 'replace',

           // whether swatches are rendered in product list or on product page
           inProductList: false,

           // sly-old-price block selector
           slyOldPriceSelector: '.sly-old-price',

           // tier prise selectors start
           tierPriceTemplateSelector: '#tier-prices-template',
           tierPriceBlockSelector: '[data-role="tier-price-block"]',
           tierPriceTemplate: '',
           // tier prise selectors end

           // A price label selector
           normalPriceLabelSelector: '.product-info-main .normal-price .price-label',
           qtyInfo: '#qty'
       },

       /**
        * Get chosen product
        *
        * @returns int|null
        */
       getProduct: function () {
           var products = this._CalcProducts();

           return _.isArray(products) ? products[0] : null;
       },

       /**
        * Get chosen product id
        *
        * @returns int|null
        */
       getProductId: function () {
           var products = this._CalcProducts();

           return _.isArray(products) && products.length === 1 ? products[0] : null;
       },

       /**
        * @private
        */
       _init: function () {
           // Don't render the same set of swatches twice
           if ($(this.element).attr('data-rendered')) {
               return;
           }

           $(this.element).attr('data-rendered', true);

           if (_.isEmpty(this.options.jsonConfig.images)) {
               this.options.useAjax = true;
               // creates debounced variant of _LoadProductMedia()
               // to use it in events handlers instead of _LoadProductMedia()
               this._debouncedLoadProductMedia = _.debounce(this._LoadProductMedia.bind(this), 500);
           }

           this.options.tierPriceTemplate = $(this.options.tierPriceTemplateSelector).html();

           if (this.options.jsonConfig !== '' && this.options.jsonSwatchConfig !== '') {
               // store unsorted attributes
               this.options.jsonConfig.mappedAttributes = _.clone(this.options.jsonConfig.attributes);
               this._sortAttributes();
               this._RenderControls();
               this._setPreSelectedGallery();
               $(this.element).trigger('swatch.initialized');
           } else {
               console.log('SwatchRenderer: No input data received');
           }
       },

       /**
        * @private
        */
       _sortAttributes: function () {
           this.options.jsonConfig.attributes = _.sortBy(this.options.jsonConfig.attributes, function (attribute) {
               return parseInt(attribute.position, 10);
           });
       },

       /**
        * @private
        */
       _create: function () {
           var options = this.options,
               gallery = $('[data-gallery-role=gallery-placeholder]', '.column.main'),
               productData = this._determineProductData(),
               $main = productData.isInProductView ?
                   this.element.parents('.column.main') :
                   this.element.parents('.product-item-info');

           if (productData.isInProductView) {
               gallery.data('gallery') ?
                   this._onGalleryLoaded(gallery) :
                   gallery.on('gallery:loaded', this._onGalleryLoaded.bind(this, gallery));
           } else {
               options.mediaGalleryInitial = [{
                   'img': $main.find('.product-image-photo').attr('src')
               }];
           }

           this.productForm = this.element.parents(this.options.selectorProductTile).find('form:first');
           this.inProductList = this.productForm.length > 0;
           $(this.options.qtyInfo).on('input', this._onQtyChanged.bind(this));
       },

       /**
        * Determine product id and related data
        *
        * @returns {{productId: *, isInProductView: bool}}
        * @private
        */
       _determineProductData: function () {
           // Check if product is in a list of products.
           var productId,
               isInProductView = false;

           productId = this.element.parents('.product-item-details')
                   .find('.price-box.price-final_price').attr('data-product-id');

           if (!productId) {
               // Check individual product.
               productId = $('[name=product]').val();
               isInProductView = productId > 0;
           }

           return {
               productId: productId,
               isInProductView: isInProductView
           };
       },

       /**
        * Render controls
        *
        * @private
        */
       _RenderControls: function () {
           var $widget = this,
               container = this.element,
               classes = this.options.classes,
               chooseText = this.options.jsonConfig.chooseText,
               showTooltip = this.options.showTooltip;

           $widget.optionsMap = {};

           $.each(this.options.jsonConfig.attributes, function () {
               var item = this,
                   controlLabelId = 'option-label-' + item.code + '-' + item.id,
                   options = $widget._RenderSwatchOptions(item, controlLabelId),
                   select = $widget._RenderSwatchSelect(item, chooseText),
                   input = $widget._RenderFormInput(item),
                   listLabel = '',
                   label = '';

               // Show only swatch controls
               if ($widget.options.onlySwatches && !$widget.options.jsonSwatchConfig.hasOwnProperty(item.id)) {
                   return;
               }

               if ($widget.options.enableControlLabel) {
                   label +=
                       '<span id="' + controlLabelId + '" class="' + classes.attributeLabelClass + '">' +
                       $('<i></i>').text(item.label).html() +
                       '</span>' +
                       '<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>';
               }

               if ($widget.inProductList) {
                   $widget.productForm.append(input);
                   input = '';
                   listLabel = 'aria-label="' + $('<i></i>').text(item.label).html() + '"';
               } else {
                   listLabel = 'aria-labelledby="' + controlLabelId + '"';
               }

               // Create new control
               container.append(
                   '<div class="' + classes.attributeClass + ' ' + item.code + '" ' +
                        'data-attribute-code="' + item.code + '" ' +
                        'data-attribute-id="' + item.id + '">' +
                       label +
                       '<div aria-activedescendant="" ' +
                            'tabindex="0" ' +
                            'aria-invalid="false" ' +
                            'aria-required="true" ' +
                            'role="listbox" ' + listLabel +
                            'class="' + classes.attributeOptionsWrapper + ' clearfix">' +
                           options + select +
                       '</div>' + input +
                   '</div>'
               );

               $widget.optionsMap[item.id] = {};

               // Aggregate options array to hash (key => value)
               $.each(item.options, function () {
                   if (this.products.length > 0) {
                       $widget.optionsMap[item.id][this.id] = {
                           price: parseInt(
                               $widget.options.jsonConfig.optionPrices[this.products[0]].finalPrice.amount,
                               10
                           ),
                           products: this.products
                       };
                   }
               });
           });

           if (showTooltip === 1) {
               // Connect Tooltip
               container
                   .find('[data-option-type="1"], [data-option-type="2"],' +
                       ' [data-option-type="0"], [data-option-type="3"]')
                   .SwatchRendererTooltip();
           }

           // Hide all elements below more button
           $('.' + classes.moreButton).nextAll().hide();

           // Handle events like click or change
           $widget._EventListener();

           // Rewind options
           $widget._Rewind(container);

           //Emulate click on all swatches from Request
           $widget._EmulateSelected($.parseQuery());
           $widget._EmulateSelected($widget._getSelectedAttributes());
       },

       /**
        * Render swatch options by part of config
        *
        * @param {Object} config
        * @param {String} controlId
        * @returns {String}
        * @private
        */
       _RenderSwatchOptions: function (config, controlId) {
           var optionConfig = this.options.jsonSwatchConfig[config.id],
               optionClass = this.options.classes.optionClass,
               sizeConfig = this.options.jsonSwatchImageSizeConfig,
               moreLimit = parseInt(this.options.numberToShow, 10),
               moreClass = this.options.classes.moreButton,
               moreText = this.options.moreButtonText,
               countAttributes = 0,
               html = '';

           if (!this.options.jsonSwatchConfig.hasOwnProperty(config.id)) {
               return '';
           }

           $.each(config.options, function (index) {
               var id,
                   type,
                   value,
                   thumb,
                   label,
                   width,
                   height,
                   attr,
                   swatchImageWidth,
                   swatchImageHeight;

               if (!optionConfig.hasOwnProperty(this.id)) {
                   return '';
               }

               // Add more button
               if (moreLimit === countAttributes++) {
                   html += '<a href="#" class="' + moreClass + '"><span>' + moreText + '</span></a>';
               }

               id = this.id;
               type = parseInt(optionConfig[id].type, 10);
               value = optionConfig[id].hasOwnProperty('value') ?
                   $('<i></i>').text(optionConfig[id].value).html() : '';
               thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : '';
               width = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.width : 110;
               height = _.has(sizeConfig, 'swatchThumb') ? sizeConfig.swatchThumb.height : 90;
               label = this.label ? $('<i></i>').text(this.label).html() : '';
               attr =
                   ' id="' + controlId + '-item-' + id + '"' +
                   ' index="' + index + '"' +
                   ' aria-checked="false"' +
                   ' aria-describedby="' + controlId + '"' +
                   ' tabindex="0"' +
                   ' data-option-type="' + type + '"' +
                   ' data-option-id="' + id + '"' +
                   ' data-option-label="' + label + '"' +
                   ' aria-label="' + label + '"' +
                   ' role="option"' +
                   ' data-thumb-width="' + width + '"' +
                   ' data-thumb-height="' + height + '"';

               attr += thumb !== '' ? ' data-option-tooltip-thumb="' + thumb + '"' : '';
               attr += value !== '' ? ' data-option-tooltip-value="' + value + '"' : '';

               swatchImageWidth = _.has(sizeConfig, 'swatchImage') ? sizeConfig.swatchImage.width : 30;
               swatchImageHeight = _.has(sizeConfig, 'swatchImage') ? sizeConfig.swatchImage.height : 20;

               if (!this.hasOwnProperty('products') || this.products.length <= 0) {
                   attr += ' data-option-empty="true"';
               }

               if (type === 0) {
                   // Text
                   html += '<div class="' + optionClass + ' text" ' + attr + '>' + (value ? value : label) +
                       '</div>';
               } else if (type === 1) {
                   // Color
                   html += '<div class="' + optionClass + ' color" ' + attr +
                       ' style="background: ' + value +
                       ' no-repeat center; background-size: initial;">' + '' +
                       '</div>';
               } else if (type === 2) {
                   // Image
                   html += '<div class="' + optionClass + ' image" ' + attr +
                       ' style="background: url(' + value + ') no-repeat center; background-size: initial;width:' +
                       swatchImageWidth + 'px; height:' + swatchImageHeight + 'px">' + '' +
                       '</div>';
               } else if (type === 3) {
                   // Clear
                   html += '<div class="' + optionClass + '" ' + attr + '></div>';
               } else {
                   // Default
                   html += '<div class="' + optionClass + '" ' + attr + '>' + label + '</div>';
               }
           });

           return html;
       },

       /**
        * Render select by part of config
        *
        * @param {Object} config
        * @param {String} chooseText
        * @returns {String}
        * @private
        */
       _RenderSwatchSelect: function (config, chooseText) {
           var html;

           if (this.options.jsonSwatchConfig.hasOwnProperty(config.id)) {
               return '';
           }

           html =
               '<select class="' + this.options.classes.selectClass + ' ' + config.code + '">' +
               '<option value="0" data-option-id="0">' + chooseText + '</option>';

           $.each(config.options, function () {
               var label = this.label,
                   attr = ' value="' + this.id + '" data-option-id="' + this.id + '"';

               if (!this.hasOwnProperty('products') || this.products.length <= 0) {
                   attr += ' data-option-empty="true"';
               }

               html += '<option ' + attr + '>' + label + '</option>';
           });

           html += '</select>';

           return html;
       },

       /**
        * Input for submit form.
        * This control shouldn't have "type=hidden", "display: none" for validation work :(
        *
        * @param {Object} config
        * @private
        */
       _RenderFormInput: function (config) {
           return '<input class="' + this.options.classes.attributeInput + ' super-attribute-select" ' +
               'name="super_attribute[' + config.id + ']" ' +
               'type="text" ' +
               'value="" ' +
               'data-selector="super_attribute[' + config.id + ']" ' +
               'data-validate="{required: true}" ' +
               'aria-required="true" ' +
               'aria-invalid="false">';
       },

       /**
        * Event listener
        *
        * @private
        */
       _EventListener: function () {
           var $widget = this,
               options = this.options.classes,
               target;
           $widget.element.on('click', '.' + options.optionClass, function () {
               return $widget._OnClick($(this), $widget);
           });

           $widget.element.on('change', '.' + options.selectClass, function () {
               return $widget._OnChange($(this), $widget);
           });

           $widget.element.on('click', '.' + options.moreButton, function (e) {
               e.preventDefault();

               return $widget._OnMoreClick($(this));
           });

           $widget.element.on('keydown', function (e) {
               if (e.which === 13) {
                   target = $(e.target);

                   if (target.is('.' + options.optionClass)) {
                       return $widget._OnClick(target, $widget);
                   } else if (target.is('.' + options.selectClass)) {
                       return $widget._OnChange(target, $widget);
                   } else if (target.is('.' + options.moreButton)) {
                       e.preventDefault();

                       return $widget._OnMoreClick(target);
                   }
               }
           });
       },

       /**
        * Load media gallery using ajax or json config.
        *
        * @private
        */
       _loadMedia: function () {
           var $main = this.inProductList ?
                   this.element.parents('.product-item-info') :
                   this.element.parents('.column.main'),
               images;

           if (this.options.useAjax) {
               this._debouncedLoadProductMedia();
           }  else {
               images = this.options.jsonConfig.images[this.getProduct()];

               if (!images) {
                   images = this.options.mediaGalleryInitial;
               }
               this.updateBaseImage(this._sortImages(images), $main, !this.inProductList);
           }
       },

       /**
        * Sorting images array
        *
        * @private
        */
       _sortImages: function (images) {
           return _.sortBy(images, function (image) {
               return parseInt(image.position, 10);
           });
       },

       /**
        * Event for swatch options
        *
        * @param {Object} $this
        * @param {Object} $widget
        * @private
        */
       _OnClick: function ($this, $widget) {
           var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
               $wrapper = $this.parents('.' + $widget.options.classes.attributeOptionsWrapper),
               $label = $parent.find('.' + $widget.options.classes.attributeSelectedOptionLabelClass),
               attributeId = $parent.data('attribute-id'),
               $input = $parent.find('.' + $widget.options.classes.attributeInput),
               checkAdditionalData = JSON.parse(this.options.jsonSwatchConfig[attributeId]['additional_data']),
               $priceBox = $widget.element.parents($widget.options.selectorProduct)
                   .find(this.options.selectorProductPrice);

           if ($widget.inProductList) {
               $input = $widget.productForm.find(
                   '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]'
               );
           }

           if ($this.hasClass('disabled')) {
               return;
           }

           if ($this.hasClass('selected')) {
               $parent.removeAttr('data-option-selected').find('.selected').removeClass('selected');
               $input.val('');
               $label.text('');
               $this.attr('aria-checked', false);
           } else {
               $parent.attr('data-option-selected', $this.data('option-id')).find('.selected').removeClass('selected');
               $label.text($this.data('option-label'));
               $input.val($this.data('option-id'));
               $input.attr('data-attr-name', this._getAttributeCodeById(attributeId));
               $this.addClass('selected');
               $widget._toggleCheckedAttributes($this, $wrapper);
           }

           //    custom code start
           var productDescription = this.options.jsonConfig.desc[this.getProduct()];
           if (productDescription) {
              $('#description .description .value').html(productDescription);
            }
            //   custom code end
            
           $widget._Rebuild();

           if ($priceBox.is(':data(mage-priceBox)')) {
               $widget._UpdatePrice();
           }

           $(document).trigger('updateMsrpPriceBlock',
               [
                   this._getSelectedOptionPriceIndex(),
                   $widget.options.jsonConfig.optionPrices,
                   $priceBox
               ]);

           if (parseInt(checkAdditionalData['update_product_preview_image'], 10) === 1) {
               $widget._loadMedia();
           }

           $input.trigger('change');
       },

       /**
        * Get selected option price index
        *
        * @return {String|undefined}
        * @private
        */
       _getSelectedOptionPriceIndex: function () {
           var allowedProduct = this._getAllowedProductWithMinPrice(this._CalcProducts());

           if (_.isEmpty(allowedProduct)) {
               return undefined;
           }

           return allowedProduct;
       },

       /**
        * Get human readable attribute code (eg. size, color) by it ID from configuration
        *
        * @param {Number} attributeId
        * @returns {*}
        * @private
        */
       _getAttributeCodeById: function (attributeId) {
           var attribute = this.options.jsonConfig.mappedAttributes[attributeId];

           return attribute ? attribute.code : attributeId;
       },

       /**
        * Toggle accessibility attributes
        *
        * @param {Object} $this
        * @param {Object} $wrapper
        * @private
        */
       _toggleCheckedAttributes: function ($this, $wrapper) {
           $wrapper.attr('aria-activedescendant', $this.attr('id'))
                   .find('.' + this.options.classes.optionClass).attr('aria-checked', false);
           $this.attr('aria-checked', true);
       },

       /**
        * Event for select
        *
        * @param {Object} $this
        * @param {Object} $widget
        * @private
        */
       _OnChange: function ($this, $widget) {
           var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
               attributeId = $parent.data('attribute-id'),
               $input = $parent.find('.' + $widget.options.classes.attributeInput);

           if ($widget.productForm.length > 0) {
               $input = $widget.productForm.find(
                   '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]'
               );
           }

           if ($this.val() > 0) {
               $parent.attr('data-option-selected', $this.val());
               $input.val($this.val());
           } else {
               $parent.removeAttr('data-option-selected');
               $input.val('');
           }

           $widget._Rebuild();
           $widget._UpdatePrice();
           $widget._loadMedia();
           $input.trigger('change');
       },

       /**
        * Event for more switcher
        *
        * @param {Object} $this
        * @private
        */
       _OnMoreClick: function ($this) {
           $this.nextAll().show();
           $this.trigger('blur').remove();
       },

       /**
        * Rewind options for controls
        *
        * @private
        */
       _Rewind: function (controls) {
           controls.find('div[data-option-id], option[data-option-id]')
               .removeClass('disabled')
               .prop('disabled', false);
           controls.find('div[data-option-empty], option[data-option-empty]')
               .attr('disabled', true)
               .addClass('disabled')
               .attr('tabindex', '-1');
       },

       /**
        * Rebuild container
        *
        * @private
        */
       _Rebuild: function () {
           var $widget = this,
               controls = $widget.element.find('.' + $widget.options.classes.attributeClass + '[data-attribute-id]'),
               selected = controls.filter('[data-option-selected]');

           // Enable all options
           $widget._Rewind(controls);

           // done if nothing selected
           if (selected.length <= 0) {
               return;
           }

           // Disable not available options
           controls.each(function () {
               var $this = $(this),
                   id = $this.data('attribute-id'),
                   products = $widget._CalcProducts(id);

               if (selected.length === 1 && selected.first().data('attribute-id') === id) {
                   return;
               }

               $this.find('[data-option-id]').each(function () {
                   var $element = $(this),
                       option = $element.data('option-id');

                   if (!$widget.optionsMap.hasOwnProperty(id) || !$widget.optionsMap[id].hasOwnProperty(option) ||
                       $element.hasClass('selected') ||
                       $element.is(':selected')) {
                       return;
                   }

                   if (_.intersection(products, $widget.optionsMap[id][option].products).length <= 0) {
                       $element.attr('disabled', true).addClass('disabled');
                   }
               });
           });
       },

       /**
        * Get selected product list
        *
        * @returns {Array}
        * @private
        */
       _CalcProducts: function ($skipAttributeId) {
           var $widget = this,
               selectedOptions = '.' + $widget.options.classes.attributeClass + '[data-option-selected]',
               products = [];

           // Generate intersection of products
           $widget.element.find(selectedOptions).each(function () {
               var id = $(this).data('attribute-id'),
                   option = $(this).attr('data-option-selected');

               if ($skipAttributeId !== undefined && $skipAttributeId === id) {
                   return;
               }

               if (!$widget.optionsMap.hasOwnProperty(id) || !$widget.optionsMap[id].hasOwnProperty(option)) {
                   return;
               }

               if (products.length === 0) {
                   products = $widget.optionsMap[id][option].products;
               } else {
                   products = _.intersection(products, $widget.optionsMap[id][option].products);
               }
           });

           return products;
       },

       /**
        * Update total price
        *
        * @private
        */
       _UpdatePrice: function () {
           var $widget = this,
               $product = $widget.element.parents($widget.options.selectorProduct),
               $productPrice = $product.find(this.options.selectorProductPrice),
               result = $widget._getNewPrices(),
               tierPriceHtml,
               isShow;

           $productPrice.trigger(
               'updatePrice',
               {
                   'prices': $widget._getPrices(result, $productPrice.priceBox('option').prices)
               }
           );

           isShow = typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount;

           $productPrice.find('span:first').toggleClass('special-price', isShow);

           $product.find(this.options.slyOldPriceSelector)[isShow ? 'show' : 'hide']();

           if (typeof result != 'undefined' && result.tierPrices && result.tierPrices.length) {
               if (this.options.tierPriceTemplate) {
                   tierPriceHtml = mageTemplate(
                       this.options.tierPriceTemplate,
                       {
                           'tierPrices': result.tierPrices,
                           '$t': $t,
                           'currencyFormat': this.options.jsonConfig.currencyFormat,
                           'priceUtils': priceUtils
                       }
                   );
                   $(this.options.tierPriceBlockSelector).html(tierPriceHtml).show();
               }
           } else {
               $(this.options.tierPriceBlockSelector).hide();
           }

           $(this.options.normalPriceLabelSelector).hide();

           _.each($('.' + this.options.classes.attributeOptionsWrapper), function (attribute) {
               if ($(attribute).find('.' + this.options.classes.optionClass + '.selected').length === 0) {
                   if ($(attribute).find('.' + this.options.classes.selectClass).length > 0) {
                       _.each($(attribute).find('.' + this.options.classes.selectClass), function (dropdown) {
                           if ($(dropdown).val() === '0') {
                               $(this.options.normalPriceLabelSelector).show();
                           }
                       }.bind(this));
                   } else {
                       $(this.options.normalPriceLabelSelector).show();
                   }
               }
           }.bind(this));
       },

       /**
        * Get new prices for selected options
        *
        * @returns {*}
        * @private
        */
       _getNewPrices: function () {
           var $widget = this,
               newPrices = $widget.options.jsonConfig.prices,
               allowedProduct = this._getAllowedProductWithMinPrice(this._CalcProducts());

           if (!_.isEmpty(allowedProduct)) {
               newPrices = this.options.jsonConfig.optionPrices[allowedProduct];
           }

           return newPrices;
       },

       /**
        * Get prices
        *
        * @param {Object} newPrices
        * @param {Object} displayPrices
        * @returns {*}
        * @private
        */
       _getPrices: function (newPrices, displayPrices) {
           var $widget = this;

           if (_.isEmpty(newPrices)) {
               newPrices = $widget._getNewPrices();
           }
           _.each(displayPrices, function (price, code) {

               if (newPrices[code]) {
                   displayPrices[code].amount = newPrices[code].amount - displayPrices[code].amount;
               }
           });

           return displayPrices;
       },

       /**
        * Get product with minimum price from selected options.
        *
        * @param {Array} allowedProducts
        * @returns {String}
        * @private
        */
       _getAllowedProductWithMinPrice: function (allowedProducts) {
           var optionPrices = this.options.jsonConfig.optionPrices,
               product = {},
               optionFinalPrice, optionMinPrice;

           _.each(allowedProducts, function (allowedProduct) {
               optionFinalPrice = parseFloat(optionPrices[allowedProduct].finalPrice.amount);

               if (_.isEmpty(product) || optionFinalPrice < optionMinPrice) {
                   optionMinPrice = optionFinalPrice;
                   product = allowedProduct;
               }
           }, this);

           return product;
       },

       /**
        * Gets all product media and change current to the needed one
        *
        * @private
        */
       _LoadProductMedia: function () {
           var $widget = this,
               $this = $widget.element,
               productData = this._determineProductData(),
               mediaCallData,
               mediaCacheKey,

               /**
                * Processes product media data
                *
                * @param {Object} data
                * @returns void
                */
               mediaSuccessCallback = function (data) {
                   if (!(mediaCacheKey in $widget.options.mediaCache)) {
                       $widget.options.mediaCache[mediaCacheKey] = data;
                   }
                   $widget._ProductMediaCallback($this, data, productData.isInProductView);
                   setTimeout(function () {
                       $widget._DisableProductMediaLoader($this);
                   }, 300);
               };

           if (!$widget.options.mediaCallback) {
               return;
           }

           mediaCallData = {
               'product_id': this.getProduct()
           };

           mediaCacheKey = JSON.stringify(mediaCallData);

           if (mediaCacheKey in $widget.options.mediaCache) {
               $widget._XhrKiller();
               $widget._EnableProductMediaLoader($this);
               mediaSuccessCallback($widget.options.mediaCache[mediaCacheKey]);
           } else {
               mediaCallData.isAjax = true;
               $widget._XhrKiller();
               $widget._EnableProductMediaLoader($this);
               $widget.xhr = $.ajax({
                   url: $widget.options.mediaCallback,
                   cache: true,
                   type: 'GET',
                   dataType: 'json',
                   data: mediaCallData,
                   success: mediaSuccessCallback
               }).done(function () {
                   $widget._XhrKiller();
               });
           }
       },

       /**
        * Enable loader
        *
        * @param {Object} $this
        * @private
        */
       _EnableProductMediaLoader: function ($this) {
           var $widget = this;

           if ($('body.catalog-product-view').length > 0) {
               $this.parents('.column.main').find('.photo.image')
                   .addClass($widget.options.classes.loader);
           } else {
               //Category View
               $this.parents('.product-item-info').find('.product-image-photo')
                   .addClass($widget.options.classes.loader);
           }
       },

       /**
        * Disable loader
        *
        * @param {Object} $this
        * @private
        */
       _DisableProductMediaLoader: function ($this) {
           var $widget = this;

           if ($('body.catalog-product-view').length > 0) {
               $this.parents('.column.main').find('.photo.image')
                   .removeClass($widget.options.classes.loader);
           } else {
               //Category View
               $this.parents('.product-item-info').find('.product-image-photo')
                   .removeClass($widget.options.classes.loader);
           }
       },

       /**
        * Callback for product media
        *
        * @param {Object} $this
        * @param {String} response
        * @param {Boolean} isInProductView
        * @private
        */
       _ProductMediaCallback: function ($this, response, isInProductView) {
           var $main = isInProductView ? $this.parents('.column.main') : $this.parents('.product-item-info'),
               $widget = this,
               images = [],

               /**
                * Check whether object supported or not
                *
                * @param {Object} e
                * @returns {*|Boolean}
                */
               support = function (e) {
                   return e.hasOwnProperty('large') && e.hasOwnProperty('medium') && e.hasOwnProperty('small');
               };

           if (_.size($widget) < 1 || !support(response)) {
               this.updateBaseImage(this.options.mediaGalleryInitial, $main, isInProductView);

               return;
           }

           images.push({
               full: response.large,
               img: response.medium,
               thumb: response.small,
               isMain: true
           });

           if (response.hasOwnProperty('gallery')) {
               $.each(response.gallery, function () {
                   if (!support(this) || response.large === this.large) {
                       return;
                   }
                   images.push({
                       full: this.large,
                       img: this.medium,
                       thumb: this.small
                   });
               });
           }

           this.updateBaseImage(images, $main, isInProductView);
       },

       /**
        * Check if images to update are initial and set their type
        * @param {Array} images
        */
       _setImageType: function (images) {

           images.map(function (img) {
               if (!img.type) {
                   img.type = 'image';
               }
           });

           return images;
       },

       /**
        * Update [gallery-placeholder] or [product-image-photo]
        * @param {Array} images
        * @param {jQuery} context
        * @param {Boolean} isInProductView
        */
       updateBaseImage: function (images, context, isInProductView) {
           var justAnImage = images[0],
               initialImages = this.options.mediaGalleryInitial,
               imagesToUpdate,
               gallery = context.find(this.options.mediaGallerySelector).data('gallery'),
               isInitial;

           if (isInProductView) {
               if (_.isUndefined(gallery)) {
                   context.find(this.options.mediaGallerySelector).on('gallery:loaded', function () {
                       this.updateBaseImage(images, context, isInProductView);
                   }.bind(this));

                   return;
               }

               imagesToUpdate = images.length ? this._setImageType($.extend(true, [], images)) : [];
               isInitial = _.isEqual(imagesToUpdate, initialImages);

               if (this.options.gallerySwitchStrategy === 'prepend' && !isInitial) {
                   imagesToUpdate = imagesToUpdate.concat(initialImages);
               }

               imagesToUpdate = this._setImageIndex(imagesToUpdate);

               gallery.updateData(imagesToUpdate);
               this._addFotoramaVideoEvents(isInitial);
           } else if (justAnImage && justAnImage.img) {
               context.find('.product-image-photo').attr('src', justAnImage.img);
           }
       },

       /**
        * Add video events
        *
        * @param {Boolean} isInitial
        * @private
        */
       _addFotoramaVideoEvents: function (isInitial) {
           if (_.isUndefined($.mage.AddFotoramaVideoEvents)) {
               return;
           }

           if (isInitial) {
               $(this.options.mediaGallerySelector).AddFotoramaVideoEvents();

               return;
           }

           $(this.options.mediaGallerySelector).AddFotoramaVideoEvents({
               selectedOption: this.getProduct(),
               dataMergeStrategy: this.options.gallerySwitchStrategy
           });
       },

       /**
        * Set correct indexes for image set.
        *
        * @param {Array} images
        * @private
        */
       _setImageIndex: function (images) {
           var length = images.length,
               i;

           for (i = 0; length > i; i++) {
               images[i].i = i + 1;
           }

           return images;
       },

       /**
        * Kill doubled AJAX requests
        *
        * @private
        */
       _XhrKiller: function () {
           var $widget = this;

           if ($widget.xhr !== undefined && $widget.xhr !== null) {
               $widget.xhr.abort();
               $widget.xhr = null;
           }
       },

       /**
        * Emulate mouse click on all swatches that should be selected
        * @param {Object} [selectedAttributes]
        * @private
        */
       _EmulateSelected: function (selectedAttributes) {
           $.each(selectedAttributes, $.proxy(function (attributeCode, optionId) {
               var elem = this.element.find('.' + this.options.classes.attributeClass +
                   '[data-attribute-code="' + attributeCode + '"] [data-option-id="' + optionId + '"]'),
                   parentInput = elem.parent();

               if (elem.hasClass('selected')) {
                   return;
               }

               if (parentInput.hasClass(this.options.classes.selectClass)) {
                   parentInput.val(optionId);
                   parentInput.trigger('change');
               } else {
                   elem.trigger('click');
               }
           }, this));
       },

       /**
        * Emulate mouse click or selection change on all swatches that should be selected
        * @param {Object} [selectedAttributes]
        * @private
        */
       _EmulateSelectedByAttributeId: function (selectedAttributes) {
           $.each(selectedAttributes, $.proxy(function (attributeId, optionId) {
               var elem = this.element.find('.' + this.options.classes.attributeClass +
                   '[data-attribute-id="' + attributeId + '"] [data-option-id="' + optionId + '"]'),
                   parentInput = elem.parent();

               if (elem.hasClass('selected')) {
                   return;
               }

               if (parentInput.hasClass(this.options.classes.selectClass)) {
                   parentInput.val(optionId);
                   parentInput.trigger('change');
               } else {
                   elem.trigger('click');
               }
           }, this));
       },

       /**
        * Get default options values settings with either URL query parameters
        * @private
        */
       _getSelectedAttributes: function () {
           var hashIndex = window.location.href.indexOf('#'),
               selectedAttributes = {},
               params;

           if (hashIndex !== -1) {
               params = $.parseQuery(window.location.href.substr(hashIndex + 1));

               selectedAttributes = _.invert(_.mapObject(_.invert(params), function (attributeId) {
                   var attribute = this.options.jsonConfig.mappedAttributes[attributeId];

                   return attribute ? attribute.code : attributeId;
               }.bind(this)));
           }

           return selectedAttributes;
       },

       /**
        * Callback which fired after gallery gets initialized.
        *
        * @param {HTMLElement} element - DOM element associated with a gallery.
        */
       _onGalleryLoaded: function (element) {
           var galleryObject = element.data('gallery');

           this.options.mediaGalleryInitial = galleryObject.returnCurrentImages();
       },

       /**
        * Sets mediaCache for cases when jsonConfig contains preSelectedGallery on layered navigation result pages
        *
        * @private
        */
       _setPreSelectedGallery: function () {
           var mediaCallData;

           if (this.options.jsonConfig.preSelectedGallery) {
               mediaCallData = {
                   'product_id': this.getProduct()
               };

               this.options.mediaCache[JSON.stringify(mediaCallData)] = this.options.jsonConfig.preSelectedGallery;
           }
       },

       /**
        * Callback for quantity change event.
        */
       _onQtyChanged: function () {
           var $price = this.element.parents(this.options.selectorProduct)
               .find(this.options.selectorProductPrice);

           $price.trigger(
               'updatePrice',
               {
                   'prices': this._getPrices(this._getNewPrices(), $price.priceBox('option').prices)
               }
           );
       }
   });

   return $.mage.SwatchRenderer;
});

In the swatch-renderer.js file, the custom code will get a description of the child product based on selection.

//  code for dynamic description  
    var productDescription = this.options.jsonConfig.desc[this.getProduct()];
    if (productDescription) {
        $('#description .description .value').html(productDescription);
    }

Conclusion:

This way, you can show dynamic product descriptions on the basis of configurable product swatch selection in Magento 2. If you face any hardship in displaying the product description dynamically for configurable products, let me know through the comment box.

Share the tutorial with your friends and stay updated for more. 

Happy Coding!

 

Previous Article

Google’s Project Magi: AI-Powered Search Engine to take over Microsoft’s Bing

Next Article

How Can Field Marketing Help B2B Marketers?

Write a Comment

Leave a Comment

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

Get Connect With Us

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨