Javascript Use Cases in Magento 2: Introduction to the Knockout.js

Magento
 
Research
 
09 August 2019 387

In the last article we reviewed the simplest custom module with minimal functionality. Using the JQuery functionality we’ve also added certain logic to JavaScript. Now let's move one step forward, let's port it to the Knockout component. Let’s refresh it in our memories a bit: our module based on the ajax response that shows the full stock of the product on the product page. The ajax call is made by clicking on the button. We have divided the logic into two parts, the first one is the event listener for the button, the next one - the logic for the button.

 

First of all, Knockout is JavaScript framework, not the best one, but not the worst one too, just of the hundreds of others. Magento developers have to choose something out of the vast amount of frameworks, let it be the Knockout.

 

Are you ready to dive into code? :) Let’s make some entry point - the empty Knockout component file `app/code/OpsWay/StoreInfo/view/frontend/web/js/infoComponent.js`

 

define([
    'jquery',
    'uiComponent',
    'ko'
    ], function ($, Component, ko) {
        'use strict';
        return Component.extend({
            initialize: function () {
                this._super();
                console.log('Opsway Info Component');
            }
        });
    }
);

 

Here we define the JQuery, UIComponent and Knockout as dependencies, where JQuery - the simple JQuery library, uiComponent - the predefined by Magento component which we are extending by adding our own functionality and finally the ko - Knockout framework. We need to create a simple HTML file `app/code/OpsWay/StockInfo/view/frontend/web/template/info.html` which we will use as Component's template in the future.

 

The first step would be to setup the new Javascript file on its place in Magento app. First of all lets generate the block and the phtml template files by using the Hotrod Cli code generator (just a tool for M2 code generation) or simply by creating them in your IDE.

 

By running this commands from the root of the project

 

vendor/bin/hotrod create:block OpsWay_StockInfo StockInfo

 

vendor/bin/hotrod create:block OpsWay_StockInfo stockinfo

 

we will receive the `app/code/OpsWay/StockInfo/Block/StockInfo.php`

 

<?php

namespace OpsWay\StockInfo\Block;

use Magento\Framework\View\Element\Template;

class StockInfo extends Template
{

}

 

and `app/code/OpsWay/StockInfo/view/frontend/templates/stockinfo.phtml`

 

<span>Hello World</span>

 

files (Another way is to manually create them).

 

Good news - now we are ready to customize the product page, lets make `app/code/OpsWay/StockInfo/view/frontend/layout/catalog_product_view.xml` file, or if you have it already - simply edit it as follows.

 

<?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="product.info.form.content">
            <block before="product.info.addtocart" class="OpsWay\StockInfo\Block\StockInfo" name="opsway.stockinfo" template="stockinfo.phtml">
            </block>
        </referenceBlock>
    </body>
</page>

 

Here we refer to the `product.info.form.content` file, and add our new block to it by means of putting it just before the `Add to Cart` button. More detailed explanation on how to customize the default layouts you can read in official documentation.

 

Please note that we did not add any code in our block class, all that is needed already exist in the code. Our `StockInfo` class extends the `Magento\Framework\View\Element\Template`class, which extends the `Magento\Framework\View\Element\AbstractBlock`

 

Shall we dive into it, shall we not? We can find the following lines in construction method (203 - 206 lines):

 

if (isset($data['jsLayout'])) {
   $this->jsLayout = $data['jsLayout'];
   unset($data['jsLayout']);
}

 

It means that the class can accept information about Java Script Component, we can pass on the needed data about the component and template in the layout file as block arguments.

 

<?xml version="1.0"?>
<!-- app/code/OpsWay/StockInfo/view/frontend/layout/catalog_product_view.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="product.info.form.content">
            <block before="product.info.addtocart" class="OpsWay\StockInfo\Block\StockInfo" name="opsway.stockinfo" template="stockinfo.phtml">
                <arguments>
                    <argument name="jsLayout" xsi:type="array">
                        <item name="components" xsi:type="array">
                            <item name="infoComponent" xsi:type="array">
                                <item name="component" xsi:type="string">OpsWay_StockInfo/js/infoComponent</item>
                                <item name="config" xsi:type="array">
                                    <item name="template" xsi:type="string">OpsWay_StockInfo/info</item>
                                </item>
                            </item>
                        </item>
                    </argument>
                </arguments>
            </block>
        </referenceBlock>
    </body>
</page>

 

In the given example we have passed on the `jsLayout` argument with our Component & we also we can declare in config array template for our Component. The only thing which is left before testing our dummy :) component is to set the place in `app/code/OpsWay/StockInfo/view/frontend/templates/stockinfo.phtml`

 

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

 

