Magento 2 module development – a comprehensive guide – Part 4 (Knockout.js)

In our previous three articles, we described how to build up a sample Magento 2.0 module. We created an admin panel and a custom database table for it, showed how to save and manage data and also how to display data on frontend. Then we went on describing how to use Observers with the help of another sample module. In this post we will explain how to use Knockout.js which is used many times within the Magento 2.0 system.



Contents:

  • What is Knockout.js?
  • Why is it worth using it? What is it good for?
  • Structure of our basic sample module
  • Creating blocks and layout
  • Using Knockout.js in PHTML template file
  • Creating and implementing our own CustomerData class
  • Using Knockout.js as an HTML template file

 

 

What is Knockout.js?

Knockout.js is an open source, simplified dynamic javascript which is a Model-View-View Model (MVVM) system/framework.

 

Why is it worth using it? What is it good for?

Of course it is not mandatory to use Knockout.js with Magento 2 projects. However, since it forms an integral part of Magento 2.0, and also because of the use of full page cache, it is worth considering applying it after taking into account the business logic to be implemented in our own module.

Without creating separate and complex javascripts in our module, we can get access to customer, product, order and other data and also display and manage them on frontend with this handy solution.

 

Structure of our basic sample module

 

We are going to use the same basic sample module that we already used earlier (see previous articles, Part 1 and Part 2).

Since our first article already covered how to build up the basic module, now we show the structure of the module itself only.

 

magento 2 module structure

 

Creating blocks and layout

 

We create a block for the sample and create two separate template files in which we use Knockout.js in two different ways.

In the first example, we use Knockout.js in the template file itself, while in the second example we implement (in other words “load in”) another HTML template file with it.

For these two examples we create a layout file as a first step and, depending on which block we use, now, for the sake of simplicity, we comment out the corresponding one.

We need a default.xml file for that, to be found at app/code/Aion/Sample/view/frontend/layout/default.xml in our sample module. The file includes the following:

 

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
     <body>
         <referenceBlock name="sidebar.additional">
             <!-- First sample -->
             <block class="Aion\Sample\Block\Sample" name="aion.sample.knockout.sidebar" template="Aion_Sample::sidebar.phtml" after="wishlist_sidebar"/>
             <!-- Second Sample -->
             <!--<block class="Aion\Sample\Block\Sample" name="aion.sample.knockout.sidebar.second" template="Aion_Sample::second-sidebar.phtml" after="wishlist_sidebar"/>-->
         </referenceBlock>
     </body>
 </page>

 

In this example our own block and the belonging template file will appear in the layout file under the sidebar (<referenceBlock name=”sidebar.additional”>).

Next, we create the block class, also seen in the layout above. It can be found at app/code/Aion/Sample/Block/Sample.php in our sample module. The file includes the following:

 

<?php
 namespace Aion\Sample\Block;
 use Magento\Framework\View\Element\Template;
 use Aion\Sample\Helper\Data as DataHelper;
 
 class Sample extends Template
 {
     /**
      * @var DataHelper
      */
     protected $_helper;
 
     /**
      * @param Template\Context $context
      * @param DataHelper $dataHelper
      * @param array $data
      */
     public function __construct(
         Template\Context $context,
         DataHelper $dataHelper,
         array $data = []
     ) {
         $this->_helper = $dataHelper;
         parent::__construct($context, $data);
         $this->_isScopePrivate = true;
     }
 
     /**
      * Get extension helper
      *
      * @return DataHelper
      */
     public function getExtensionHelper()
     {
         return $this->_helper;
     }
 
     /**
      * Sample items for example
      *
      * @return array
      */
     public function getInitSecondItems()
     {
         $sampleData = $this->_helper->getSampleProductNames();
 
         return $sampleData;
     }
 }

 

The getInitSecondItems() function, seen in the block, will be mentioned in detail later in the second example.

 

Use of Knockout.js in PHTML template file

 

It is also necessary to create the corresponding javascript files for introducing Knockout.js. As a first step, we define the javascript file, belonging to our module, in requirejs-config.js. This can be found at app/code/Aion/Sample/frontend/requirejs-config.js in our sample module. The file includes the following:

 

