Javascript Promises

I promise….
( 8 min reading and you will know what Javascript promises are )

The most commonly and famously asked question in the interview of a Javascript or frontend developer is: What are the promises? Why should we make promises? What are the states of promises? What are the promise chains? So what is a promise? Let’s try to understand it.

What is promise

As per the mdn web docs :
The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Bit technical definition to understand isn’t it?

Let’s try to understand promises with a story.

So suppose there is a family in which there are parents who have three children. One day, my parents were going to the supermarket. So one of their children asked them to bring ice cream for him or her. As usual, parents said okay to him or her.
Now that child asked the parents to please make me a promise that you will bring ice cream from the supermarket when you return, and the parent made a promise.

  1. Either parent will bring the ice cream from the supermarket and make their children happy.
  2. Or parents forgot to bring ice cream from the supermarket and made their children mad.

If parents bring ice cream for children, it means they have fulfilled their promise that they made to their child (this is a full-filled state of promise in JavaScript).

If parents didn’t bring the ice cream for children, it means they weren’t able to fulfill their promise (this is a rejected state of promise in JavaScript).

That’s it…. Yes, guys, this is actually what javascript promises are. Let’s understand now in technical terms.

Based on the technical definition promise is an object which eventually represents the completion or failure of an asynchronous operations.

Promises of having three states
1. Pending: It will be an initial state of promise.
2. Fulfilled: It will be a state when a promise is successfully completed (this will come under). then(), where we can pass or attach a successful function handler.
3. Rejected: It will be a state when a promise is rejected (this will come under). catch(), where we can pass or attach a rejected or failure function handler.

Why we should use the promise ?

Interesting another part of the promises is why we should use it? what kind of purposes it will resolve?

So, suppose if we are building a big application or if we are working on big projects where we need to make multiple asynchronous ajax calls and these ajax calls are dependent on each other, e.g., we need to make 2 ajax calls. makeAjax1(), makeAjax2(), and makeAjax2() calls should only be called if and only if the makeAjax1() call successfully returns.
In such cases, we can use promises.

Another bigger problem that we can solve using promises are callback hell scenarios( where the code or function has multiple callback functions and our code is structured like a pyramid of doom).

Another example is that we can get rid of giving our own code or function control to another function by passing the callback function.

In Such scenarios we can make our code more efficient and better if we will use promise.

Thanks for reading the article. Please let me know if I am able to clear a few doubts on the Javascript Promises.


In the next article, I will write about the promise chaining. Till that time, keep giving your support so that I can write more interesting topics in JavaScript.

Magento 2: Frontend Coding Standards

A short summary to keep your Magento frontend code up to standards

