Magento 2 module development – a comprehensive guide – Part 4 (Knockout.js)
Contents:
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.
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.

<!– 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.
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:
The content of the object returned in the ajax call:
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:
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; }/** * Get sample product names * * @return array */
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:
See also Part 1 (how to start), Part 2 (create admin grid), Part 3 (observers).