var config = {
     map: {
         '*': {
             sample: 'Aion_Sample/js/view/sample-sidebar'
         }
     }
 };

 

Here we define where our own javascript file can be found in our module.

Naturally, the sample-sidebar.js file is needed as well. This is located here in the sample module: app/code/Aion/Sample/frontend/web/js/view/sample-sidebar.js

The file includes the following:

 

define([
     'ko',
     'uiComponent',
     'Magento_Customer/js/customer-data',
     'mage/translate'
 ], function (ko, Component, customerData, $t) {
     'use strict';
     return Component.extend({
         // Second example
         defaults: {
             template: 'Aion_Sample/second-sidebar'
         },
         displayContent: ko.observable(true),
         initialize: function () {
             this._super();
             this.sample = customerData.get('sample');
             // Second example
             this.someText = $t('Sample content with template.');
             // Second example. foreach examples
             this._showItems();
             // Second example, foreach example
             this._showMonths();
             // Second example, other foreach example
             this._showCategories();
             // Second example and another foreach example
             this._showMyItems();
         },
         getInfo: function() {
             return this.sample().info || this.initFullname || customerData.get('customer')().fullname;
         },
         getCartItemsCountText: function () {
             return this.sample().cart_items_text;
         },
         getCartItemsCount: function () {
             return this.sample().cart_count;
         },
         getHint: function() {
             return this.sample().hint || this.initHint;
         },
         _showItems: function() {
             var self = this;
             if (typeof this.initSampleData !== "undefined") {
                 self.sampleItems = JSON.parse(this.initSampleData);
             }
         },
         _showMonths: function() {
             var self = this;
             self.months = [ 'Jan', 'Feb', 'Mar', 'etc' ];
         },
         _showCategories: function() {
             var self = this;
             self.categories = [
                 { name: 'Fruit', items: [ 'Apple', 'Orange', 'Banana' ] },
                 { name: 'Vegetables', items: [ 'Celery', 'Corn', 'Spinach' ] }
             ];
         },
         _showMyItems: function() {
             var self = this;
             self.myItems = [ 'First', 'Second', 'Third' ]
         }
     });
 });

 

Before the functions in this example, we marked with comments those which belongs to the second example. We can see //Second example before such objects and functions.

 

Let’s see how it works!

Javascript extends the uiComponent object present in Magento 2.0 and then our own sample object gets defined, which becomes a part of CustomerData (this.sample = customerData.get (‘sample’);). Four additional functions are defined in the example that we will use in the template file.

 

Functions:

  • getInfo() – displays customer name
  • getCartItemsCountText() – string with sample text, which includes the number of items in the shopping cart
  • getCartItemsCount() – integer that includes the number of the products in the shopping cart (NOT the total quantity, sum qty)
  • getHint() – string, only contains information

 

The data, returned by the aforementioned functions, will be managed by the Sample class described in the section “Creating and implementing our own CustomerData class”.

Let’s take a look at the content of the template file belonging to the block, located at app/code/Aion/Sample/frontend/templates/sidebar.phtml in our sample module. The file includes the following:

 

<?php
 $sampleHelper = $block->getExtensionHelper();
 $initFullName = $sampleHelper->getIsLoggedIn() ? $sampleHelper->getCustomerFullName() : __('You are logged out.');
 ?>
 <?php if ($sampleHelper->isEnabled()) : ?>
     <div class="block block-compare block-aion-sample" data-bind="scope: 'sample'">
         <div class="block-title">
             <strong><?php /* @escapeNotVerified */ echo __('Aion Sample Block'); ?></strong>
         </div>
         <div class="block-content">
             <strong class="subtitle" style="display: inline-block">
                 <?php /* @escapeNotVerified */ echo __('Customer Info:') ?>
             </strong>
             <p class="description">
                 <span data-bind="text: getInfo()"></span><br />
             </p>
             <p class="description">
                 <!-- ko if: getCartItemsCount() -->
                 <strong class="subtitle" style="display: inline-block">
                     <?php /* @escapeNotVerified */ echo __('Cart Info:') ?>
                 </strong>
                 <br />
                 <span data-bind="text: getCartItemsCountText()"></span>
                 <!-- /ko -->
             </p>
             <p class="hint"><small data-bind="text: getHint()"></small></p>
         </div>
     </div>
     <script type="text/x-magento-init">
     { "*": {
             "Magento_Ui/js/core/app": {
                 "components": {
                     "sample": {
                         "component": "Aion_Sample/js/view/sample-sidebar",
                         "initHint": "<?php echo __('(Refresh automatically after cart modification)') ?>",
                         "initFullname": "<?php echo $initFullName ?>"
                     }
                 }
             }
         }
     }
     </script>
 <?php endif; ?>

 