In the code sample above we are creating the `<div>` element with a special attribute which gives us the Knockout, binding the scope of the Component. The Comment in the `<div>` tells the Knockout to render the template. Using the `text/x-magento-init` type the script is calling `Magento_Ui/js/core/app` Magento function, which initiates the UI Component based on config that we receive from our block. For more detailed guide feel free to check out the documentation.

 

 

Now by flushing the cache and running setup:upgrade we can finally test our component.

 

php bin/magento cache:flush
php bin/magento setup:upgrade 

 

As a result Magento should render the div with a `test` message before the button. In the console we will see the `Opsway Info Component` message, which is running from the component on initialization.

 

 

 

Everything works as expected, let’s move further and translate the JQuery UI widget from the last article to the Component. To do this we need to encapsulate the ajax code to its own file `html/app/code/OpsWay/StockInfo/view/frontend/web/js/getStock.js`

 

define(
 [
   'jquery',
   'mage/storage'
 ],
 function (
   $,
   storage
 ) {
   'use strict';
   return function (product, deferred) {
     deferred = deferred || $.Deferred();
     var url = '/opsway/stock/info';

     return storage.post(
       url,
       JSON.stringify({
         productId: product
       })
     ).done(
       function (response) {
         if (response) {
           deferred.resolve({
             response: response
           });
         } else {
           deferred.reject();
         }
       }
     ).fail(
       function (response) {
         deferred.reject();
       }
     );
   };
 }
);

 

On the given example we have nothing common with Knockout. Just a function which returns $.Deferred status and using the `mage/storage` helps us to make ajax calls. We are ready to inject it in our component as dependency

 

define([
   'jquery',
   'uiComponent',
   'OpsWay_StockInfo/js/getStock',
   'ko'
 ], function ($, Component, getStock, ko) {
   'use strict';
   return Component.extend({
     initialize: function () {
       this._super();
     }
   });
 }
);

 

Lets add two attributes to our component, the first one will store the quantity, while the second one will be responsible for showing hinted text. We will manipulate it in the `checkTheStock` method, which will be bound for listening the click event from template

 

define([
   'jquery',
   'uiComponent',
   'OpsWay_StockInfo/js/getStock',
   'ko'
 ], function ($, Component, getStock, ko) {
   'use strict';

   var qty = ko.observable('');
   var showInfo = ko.observable(false);

   return Component.extend({
     productId: $('input[name="product"]').val(),
     qty: qty,
     showInfo: showInfo,

     initialize: function () {
       this._super();
     },

     checkTheStock: function () {
       var deferred = $.Deferred();
       var self = this;

       getStock(this.productId, deferred);

       $.when(deferred).done(function(response) {
         self.qty(response.response.qty);
         self.showInfo(true);
       });

       $.when(deferred).fail(function() {
         self.showInfo(false);
       });
     }
   });
 }
);

 

This is our final Component. And the template for it would be:

 

<div class="action primary"
    data-bind="click:checkTheStock"
    id="get-the-qty">
   <span><!-- ko i18n: 'Check the stock'--><!-- /ko --></span>
</div>
<!-- ko if: showInfo -->
<div>
   <p>
       <span data-bind="text:qty"></span>
       <!-- ko i18n: 'in stock'--><!-- /ko -->
   </p>
</div>
<!-- /ko -->
<br>

 

We have bound the click event on `checkTheStock` component method, performed a check to make sure we show information by looking on `showInfo` parameter. Besides we have to fix the Controller execute method from previous article. See below:

 

public function execute()
{
      $data = json_decode($this->getRequest()->getContent());

      if ($this->getRequest()->isAjax() && $data->productId) {
          $stock = $this->stockItemRepository->get($data->productId);

          $result = $this->resultJsonFactory->create();
          $result = $result->setData([
              'success' => 'true',
              'qty' => $stock->getQty()
          ]);

          return $result;
      }

      $this->_forward('noroute');
      return;
}

 

The reason to do it is sending the ajax data via JSON from our component. You can find the full source code here.

 

Thanks for reading. We hope you enjoyed it. If you want more articles on the technical topics, please leave a comment and share the article - doing so you will let us know that this content is interesting and useful.

 

This article was written by Dmitriy Ivanenko, senior full stack Magento 2 developer & clean code evangelist, who likes to read an elegant code as much as to write it by himself.

Comments

What are benefits of using knockout.js ?

Actually, Magento is able to be customized in different ways, you can use any Framework on frontend, or even write PWA, or use PWA which Magento suggests (https://magento.github.io/pwa-studio/ here you can read more), but Knockout is a native Magento 2 Framework, and developers have to know how to use it if they want to customize for example the native Checkout page

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.