HTML

    • Always close self-closing tags. Examples of self-closing tags include <br> tag and <img> tags.
    • Avoid code lines longer than 120 characters. For example, input tags with multiple attributes. Align attributes under one another to solve the problem.
    • Use appropriate HTML5 elements/semantic HTML for blocks. Different sections of the HTML page should be clearly demarcated with header, footer, aside, etc. tags.
    • Try to keep class/ID names logical and stick to the same pattern everywhere.
    • Design the page keeping accessibility in mind. Keep font-sizes large enough to be easily readable.

    LESS/CSS

    • Use proper spacing and indentation.
    • Use single quotes for property values.
    • Avoid using the !important property if possible. If it is required, add a space before the property.
    • Avoid using the id selector to style elements.
    • Class names should be lowercase, words should be separated with dash ‘-‘.
    • Use meaningful, specific class names that reflect the purpose of the element.
    • Avoid using more than three levels of nesting.
    • Sort all properties in the alphabetical order. Mixins, variables, and so on should go first.
    • Use shorthand properties wherever possible.
    • Do not specify units “0” value. The “0” should be followed by the appropriate unit.
    • Use three-character hexadecimal notation where possible. The hexadecimal value should be in lowercase.
    • Use variables for colors and other reusable values.

    JavaScript/jQuery

    • Use proper spacing and indentation.
    • Keep the max code line length to 80 characters. Use string concatenation for multi-line string literals.
    • Use braces with all multiline blocks. Omit braces only if entire block can be written in one line and improves readability.
    • Always put semicolons as statement terminators.
    • Use single quotes instead of double quotes for consistency.
    • All variables or methods should have names that accurately describe their purpose or behavior.
    • Methods that return status flags or Boolean values should have the has or is prefix.
    • Always put the binary/ternary operators on the preceding line to avoid JavaScript’s implicit automatic semi-colon (ASI) insertion issues.
    • Use a variable initialized with a function expression to define functions within a block.
    • Use custom exceptions when appropriate.
    • Use single-line array and object initializations only if they fit in one line.
    • Use Object instead of Array for associative arrays.
    • Combine multiple var declarations to increase readability.

    The above points can help you keep your code up to standard. For detailed explanations and more information please see the Adobe source docs and w3.org resources.

    Image Zoom in/our on single click for Desktop using jQuery

    Simple jQuery plugin for image zoom on click with better quality.

    Prerequisite to use this plugin

    Image container should be wrapped by a div element with class name zoomImageContainer (should use same class for styling purpose)

    e.g.

    <div class="zoomImageContainer">
        <img ..../>
    </div>

    To have better image on zoom img tag should have data attribute with name data-zoom which container big/better quality image

    To initialise the zoom function call below function with few settings if require to change on document.ready

    $(element).onClickZoomImg();

    if multiple images needs to initialised than call below function

    $(element).each(function() {
        $(this).onClickZoomImg(); 
    });

    NOTES:

    1) At the time of calling imageZoom on the element, it is mandatory to pass element as a options

    i.e.

    $(element).onClickZoomImg({
    'element': $(element)
    })

    2) If needed to wrap image element with default containerZoom class please pass addWrapper: true

    as a options otherwise default option would be false

    $(element).onClickZoomImg({
    'element': $(element),
    'addWrapper':true
    })
    
    
    
    

    Full Code:

    <!DOCTYPE html>
    <html>
    <head>
    
    <script
      src="https://code.jquery.com/jquery-3.7.0.min.js"
      integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
      crossorigin="anonymous"></script>
    <style>
    div.zoomImageContainer {
        background-position: 50% 50%;
        position: relative;
        width: 100%;
        overflow: hidden;
        cursor: zoom-in;
        margin: 0
        background-repeat: no-repeat;
    }
    
    
    div.zoomImageContainer img {
        transition: opacity .5s;
        display: block;
        width: 100%
    }
    div.zoomImageContainer.active {
    	cursor: zoom-out;
    }
    
    div.zoomImageContainer.active img {
        opacity: 0
    }
    </style>
    <script>
    (function ($) {
        const imageObj = {};
    
      $.fn.onClickZoomImg = function (options,e) {
        //element should be present as a options
        if(options.element === 'undefined') {
            return;
        }
    
        // Default settings for the zoom level
        var settings = $.extend({
          zoom: options.zoom || 150,
          imageContainerClass: options.imgContainerClass || '.zoomImageContainer',
          element: options.element,
          addWrapper: options.addWrapper || false
        }, options); // Main html template for the zoom in plugin
       
        function onClickZoom(e) {
          var zoomEle = e.currentTarget;
          var x, y, offsetX, offsetY;
          e.offsetX ? offsetX = e.offsetX : offsetX = e.touches[0].pageX;
          e.offsetY ? offsetY = e.offsetY : offsetY = e.touches[0].pageX;
          x = offsetX / zoomEle.offsetWidth * 100;
          y = offsetY / zoomEle.offsetHeight * 100;
          $(zoomEle).css({
            "background-position": `${x}% ${y}%`
          });
        }
    
        function init() {
            var parentElementExist = $(settings.element).closest(settings.imageContainerClass).length;
            if(!parentElementExist || !settings.addWrapper){
                $(settings.element).parent().addClass('zoomImageContainer');
            }else {
                $(settings.element).wrap( "<div class='zoomImageContainer'></div>" );
            }
        }
          
        function bindEvents(container) {
          $(document).on('click',container,function(e){
            e.stopImmediatePropagation();
            e.stopPropagation();
            var zoomImg = $(e.target).attr('data-zoom') || $(e.target).attr('src'); //getting it through attr as data will not updated in dom cache
            $(this).css('background-image','url('+zoomImg+')');
            $(this).css('background-size',settings.zoom+'%');
    $(this).css('background-repeat','no-repeat');
            if(zoomImg){
              if ("zoom" in imageObj == false) {
                // zoom is not defined at start so define it
                imageObj.zoom = false;
              }
              if (imageObj.zoom) {
                imageObj.zoom = false;
                $(this).removeClass('active');
                $(this).css('background-image','')
                $(this).css('background-size','')
                $(this).css('background-position','')
              } else {
                imageObj.zoom = true;
                $(this).addClass('active');
                onClickZoom(e);
              }
            }
           
          });
          
          $(document).on('mousemove',container,function(e){
              imageObj.zoom ? onClickZoom(e) : null;
          });
    
          $(document).on('mouseleave',container,function(e){
              imageObj.zoom = false;
              $(this).removeClass('active');
              $(this).css('background-image','')
              $(this).css('background-size','')
              $(this).css('background-position','')
          });
        }
       init();
       bindEvents(settings.imageContainerClass);
    
      };
    })(jQuery);
    </script>
    </head>
    <body>
    
    <div class="zoomImageContainer">
        <img class="zoomImage" src="https://picsum.photos/id/870/200/300" data-zoom="https://picsum.photos/id/870/2000/2000"/>
    </div>
    <script>
    // A $( document ).ready() block.
    $( document ).ready(function() {
        $('.zoomImage').onClickZoomImg({
           'element': $('.zoomImage'),
           'addWrapper':false
    });
    });
    </script>
    </body>
    </html>
    
    

    To See full result click on the image:

    Magento 2 B2B Company Add Custom Attribute

    I have a client’s requirement to add a new custom attribute in the company module of the B2B extension.

    The need was to add a unique SAP ID in the Account Information section of the company form

    To achieve this requirement we follow below steps

    Step 1

    Create a new module app/code/XHTMLXPERT/Company

    Step 2

    Add new field sap_id in company table using declarative schema under app/code/XHTMLXPERT/Company/etc/db_schema.xml

    <?xml version="1.0" ?>
    <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
        <table name="company">
            <column name="sap_id" nullable="true" xsi:type="varchar" comment="company SAP ID" length="255"/>
            <constraint xsi:type="unique" referenceId="UNQIQUE_SAP_ID">
                <column name="sap_id"/>
            </constraint>
        </table>
    </schema>
    

    Step 3

    Add SAP ID field in the company form

    To achieve this create file app/code/XHTMLXPERT/Company/view/base/ui_component/company_form.xml
    fieldset name should remain information, if you want to show the field in any other section change name accordingly

    <?xml version="1.0" ?>
    <form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    	<fieldset name="information">
    		<field name="sap_id" formElement="input">
    			<argument name="data" xsi:type="array">
    				<item name="config" xsi:type="array">
    					<item name="source" xsi:type="string">sap_id</item>
    				</item>
    			</argument>
    			<settings>
    				<label translate="true">SAP ID</label>
    				<dataType>text</dataType>
    				<validation>
    					<rule name="required-entry" xsi:type="boolean">true</rule>
    				</validation>
    			</settings>
    		</field>
    	</fieldset>
    </form>

    Step 4

    To show the field in Companies grid

    Create file app/code/XHTMLXPERT/Company/view/adminhtml/ui_component/company_listing.xml and copy and paste the below xml in the same file

    <?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="company_columns">
            <column name="sap_id" sortOrder="230">
                <settings>
                    <filter>text</filter>
                    <label translate="true">SAP ID</label>
                    <visible>true</visible>
                </settings>
            </column>
        </columns>
    </listing>
    

    Step 5

    Add Extension Attribute

    app/code/XHTMLXPERT/Company/etc/extension_attributes.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\Company\Api\Data\CompanyInterface">
    		<attribute code="sap_id" type="string"/>
    	</extension_attributes>
    </config>

    Step 6

    To save and retrieve SAP ID it is required to create the below plugin’s

    Create app/code/XHTMLXPERT/Company/etc/di.xml

    <?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\Company\Model\Company\DataProvider">
    		<plugin name="XHTMLXPERT_Company_Plugin_Magento_Company_Model_Company_DataProvider" type="XHTMLXPERT\Company\Plugin\Magento\Company\Model\Company\DataProvider" sortOrder="10" disabled="false"/>
    	</type>
    	<type name="Magento\Company\Controller\Adminhtml\Index\Save">
    		<plugin name="XHTMLXPERT_Company_Plugin_Magento_Company_Controller_Adminhtml_Index_Save" type="XHTMLXPERT\Company\Plugin\Magento\Company\Controller\Adminhtml\Index\Save" sortOrder="20" disabled="false"/>
    	</type>
    	<type name="Magento\Company\Api\CompanyRepositoryInterface">
    		<plugin name="XHTMLXPERT_Company_Plugin_Magento_Company_Api_CompanyRepositoryInterface" type="XHTMLXPERT\Company\Plugin\Magento\Company\Api\CompanyRepositoryInterface" sortOrder="10" disabled="false"/>
    	</type>
    </config>

    To save the unique values in the database create the below plugin

    app/code/XHTMLXPERT/Company/Plugin/Magento/Company/Controller/Adminhtml/Index/Save.php

    <?php
    
    declare(strict_types=1);
    
    namespace XHTMLXPERT\Company\Plugin\Magento\Company\Controller\Adminhtml\Index;
    
    use Magento\Company\Api\CompanyRepositoryInterface;
    use Magento\Company\Controller\Adminhtml\Index\Save as ParentSave;
    use Magento\Framework\Api\FilterBuilder;
    use Magento\Framework\Api\Search\FilterGroupBuilder;
    use Magento\Framework\Api\SearchCriteriaBuilder;
    use Magento\Framework\App\ResponseFactory;
    use Magento\Framework\Controller\Result\RedirectFactory;
    use Magento\Framework\Message\ManagerInterface;
    use Magento\Framework\UrlInterface;
    
    class Save
    {
    
        /**
         * @var SearchCriteriaBuilder
         */
        private SearchCriteriaBuilder $searchCriteriaBuilder;
    
        /**
         * @var FilterBuilder
         */
        private FilterBuilder $filterBuilder;
    
        /**
         * @var FilterGroupBuilder
         */
        private FilterGroupBuilder $filterGroupBuilder;
    
        /**
         * @var \Magento\Framework\App\ResponseFactory
         */
        private $responseFactory;
    
        /**
         * @var \Magento\Framework\UrlInterface
         */
        private $url;
    
        /**
         * @var ManagerInterface
         */
        private $messageManager;
    
        public function __construct(
            SearchCriteriaBuilder $searchCriteriaBuilder,
            FilterBuilder $filterBuilder,
            FilterGroupBuilder $filterGroupBuilder,
            CompanyRepositoryInterface $companyRepository,
            ResponseFactory $responseFactory,
            UrlInterface $url,
            ManagerInterface $messageManager
        ) {
            $this->searchCriteriaBuilder = $searchCriteriaBuilder;
            $this->filterBuilder = $filterBuilder;
            $this->filterGroupBuilder = $filterGroupBuilder;
            $this->companyRepository = $companyRepository;
            $this->responseFactory = $responseFactory;
            $this->url = $url;
            $this->messageManager = $messageManager;
        }
    
        /**
         * @param ParentSave $subject
         * @param $result
         * @return mixed
         */
        public function afterSetCompanyRequestData(
            ParentSave $subject,
            $result
        ) {
                $companyExtension = $result->getExtensionAttributes();
                $companyExtension->setSapId($subject->getRequest()->getPostValue('information')['sap_id']);
                $result->setExtensionAttributes($companyExtension);
    
            return $result;
        }
    
        public function beforeExecute(
            ParentSave $subject
        ) {
            $request = $subject->getRequest();
            $id = $request->getParam('id') ? $request->getParam('id') : null;
            $sapID = $subject->getRequest()->getPostValue('information')['sap_id'] ??  $subject->getRequest()->getPostValue('information')['sap_id'];
            if($sapID) {
                $sapIdFilter = $this->filterBuilder
                    ->setField('sap_id')
                    ->setValue($sapID)
                    ->setConditionType('eq')
                    ->create();
    
                $sapIdFilterGroup = $this->filterGroupBuilder
                    ->addFilter($sapIdFilter)
                    ->create();
    
                $entityIdFilter = $this->filterBuilder
                    ->setField('entity_id')
                    ->setValue($id)
                    ->setConditionType('neq')
                    ->create();
                $entityIdFilterGroup = $this->filterGroupBuilder
                    ->addFilter($entityIdFilter)
                    ->create();
                if ($id) {
                    $searchCriteria = $this->searchCriteriaBuilder
                        ->setFilterGroups([$sapIdFilterGroup, $entityIdFilterGroup])
                        ->create();
                    $redirectionUrl = $this->url->getUrl('company/index/edit',['id' => $id]);
                } else {
                    $searchCriteria = $this->searchCriteriaBuilder
                        ->setFilterGroups([$sapIdFilterGroup])
                        ->create();
                    $redirectionUrl = $this->url->getUrl('company/index/new');
                }
                $items = $this->companyRepository->getList($searchCriteria);
                if ($items->getTotalCount()) {
                    $this->messageManager->addErrorMessage('SAP ID already exist');
                    $this->responseFactory->create()->setRedirect($redirectionUrl)->sendResponse();
                    die();
                }
            }
            return [];
        }
    }
    

    create plugin app/code/XHTMLXPERT/Company/Plugin/Magento/Company/Model/Company/DataProvider.php

    <?php
    
    declare(strict_types=1);
    
    namespace XHTMLXPERT\Company\Plugin\Magento\Company\Model\Company;
    
    use Magento\Company\Model\Company\DataProvider as ParentDataProvider;
    use Magento\Company\Api\Data\CompanyInterface;
    
    class DataProvider
    {
        private const SAP_ID = 'sap_id';
    
        /**
         * @param ParentDataProvider $subject
         * @param $result
         * @return mixed
         */
        public function afterGetInformationData(
            ParentDataProvider $subject,
            $result,
            CompanyInterface $company
        ) {
            $result[self::SAP_ID] = $company->getData('sap_id');
            return $result;
        }
    }
    

    create plugin app/code/XHTMLXPERT/Company/Plugin/Magento/Company/Api/CompanyRepositoryInterface.php

    <?php
    
    declare(strict_types=1);
    
    namespace XHTMLXPERT\Company\Plugin\Magento\Company\Api;
    
    use Magento\Company\Model\CompanyRepository;
    use Magento\Company\Api\Data\CompanyExtensionFactory;
    use Magento\Company\Api\CompanyRepositoryInterface as ParentCompanyRepositoryInterface;
    
    class CompanyRepositoryInterface
    {
        /**
         * @var CompanyExtensionFactory
         */
        protected $companyExtensionFactory;
    
        /**
         * @var CompanyRepository
         */
        protected $companyRepository;
    
    
        public function __construct(
            CompanyRepository $companyRepository,
            CompanyExtensionFactory $companyExtensionFactory
        ) {
            $this->companyRepository = $companyRepository;
            $this->companyExtensionFactory = $companyExtensionFactory;
        }
    
        /**
         * @param ParentCompanyRepositoryInterface $subject
         * @param $result
         * @return mixed
         */
        public function afterGet(
            ParentCompanyRepositoryInterface $subject,
            $result
        ) {
            $company = $result;
            $extensionAttributes = $company->getExtensionAttributes();
            $companyExtension = $extensionAttributes ? $extensionAttributes : $this->companyExtensionFactory->create();
            
            $companyExtension->setSapId($company->getData('sap_id'));
    
            $company->setExtensionAttributes($companyExtension);
            return $company;
        }
    
        /**
         * @param ParentCompanyRepositoryInterface $subject
         * @param $result
         * @return mixed
         */
        public function afterSave(
            ParentCompanyRepositoryInterface $subject,
            $result
        ) {
            $company = $result;
            $extensionAttributes = $company->getExtensionAttributes();
            if (!$extensionAttributes) {
                return $company;
            }
            
            $company->setData('sap_id', $extensionAttributes->getSapId());
    
            $company->save();
            return $company;
        }
    
        /**
         * @param ParentCompanyRepositoryInterface $subject
         * @param $result
         * @return mixed
         */
        public function afterGetList(
            ParentCompanyRepositoryInterface $subject,
            $result
        ) {
            foreach ($result->getItems() as $company) {
                $this->afterGet($subject, $company);
            }
            return $result;
        }
    }
    

    Show coupon_code in the admin sales order grid and also make it searchable through filters as well as from keyword search field

    Sometimes there is a requirement to show a few fields of sales_order table in the admin order grid and also make the field filterable as well as searchable through keyword search.

    To do this follow the below steps

    Step 1

    Add db_schema.xml in your module to add a new column in sales_order_grid table which you want to show in the admin order grid.
    Add full-text index for column else you will and able to search it through the keyword search field, Make shore referenceId is same as what is used in

    vendor/magento/module-sales/etc/db_schema.xml

    <?xml version="1.0"?>
    <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
        <table name="sales_order_grid" resource="default" engine="innodb">
            <column xsi:type="varchar" name="coupon_code" nullable="true" length="50" comment="Coupon Code"/>
            <index referenceId="FTI_65B9E9925EC58F0C7C2E2F6379C233E7" indexType="fulltext">
                <column name="coupon_code"/>
            </index>
        </table>
    </schema>

    Step 2

    Add sales_order_grid.xml file in the below path of your module, This file is responsible for adding a new column on the admin order grid.

    app/code/XHTMLXPERT/OrderGrid/view/adminhtml/ui_component/sales_order_grid.xml

    <?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="sales_order_columns">
            <column name="coupon_code">
                <argument name="data" xsi:type="array">
                    <item name="config" xsi:type="array">
                        <item name="filter" xsi:type="string">text</item>
                        <item name="label" xsi:type="string" translate="true">Coupon</item>
                    </item>
                </argument>
            </column>
        </columns>

    Step 3

    Add virtual type in di.xml, This file is responsible for adding and updating column data in sales_order_grid table when ever it is added or updated in sales_order table

    app/code/XHTMLXPERT/OrderGrid/etc/di.xml

    <?xml version="1.0"?>
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <virtualType name="Magento\Sales\Model\ResourceModel\Order\Grid">
            <arguments>
                <argument name="columns" xsi:type="array">
                    <item name="coupon_code" xsi:type="string">sales_order.coupon_code</item>
                </argument>
            </arguments>
        </virtualType>
    </config>
    

    Related/Cross-sell/Upsell Products in Minicart (Front-End)

    Suppose you are to build a feature where related, cross-sell products, etc. should appear in the minicart. This article will talk about the steps involved to achieve this functionality from the front-end perspective.

    Adding the block to minicart phtml

    Our first step will be showing our content in the minicart. We will be updating the minicart.phtml file in our own theme.

    Copy the file on this path Magento\site\public\vendor\magento\module-checkout\view\frontend\templates\cart\minicart.phtml and add your own custom block (custom.addon) below the pre-existing minicart.addons block.

    <div id="minicart-content-wrapper" data-bind="scope: 'minicart_content'">                <!-- ko template: getTemplate() --><!-- /ko -->
    </div>
    <?= $block->getChildHtml('minicart.addons') ?>
    <?= $block->getChildHtml('custom.addon') ?>
          

    Adding our block to XML layout

    Now, we will add the custom.addon block in the default.xml of our own custom module in the view/frontend/layout folder. Here it is important to point out that creating our own module and a few other tasks required to achieve this functionality are a part of backend development which are not covered here. However, all such tasks will be listed in the end of the article for reference.

    This is how our default.xml will look like.

    <?xml version=”1.0″?>
    <page xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:View/Layout/etc/page_configuration.xsd”>
    <body>
    <referenceBlock name=”minicart”>
    <container name=”custom.addon” htmlTag=”div” htmlClass=”custom-minicart”>
    <block ifconfig=”custom_minibasket/general/enable” class=”CustomMinicart\Block\minicart” name=”custom.minicart” template=”CustomMinicart::minicart-addon.phtml”>
    <arguments>
    <argument name=”jsLayout” xsi:type=”array”>
    <item name=”components” xsi:type=”array”>
    <item name=”minicart_info_bottom” xsi:type=”array”>
    <item name=”component” xsi:type=”string”>uiComponent</item>
    <item name=”children” xsi:type=”array”>
    <item name=”minicart.minicart” xsi:type=”array”>
    <item name=”component” xsi:type=”string”>CustomMinicart/js/minicart/minicartcustom</item>
    <item name=”config” xsi:type=”array”>
    <item name=”template” xsi:type=”string”>CustomMinicart/minicart/item/minicartcustom</item>
    </item>
    </item>
    </item>
    </item>
    </item>
    </argument>
    </arguments>
    </block>
    </container>
    </referenceBlock>
    </body></page>

    We have wrapped our block in ifconfig to be able to toggle off the functionality in case its required.

    Now, we will bind our knockout template (minicartcustom.js and minicartcustom.html) which will build our component in minicart-addon.phtml.

    <div id="custom-js-component" data-bind="scope:'minicart_info_bottom'">
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">
     {
    "#custom-js-component": { 
    "Magento_Ui/js/core/app":  <?php /* @escapeNotVerified */ echo $block->getJsLayout();?>
    }    }
    </script>
    </div>

    Now all that’s left is to populate our custom knockout template file as required. We can take reference from Magento\site\public\vendor\magento\module-checkout\view\frontend\web\template\minicart\item\default.html. Since we can have any number of products as related or cross-sell, we would need to iterate over the data in our knockout html file to render all products using ko foreach. We can get the data from cart customer data and bind it to a knockout observable in our JS file. However, we may run into a problem when rendering swatches. For this, we need to see how the swatches are called in vendor: Magento\site\public\vendor\magento\module-swatches\view\frontend\templates\product\view\renderer.phtml

    Similar to the renderer.phtml file, we would need to initialize the swatches in our knockout template. Here’s how you will do it:

    <div class=”swatch-options” data-bind=”attr: {mageInit:
    {‘Magento_Swatches/js/swatch-renderer’: {
    ‘jsonConfig’: data.jsonConfig,
    ‘jsonSwatchConfig’: data.jsonSwatchConfig,
    ‘mediaCallback’: data.mediaCallback,
    ‘jsonSwatchImageSizeConfig’: data.jsonSwatchImageSizeConfig
    }}”>
    </div>

    Potential BE tasks

    Up till now, we have listed all the steps with the assumption that we already have the relevant data from the BE. Here, we will try to list out what all data we might need and other important BE tasks:
    1.Dynamic related products/cross-sell products data in cart object.
    2.Jsconfig data for configurable products
    3.Priceconfig data for all products

    Besides the data listed above, creating the module and creating ifconfig will also be need to be handled on the BE side. Also, to get the js layout in our phtml file, a custom block(CustomMinicart) will need to be created. This will have the getJsLayout function to render our template.

    Fin.

    Adding new Ui Component Magento 2

    Many Developers see that implementation of Ui component is not helpful where we can easily achieve the desired functionality using plan javascript by simply adding it inside any of the phtml files.

    hmmm… wait!!! is that really true in all cases ? Just think about it twice.

    The correct answer is NO. Magento 2 provides a powerful component when you need to deal with dynamic UI’s on the frontend and that is Ui components. Even if we talks about the best practices in Magento 2 it always suggest to use Ui components as much as possible.

    So let’s understand if we need to add new Ui component on any of the page what would be the process of adding it.

    For an Instance I am taking an example of adding the Ui component on Product Detail Page (PDP). As on live projects there are very frequent scenarios comes where we need to add some dynamic information on the PDP pages. So without any more trouble let’s start with PDP

    Prerequisites

    • Magento 2 Layout understanding – Basic knowledge of layout customization would be good enough
    • Magento 2 JavaScript Initialization – Knowledge how to add custom JavaScript’s to any of the phtml files

    I would suggest please have a look once in official dev docs here.

    To create new Ui component on PDP page we first need layout file where we can add our custom phtml which will responsible to initialize the ui components

    If you are working in any custom module then you layout file should be placed below path:

    <Custom_Module_dir>/view/frontend/layout/catalog_product_view.xml

    Here we are going to extend the layout of product view page / product detail page.

    NOTE: We should always avoid to override any of the layout until it is not totally necessary. We should always extend the layouts not override them totally.

    catalog_product_view.xml should looks like this if we are extending new one in our custom module:

    <?xml version="1.0"?>
    <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
        <body>
            
        </body>
    </page>
    

    To extend the above layout we need to use referenceContainer or referenceBlock like below:

    <referenceContainer name="product.info.main">
    
    </referenceContainer>

    As we are taking referenceContainer name product info main our all changes will go inside this container only no other containers or blocks will get interrupted.

    To add custom phtml inside our layout we need to add block node inside referenceContainer like below:

    <referenceContainer name="product.info.main">
        <block class="Magento\Catalog\Block\Product\View" name="product.custom" template="Custom_Module_dir::catalog/product/view/custom.phtml" before="product.info.price"/>
    </referenceContainer>

    After adding above in our layout the whole file will look like below:

    <?xml version="1.0"?>
    <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
        <body>
            <referenceContainer name="product.info.main">
    	   <block class="Magento\Catalog\Block\Product\View"    name="product.custom" template="Custom_Module_dir::catalog/product/view/custom.phtml" before="product.info.price"/>
    	</referenceContainer>
        </body>
    </page>
    

    Now we need to create custom.phml file at described path in the layout, Magento will locate this phtml file in below location:

    <Custom_Module_dir>/view/frontend/templates/catalog/product/view/custom.phtml

    Add below content in the phtml file:

    <?php
    /** You can add php logics here if there are any*/
    ?>
    <div id="custom-uicomponent" data-bind="scope:'customcomponent'">
        <!-- ko template: getTemplate() --><!-- /ko -->
        <script type="text/x-magento-init">
        {
            "#custom-uicomponent": {
                "Magento_Ui/js/core/app": {
                   "components": {
                        "customcomponent": {
                            "component": "Custom_Module_dir/js/custom-ui-component"
                        }
                    }
                }
            }
        }
        </script>
    </div>
    

    Few things to note down here:

    1. We should always initialize component on id and in our case it is custom-uicomponent
    2. Script initialization should only be done using type = “text/x-magento-init”
    3. As we are creating uiComponent we must need to extend the Magento_Ui/js/core/app component here
    4. To define the scope we should use the data binding scope and in our case we used it like data-bind=”scope:’customcomponent'” on div element.
    5. In the components object key we need to define our component which will responsible for UI in our case it is “customcomponent”: { “component”: “Custom_Module_dir/js/custom-ui-component” }

    Now let’s create our ui component JavaScript file on described path in the custom.phtml file

    Magento 2 will look the JavaScript file at below location:

    <Custom_Module_dir>/view/frontend/web/js/custom-ui-component.js

    Add below code inside above JavaScript file:

    define([
      'jquery',
      'uiComponent',
      'ko',
      'jquery/ui',
      'domReady!'],
      function ($, Component, ko, utils) {
        'use strict';
    
        return Component.extend({
          defaults: {
            template: 'Custom_Module_dir/custom-ui-component'
          },
    	  
          initialize: function (config) {
            this._super();
    
            //config args is to take data from phtml files
          },
        });
      }
    );

    Few things to note down here:

    1. As a dependency we mainly need uiComponent, jquery however we add few more will discuss those dependencies in separate article.
    2. As we are extending the component we are returning Component.extend method.
    3. Ui component needs an html file and it should be defined under defaults object of template property.
    4. Ui component having predefined functions also and initialize method is one of them.

    Now let’s create html file for our uiComponent.

    Magento 2 will look the html file at below location:

    <Custom_Module_dir>/view/frontend/web/template/custom-ui-component.html

    Add below code in the html:

    <div class="custom-ui-component">
     <span data-bind="text:$t('Hi I am new component')"></span>
    </div>

    Few things to note down here:

    1. We have used knockout data binding text to bind our text
    2. We have wrap our text inside translation function. There are other ways as well to make the text translatable

    Finally, It’s done. Yeah!!! that is the only steps we need to do to adding new ui component to any of the page.

    Run the cache flush command in magento 2 and see the pdp pages our custom ui component will be rendered.

    I’ll keep you posted my knowledge where many of the frontend developers stuck in magento 2 specially on HOW’s section.

    Debugging Transactional Emails in Magento

    Getting emails to work fine on all email clients is always challenging. We already know the basic rules to follow and the same apply when we are working on the order confirmation/return emails in Magento.

    1. Remove DIVS from layout

    Since the transactional emails call a lot of different blocks and layout, it may not be possible to replace all the divs with table structure. However, if your email is coming broken on any client, you can try removing the specific div tags that may be wrapped around the broken content.

    If, for instance, the below section is coming broken then we would need to go to the file being called by the layout handle and replace the div tags with correct structure.

    <tr class="email-information">
    <td class="email-information-block">

    {{layout handle="sales_email_order_items" order=$order area="frontend"}}

    </td>
    </tr>


    2. Verify the table layout has correct structure

    Oftentimes, the table structure itself might have some problems. For example, in the below snippet, the ‘Order Totals’ HTML might not be having tr and td tags enveloping the content. The result would be incorrect rendering in some email clients although Gmail, etc. can still show it correctly.

    <table style="width: 100%;">
    <?= $block->getChildHtml('order_totals') ?>
    </table>


    3.Use Borders to find out where the styling is broken

    When the email is large and has many dynamic blocks, it can be time-taking to find out the faulty layout. We can use inline borders to help narrow out the areas that have the problem. Additionally, the Magento admin email template area can be used for quick debugging of the issue without needing to make changes in the code.


    4.Use local email client

    Since it is not possible to view our email HTML in gmail and other clients, a local email client – like MailHog – can help us see the rendered structure. This can be useful if we have css coming from classes in less files. Even though all styles come merged and inline in the browser, the HTML still shows the classes in the local client and we can see what all styles are there and rule out those that are not causing the issue.

    In conclusion, we must follow all the existing good practices when working on emails. The above tips can be useful while debugging the emails in Magento.

    How to Load Products by Product ID and SKU in Magento 2?

    How to Load Products by Product ID and SKU in Magento 2?

    While customization magento2 multiple times developer required to load product data by id or sku.

    There are multiple ways to do so but best way which magento dev doc suggest is to use ProductRepositoryInterface

    Below is detail example class with two methods to load product data by id or sku

    namespace XhtmlXpert\Model;
    
    use Magento\Catalog\Api\ProductRepositoryInterface;
    
    class Product
    {
       protected $productrepository;  
    
       public function __construct(
       ProductRepositoryInterface $productrepository
       ) {
    	$this->productrepository = $productrepository;
       }
       
       /**
         * Get product info by product id
         * @return \Magento\Catalog\Api\Data\ProductInterface
         */
    
       public function getProductDataById(int $productid) 
       {
           return $this->productrepository->getById($productid);
       }
       
       /**
         * Get product info by product SKU
         * @return \Magento\Catalog\Api\Data\ProductInterface
         */
       
       public function getProductDataBySku(string $sku) 
       {
           return $this->productrepository->get($sku);
       }
    }

    Magento 2 Create Custom Swatch Attribute Programmatically

    Magento 2 Create Custom Swatch Attribute Programmatically

    Create magento 2 module ProductAttribute under name space xhtmlxpertCreate magento 2 module ProductAttribute under name space xhtmlxpert

    Create folder Setup/Patch/Data/ add file AddColourAttributes.php

     

    <?php
    namespace xhtmlxpert\ProductAttribute\Setup\Patch\Data; use Magento\Catalog\Model\Product as Product; use Magento\Eav\Setup\EavSetupFactory; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Eav\Model\Config; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory; /** * Class AddColourAttributes * @package xhtmlxpert\ProductAttribute\Setup\Patch\Data */ class AddColourAttributes implements DataPatchInterface { /** * @var array */ protected $colorMap = [ 'Pina colada' => '#F7EA48', 'Amaretto' => '#742C3F', 'Gin and Tonic' => '#C8C9C7', 'Light Gray' => '#F2F2F2' ]; /** * @var ModuleDataSetupInterface */ private $moduleDataSetup; /** * @var EavSetupFactory */ private $eavSetupFactory; /** @var Config */ private $eavConfig; /** * constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param EavSetupFactory $eavSetupFactory * @param Config $eavConfig * @param CollectionFactory $attrOptionCollectionFactory */ public function __construct( ModuleDataSetupInterface $moduleDataSetup, EavSetupFactory $eavSetupFactory, Config $eavConfig, CollectionFactory $attrOptionCollectionFactory ) { $this->moduleDataSetup = $moduleDataSetup; $this->eavSetupFactory = $eavSetupFactory; $this->eavConfig = $eavConfig; $this->attrOptionCollectionFactory = $attrOptionCollectionFactory; } /** * @return DataPatchInterface|void * @throws \Magento\Framework\Exception\LocalizedException */ public function apply() { $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); $sortOrder = 40; $attribute_id = "ui_colour"; if ($eavSetup->getAttributeId(Product::ENTITY, $attribute_id)) { $eavSetup->removeAttribute(Product::ENTITY, $attribute_id); } $attributeDetail = [ 'type' => 'int', 'frontend' => '', 'label' => 'UI Colour', 'input' => 'select', 'class' => '', 'visible' => true, 'required' => false, 'user_defined' => true, 'default' => null, 'searchable' => false, 'filterable' => false, 'comparable' => true, 'visible_on_front' => true, 'used_in_product_listing' => true, 'unique' => false, 'apply_to' => '', 'system' => false, 'global' => ScopedAttributeInterface::SCOPE_STORE, 'option' => [ 'values' => [ 'Pina colada', 'Amaretto', 'Gin and Tonic', 'Light Gray' ] ] ]; //add product addtibute $eavSetup->addAttribute( Product::ENTITY, $attribute_id, $attributeDetail ); //assign product addtibute to attribute set and attribute group $setId = $eavSetup->getAttributeSetId(Product::ENTITY, 'Default'); $groupId = $eavSetup->getAttributeGroupId(Product::ENTITY, $setId, 'Default'); $eavSetup->addAttributeToGroup( Product::ENTITY, $setId, $groupId, $attribute_id, $sortOrder ); //convert attribute to swatch $this->eavConfig->clear(); $attribute = $this->eavConfig->getAttribute('catalog_product', $attribute_id); $attribute_db_id = $eavSetup->getAttributeId('catalog_product', $attribute_id); if (!$attribute) { return; } $attributeData['option'] = $this->addExistingOptions($attribute_db_id); $attributeData['frontend_input'] = 'select'; $attributeData['swatch_input_type'] = 'visual'; $attributeData['update_product_preview_image'] = 1; $attributeData['use_product_image_for_swatch'] = 0; $attributeData['optionvisual'] = $this->getOptionSwatch($attributeData); $attributeData['swatchvisual'] = $this->getOptionSwatchVisual($attributeData); $attribute->addData($attributeData); $attribute->save(); } /** * @param array $attributeData * @return array */ protected function getOptionSwatch(array $attributeData) { $optionSwatch = ['order' => [], 'value' => [], 'delete' => []]; $i = 0; foreach ($attributeData['option'] as $optionKey => $optionValue) { $optionSwatch['delete'][$optionKey] = ''; $optionSwatch['order'][$optionKey] = (string)$i++; $optionSwatch['value'][$optionKey] = [$optionValue, '']; } return $optionSwatch; } /** * @param array $attributeData * @return array */ private function getOptionSwatchVisual(array $attributeData) { $optionSwatch = ['value' => []]; foreach ($attributeData['option'] as $optionKey => $optionValue) { if (substr($optionValue, 0, 1) == '#' && strlen($optionValue) == 7) { $optionSwatch['value'][$optionKey] = $optionValue; } else if ($this->colorMap[$optionValue]) { $optionSwatch['value'][$optionKey] = $this->colorMap[$optionValue]; } else { $optionSwatch['value'][$optionKey] = $this->colorMap['White']; } } return $optionSwatch; } /** * @param $attributeId * @return array */ private function addExistingOptions($attributeId) { $options = []; if ($attributeId) { $this->loadOptionCollection($attributeId); /** @var \Magento\Eav\Model\Entity\Attribute\Option $option */ foreach ($this->optionCollection[$attributeId] as $option) { $options[$option->getId()] = $option->getValue(); } } return $options; } /** * @param $attributeId */ private function loadOptionCollection($attributeId) { if (empty($this->optionCollection[$attributeId])) { $this->optionCollection[$attributeId] = $this->attrOptionCollectionFactory->create() ->setAttributeFilter($attributeId) ->setPositionOrder('asc', true) ->load(); } } /** * @return array|string[] */ public static function getDependencies() { return []; } /** * @return array|string[] */ public function getAliases() { return []; } }
    ?>