Basically, here we can see how Knockout.js is used. Let’s see in detail where we use it and how it works.

 

<span data-bind=”text: getInfo()”></span>

The string returned by the getInfo() function, defined in the javascript file, will appear in the <span> tag.

 

Knockout JS:

 

<!– ko if: getCartItemsCount() –>

               <strong class=”subtitle” style=”display: inline-block”>

                               <?php /* @escapeNotVerified */ echo __(‘Cart Info:’) ?>

               </strong>

              <br />

              <span data-bind=”text: getCartItemsCountText()”></span>

<!– /ko –>

 

If the value of the getCartItemsCount() function, defined in the javascript file, is not 0, then the part in the if section will appear. The getCartItemsCountText() function will fill the <span> tag with a string.

 

<p class=”hint”><small data-bind=”text: getHint()”></small></p>

The getHint() function will fill the <span> tag with a string.

 

Now let’s check how the finished block will show in the Magento 2.0 sidebar.

 

magento 2 module development sidebar

 

 

How can we modify these data with different user interactions? We get the answer in the next section.

 

 

Creating and implementing our own CustomerData class

 

In order to make the above block and its content modified interactively, we need a specific CustomerData class which provides the sample class, defined in the javascript, with data.

The class is located at app/code/Aion/Sample/CustomerData/Sample.php in our sample module. The file includes the following:

 

<?php
 namespace Aion\Sample\CustomerData;
 use Magento\Customer\CustomerData\SectionSourceInterface;
 use Aion\Sample\Helper\Data as DataHelper;
 /**
  * Sample section
  */
 class Sample implements SectionSourceInterface
 {
     /**
      * @var DataHelper
      *
     protected $_helper;
 
     /**
      * @param DataHelper $dataHelper
      */
     public function __construct(
         DataHelper $dataHelper
     ) {
         $this->_helper = $dataHelper;
     }
 
     /**
      * {@inheritdoc}
      */
     public function getSectionData()
     {
         $sampleData = $this->_getSampleData();

         return $sampleData;
     }
 
     /**
      * First sample data example
      *
      * @return array
      */
     protected function _getSampleData()
     {
         $sampleData = [
             'info' => __('You are logged out.')
         ];
         $isLoggedIn = $this->_helper->getIsLoggedIn();
         if ($isLoggedIn) {
             $sampleData = [
                 'info' => __('You are logged in as: %1', $this->_helper->getCustomerFullName())
             ];
         }
 
         $cartItemsCount = $this->_helper->getCartItemCount();
         $sampleData = array_merge(
             $sampleData,
             [
                 'cart_items_text' => __('You have %1 item(s) in your cart', $cartItemsCount),
                 'cart_count' => (int)$cartItemsCount,
                 'hint' => __('(Refresh automatically after cart modification)')
             ]
         );
 
         return $sampleData;
     }
 }

 

Within the class, the getSectionData() function provides the sample class, defined in the javascript, with data. Here it’s important to note that it’s worth giving the two classes the same name in the PHP and Javascript codes.

 

_getSampleData returns with an array whose keys are identical to the values used in the javascript code. Here we implement the necessary business logic. In this sample we see that only the whole name of the customer is to be transmitted and the number of cart items are to be calculated.

 

It is also necessary to define the CustomerData class to let the Magento 2.0 system “know” about it. This we need to set in the di.xml file which can be found under app/code/Aion/Sample/etc/frontend/di.xml in our sample module. The file includes the following:

 

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
     <type name="Magento\Customer\CustomerData\SectionPoolInterface">
         <arguments>
             <argument name="sectionSourceMap" xsi:type="array">
                 <item name="sample" xsi:type="string">Aion\Sample\CustomerData\Sample</item>
             </argument>
         </arguments>
     </type>
 </config>

 

The next step is to define when and for what kind of user interactions the getSectionData() function, defined in the Sample class, should run. For this we need to provide the list of controllers in an xml file.

 

This we need to define in the sections.xml file, located at app/code/Aion/Sample/etc/frontend/sections.xml in our sample module. The file includes the following:

 

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
     <action name="checkout/cart/add">
         <section name="sample"/>
     </action>
     <action name="checkout/cart/delete">
         <section name="sample"/>
     </action>
     <action name="checkout/cart/updatePost">
         <section name="sample"/>
     </action>
     <action name="checkout/cart/updateItemOptions">
         <section name="sample"/>
     </action>
     <action name="checkout/sidebar/removeItem">
         <section name="sample"/>
     </action>
     <action name="checkout/sidebar/updateItemQty">
         <section name="sample"/>
     </action>
 </config>

 

We can see in the xml what controller actions we have defined. These refer to adding the product to the cart, deleting it, updating the number of products etc.

 

What does it mean exactly?

 

After we have finished with the above Sample class, the aforementioned getSectionData() function will run provided these controller actions are run, and our block, displayed on frontend will be updated by ajax with the data returned by the getSectionData() function.

All this happens without defining any ajax function or management in our javascript file.

Our block looks like this, after logging in with a user account and adding a random product to the cart:

 

magento 2 module development cart

 

The content of the object returned in the ajax call:

 

magento 2 module development ajax content

 

Using Knockout.js as an HTML template file

 

In the previous example we used the Knockout.js directly in the phtml file.

Here we are going to apply it as well, but we will use a separate html file for displaying the data.

To achieve this, first we create a second phtml file. This file in our module is located here: app/code/Aion/Sample/view/frontend/templates/second-sidebar.phtml. The file includes the following:

 

<?php
 $sampleHelper = $block->getExtensionHelper()
 ?>
 <?php if ($sampleHelper->isEnabled()) : ?>
     <div class="block block-compare block-aion-sample" data-bind="scope: 'sample'">
         <div class="block-title">
             <strong><?php /* @escapeNotVerified */ echo __('Aion Second Sample Block'); ?></strong>
         </div>
         <div class="block-content">
             <!-- ko template: getTemplate() -->
             <!-- /ko -->
         </div>
     </div>
     <script type="text/x-magento-init">
     {
         "*": {
             "Magento_Ui/js/core/app": {
                 "components": {
                     "sample": {
                         "component": "Aion_Sample/js/view/sample-sidebar",
                         "initSampleData": "<?php echo addslashes(json_encode($block->getInitSecondItems())) ?>"
                     }
                 }
             }
         }
     }
     </script>
 <?php endif; ?>

 

Compared to the first phtml file, the difference is clearly visible. Getting the html template is implemented in the content part:

<!– ko template: getTemplate() –>

<!– /ko –>

 

Here it is important to get back to the sample-sidebar.js file, mentioned at the beginning of this post, and take the //Second sample code parts into account because our second sample is handled by these. Besides, we use only the second block in the default.xml file, described in the 1st section:

<block class=”Aion\Sample\Block\Sample” name=”aion.sample.knockout.sidebar.second” template=”Aion_Sample::second-sidebar.phtml” after=”wishlist_sidebar”/>

 

We comment out the first.

Next we create the HTML file: app/code/Aion/Sample/view/frontend/web/template/second-sidebar.html. The file includes the following:

 

<!-- ko if: displayContent() -->
     <p data-bind="text: someText"></p>
 
     <h4>Items Form Block Class</h4>
     <ul data-bind="foreach: sampleItems">
         <li>
             <span data-bind="text: $data"> </span>
         </li>
     </ul>
 
     <h4>Months</h4>
     <ul data-bind="foreach: months">
         <li>
             <span data-bind="text: $data"> </span>
         </li>
     </ul>
 
     <h4>Categories</h4>
     <ol data-bind="foreach: { data: categories, as: 'category' }">
         <li>
             <ul data-bind="foreach: { data: items, as: 'item' }">
                 <li>
                     <span data-bind="text: category.name"></span>:
                     <span data-bind="text: item"></span>
                 </li>
             </ul>
         </li>
     </ol>
 
     <h4>My Items</h4>
     <ul>
         <!-- ko foreach: myItems -->
         <li>Item name: <span data-bind="text: $data"></span></li>
         <!-- /ko -->
     </ul>
 
 <!-- /ko -->
 <!-- ko ifnot: displayContent() -->
     <p class="empty-text" data-bind="text: $t('Content is empty.')"></p>
 <!-- /ko -->

 

By analysing the sample-sidebar.js, let’s look at how the HTML template file works.

First the template file itself and a true value is defined, which is present in the if condition. Of course, it can be modified according to the required business logic.

 

        // Second example

        defaults: {

            template: ‘Aion_Sample/second-sidebar’

        },

        displayContent: ko.observable(true),

 

The functions that provide the sample data will run automatically during initialization:

 

           // Second example

            this.someText = $t(‘Sample content with template.’);

            // Second example. foreach examples

            this._showItems();

            // Second example, foreach example

            this._showMonths();

            // Second example, other foreach example

            this._showCategories();

            // Second example and another foreach example

            this._showMyItems();

 

For the sake of the example, there’s only one out of these whose data are received from the block class (app/code/Aion/Sample/Block/Sample.php).

 

The transfer is implemented in the phtml file:

 

“initSampleData”: “<?php echo addslashes(json_encode($block->getInitSecondItems())) ?>”

 

The getInitSecondItems() function is created in the helper class:

 

/**
  * Get sample product names
  *
  * @return array
  */
 public function getSampleProductNames()
 {
     $sampleData = [];
     /* @var $product Product */
     $product = $this->_productFactory->create();
     /* @var $collection Collection */
     $collection = $product->getCollection();
     $collection->setVisibility($this->_catalogProductVisibility->getVisibleInCatalogIds());
     $collection->addStoreFilter()->addAttributeToSelect(
         ['name']
     );
     $collection->getSelect()->orderRand('e.entity_id');
     $collection->setPageSize(
         5
     )->setCurPage(
         1
     )->toArray(['name']);
 
     /* @var $item Product */
     foreach ($collection as $item) {
         $sampleData[] = $item->getName();
     }
 
     return $sampleData;
 }

 

 

It selects five out of the displayed products randomly and transmits their names as a single array.

 

The rest of the functions, initialized in the sample-sidebar.js, create simple sample data in order to help understand the operations by iterating all through these in the HTML template file. Our second sample block will appear on frontend the following way:

 

magento 2 module development frontend

 

SummaryThis post showed you how to use Knockout.js with custom module development. In Magento 2.0, it is applied in various cases, displaying customer, order, cart, wishlist and other data on frontend.

 

See also Part 1 (how to start), Part 2 (create admin grid)Part 3 (observers).

 

 

Attila Sági

Attila Sági

Backend Developer

Attila Sági is a very experienced senior developer having a Magento Developer Plus certification. He started developing custom Magento stores and extensions in 2008. As one of the first members in the AionHill team, he developed extensions under the public alpha and beta versions of Magento 2 before its official launch. Beyond his development duties, he also trains colleagues with special regard to the Magento 2 system.


NEED A RELIABLE, PROFESSIONAL MAGENTO DEVELOPMENT PARTNER?

Contact us if you have any question or requirement related to the preparation of a new or renewal of an existing online store.

magento_request
Do you need our support?
  • Magento Site Check
  • Magento Code Audit
  • Magento SEO Audit
  • Magento Project Rescue
Request help
Next