Using KnockOut JS in Magento 2

By

Magento 2 has undergone a lot of changes of late. Whatever your views on the updates, the switchover to using Knockout JS has surely been one of the best decisions. Sure, it’s not perfect, but it provides a great way to create interactive frontend data bound components within your Magento 2 store.

This, as a developer, allows you to add a great deal of seamless functionality to your store. It makes developing on Magento 2 more interesting – and infinitely more powerful.

Many people new to Magento 2 who've read about Knockout JS will think it’s just confined to the new checkout (and that is where the main chunk of Knockout JS is used in M2). In reality, Knockout JS can be used everywhere within the frontend to create anything from a custom colour picker, to your own custom image viewer.

So now we know how Knockout JS can be used within Magento 2, let’s jump into a quick example of how we setup a basic Component. If you want to read more about how the layout renderer works in Magento 2, take a look at my previous article on the Magento 2 checkout.

Create a component

In this example we’ll be defining a component outside the checkout (see my previous article for more on integrating a component into the checkout). There are five steps to getting a working component:

  1. Create a new block template file (phtml) in your module of choice (our example will use the Magento_Catalog module)
  2. Update your module XML file to place the block template you created in step 1 into you Magento 2 store
  3. Define your KO component initialisation code within the new block template file –passing in where it will render / its scope and any other parameters you need to pass into the component, and defining the location of the component JS file
  4. Create the JS component file
  5. Create the html template for your component

So let’s go through each of these steps one-by-one.

Create a new phtml template file

As mentioned, we'll be hijacking the Magento_Catalog module to create our new component, and will be creating this in our own theme. If you need to learn how to create your own theme in Magento 2, please refer to the documentation on the Magento 2 dev docs site.

So, in your own theme, let’s create our new phtml file and add a block via XML into the catalog page. Create the file here:

app/design/frontend/<your_package>/<your_theme>/Magento_Catalog/templates/newko-component.phtml

The file should just be blank for now.

Tip: if you want to add in an <h1> tag just to make sure the file includes correctly, then that’s also fine.

We also now need to place this phtml block into our catalog page. To do this we just need to update our XML file for the category page.

Create an XML file like so:

app/design/frontend/<your_package>/<your_theme>/Magento_Catalog/layout/catalog_category_view.xml

 

<?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="content">
        <block class="Magento\Framework\View\Element\Template" name="new.ko.component" template="Magento_Catalog::newko-component.phtml" before="category.products" />
      </referenceContainer>
    </body>
</page>

This should now include our phtml file into the category page at the top of the content before any product listings.

Define KO initialisation code

Now we’ve included the file block on category page we need to initialise and create our new component. So if we reopen / go back to the phtml file we just made, we need to add the following:

<div id="m2-component" data-bind="scope:'m2kocomponent'">
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">
    {
        "#m2-component": {
            "Magento_Ui/js/core/app": {
               "components": {
                    "m2kocomponent": {
                        "component": "Magento_Catalog/js/m2kocomponent",
                        "template" : "Magento_Catalog/m2kocomponent-template"
                    }
                }
            }
        }
    }
    </script>
</div>

Taking a closer look at this, Magento parses the script contents within the <script type="text/x-magento-init">. The first key it looks at is the ID the component is set to be initialised on. In our case if you look at the HTML code above the ID of the div matches the key of the JS object we have defined within the <script> tag.

It then looks for the list of components that need to be initialised on this div within the"components" key as it can be more than one. As you can see, the component to be intialised is the 'm2kocomponent' where we define the location of the component javascript file via thecomponent key, and also the template. Although we are defining the template here, you can also define one in the component JS file. The one defined here will override the one defined in the component file itself.

Create the JS component file

Our next step is to create the actual component file. To do this we need to create a JS file at the following location:

app/design/frontend/<your_package>/<your_theme>/Magento_Catalog/web/js/m2kocomponent.js

With the following contents:

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

There is a 'parent' component from which all other KO components must be extended, and this is injected in the file above via the alias uiComponent. We can then extend from this, and initialise our component by defining the intialize function and calling this._super();which calls the initialisation code of the parent component from which this is extended.

Now we have the JS portion of our component, we need to create the template file for it.

Create the HTML template file

The last step is to create the actual template file for the component. We already defined the name of this before, but now we need to create it in the following location:

app/design/frontend/<your_package>/<your_theme>/Magento_Catalog/web/template/m2kocomponent-template.html

Note: this MUST be made with the .html extension.

<div class="component-wrapper">
    <div data-bind="text: 'Catalog Timer'"></div>
</div>

If everything has worked if you clear your varnish cache along with the main Magento 2 cache you should see some text at the top of your category page which says 'Catalog Filter'.

Looking in greater detail at KO

Now we have a working component setup we can start to have a look at how powerful KO JS is within the context of Magento 2.

KO observables

The most useful feature in Knockout JS is the observable along with observable arrays. It enables you to have dynamic data-binding with variables which are immediately updated in the template when the value within the component changes. An observable array acts in the same way, and like the normal observable you can subscribe to the change events and create logic within your component when the values change.

So let’s go about updating our component and template to test out an observable.

In our component file we need to add a new key attached to the component object and assign its value as an observable with an initial value of 0. This allows us to access the myTimer value in the template.

define(['jquery', 'uiComponent', 'ko'], function ($, Component, ko) {
        'use strict';
        return Component.extend({
            myTimer: ko.observable(0),
            initialize: function () {
                this._super();
            }
        });
    }
);

We now need to update our template to display our new myTimer variable.

<div class="component-wrapper">
    <div data-bind="text: 'Catalog Timer'"></div>
    <div data-bind="text: myTimer"></div>
</div>

Unlike some applications where you might use the {{myTimer}} notation to display the parsed value of the variable, KO JS uses the data-bind attribute and places the output of this (you can also create simple logic in here) into the element it is declared in.

So in this case:

<div data-bind="myTimer"></div>

will be parsed and changed to:

<div>0</div>

So all of that is great, but it doesn’t really show the data-binding aspect of the observable. Let’s create a timer loop within our component which updates the value every second.

define(['jquery', 'uiComponent', 'ko'], function ($, Component, ko) {
        'use strict';
        
        var self;
        return Component.extend({
            myTimer: ko.observable(0),
            initialize: function () {
                self = this;
                this._super();
                //call the incrementTime function to run on intialize
                this.incrementTime();
            },
            //increment myTimer every second
            incrementTime: function() {
                var t = 0;
                setInterval(function() {
                    t++;
                    self.myTimer(t);
                }, 1000);
            }
        });
    }
);

Notice here that we set the observable value by passing the new value into the observable as we would pass a new value into a function. If you assign a new value to the myTimer as you normally would, it will replace the observable with the value you just assigned, losing its functionality.

So if you clear your cache and refresh the page you will see that the myTimer value is updated every 1000ms on the template. Pretty nice, although this is a very basic example. It’s extremely useful to be able to update values in your JS component and see the results instantly in the template.

Note: another quick note on observables is that if you just want to get the actual value logged out or assigned somewhere, you need to add parantheses to the variable in order to grab the value:

console.log(myTimer); //this would return a function (the observable)
console.log(myTimer()); // this returns the actual value of the observable

Subscribe to observables

You can also subscribe to observables, meaning that when the value of them is changed, an event is fired and you can hook into this in order to run some kind of logic within another function or multiple functions. So, for our example, we are going to update the colour of the timer text to a random colour every time the myTimer variable updates.

define(['jquery', 'uiComponent', 'ko'], function ($, Component, ko) {
        'use strict';
        
        var self;
        return Component.extend({
            myTimer: ko.observable(0),
            randomColour: ko.observable("rgb(0, 0, 0)"),
            initialize: function () {
                self = this;
                this._super();
                //call the incrementTime function to run on intialize
                this.incrementTime();
                this.subscribeToTime();
            },
            //increment myTimer every second
            incrementTime: function() {
                var t = 0;
                setInterval(function() {
                    t++;
                    self.myTimer(t);
                }, 1000);
            },
            subscribeToTime: function() {
                this.myTimer.subscribe(function(newValue) {
                    console.log(newValue);
                    self.updateTimerTextColour();
                });
            },
            randomNumber: function() {
                return Math.floor((Math.random() * 255) + 1);
            },
            updateTimerTextColour: function() {
                //define RGB values
                var red = self.randomNumber(),
                    blue = self.randomNumber(),
                    green = self.randomNumber();
                    
                self.randomColour('rgb(' + red + ', ' + blue + ', ' + green + ')');
            }
        });
    }
);

So let’s take a look at the code in more detail.

We have added the subscribeToTime function which is called on initialize which subscribes to our myTimer observable. Every time that value is updated, the subscribe function is invoked and, in our case, it console.log's the value of the new myTimer value as well as generating a random colour for the timer text via the new updateTimerTextColour function.

In order for us to see these changes we need to update our template as follows:

<div class="component-wrapper">
    <div data-bind="text: 'Catalog Timer'"></div>
    <div data-bind="text: myTimer, style: {color: randomColour}"></div>
</div>

As you can see, we can bind multiple events to an element with KO JS and the data-bind syntax by simply separating them with a comma. In the new template above we’ve simply used the native style event which will change the style of the div by assigning our new randomColor observable (generated with updateTimerTextColour) to the color attribute.

Once you’ve updated both the JS component file and the template file, clear all your caches and refresh the page to see if it’s worked!

More complex logic

Obviously observables are great if you want deal with strings or boolean values, but what about more complex logic?

KO JS provides us with the computed method which allows us to create an observable based on the logic returned within the computed function. This can be a combination of anything including normal observables or simple string values.

So let’s improve our code so that the colour of the timer updates when just one or more of the RGB values changes.

define(['jquery', 'uiComponent', 'ko'], function ($, Component, ko) {
    'use strict';
    var self;
    return Component.extend({
        myTimer: ko.observable(0),
        red: ko.observable(0),
        blue: ko.observable(0),
        green: ko.observable(0),
        initialize: function () {
            self = this;
            this._super();
            //call the incrementTime function to run on intialize
            this.incrementTime();
            this.subscribeToTime();
            this.randomColour = ko.computed(function() {
                //return the random colour value
                return 'rgb(' + this.red() + ', ' + this.blue() + ', ' + this.green() + ')';
            }, this);
        },
        //increment myTimer every second
        incrementTime: function() {
            var t = 0;
            setInterval(function() {
                t++;
                self.myTimer(t);
            }, 1000);
        },
        subscribeToTime: function() {
            this.myTimer.subscribe(function(newValue) {
                console.log(newValue);
                self.updateTimerTextColour();
            });
        },
        randomNumber: function() {
            return Math.floor((Math.random() * 255) + 1);
        },
        updateTimerTextColour: function() {
            //define RGB values
            /*notice we now no longer have to set and return the RBG style code here
             we simply update the red/blue/green observables and the computed observable
             returns the style element to the template */
            this.red(self.randomNumber());
            this.blue(self.randomNumber());
            this.green(self.randomNumber());
        }
    });
});

With the new code this also means we can just update one colour observable from anywhere else in the code base and the colour will automatically be updated on the page. Pretty cool!

Share data between components

In Magento 2, all the variables defined within our component are restricted to the scope of that component. So how do we share data between components?

The solution for this in Magento 2 is to create a storage model, which is just a simple Javascript object, which sits above the components, but is not a component itself. It can then be injected into the component – and aliased – where it's needed and can be used.

Remember as well that because Javascript objects are passed by reference, if you update the observable (via its alias) in your component, it will also affect the value in the original object where it’s defined – in this case, the model.

Therefore by sharing the variables via the model, if it’s updated, that update will be reflected in every component where that variable is used (assuming it’s been defined and shared via a model).

So let’s take a look at a simple example by moving the RGB values into a simple storage model. We first need to create a new JS file in the following location:

app/design/frontend/<your_package>/<your_theme>/Magento_Catalog/web/js/model/rgb-model.js

 

define(
    ['ko'],
    function (ko) {
        'use strict';
        var red = ko.observable(0);
        var blue = ko.observable(0);
        var green = ko.observable(0);
        function randomNumber() {
            return Math.floor((Math.random() * 255) + 1);
        }
        function updateColour() {
            red(randomNumber());
            blue(randomNumber());
            green(randomNumber());
        }
        return {
            randomNumber: randomNumber,
            updateColour: updateColour,
            red: red,
            blue: blue,
            green: green
        };
    }
);

Here we assign a ko.observable value to each colour and return each of these values aliased with the same name as the local variables. We’ve also moved some of the functions associated with setting a new version of these colours to the model and renamed them to make more sense now they are in the model.

So, to access these values, we need to inject this model into our component. Here’s what our component file should look like now.

define(['jquery', 'uiComponent', 'ko', 'Magento_Catalog/js/model/rgb-model'], function ($, Component, ko, rgbModel) {
        'use strict';
        
        var self;
        return Component.extend({
            myTimer: ko.observable(0),
            randomColour: ko.computed(function() {
                //we are using the aliased rgbModel here giving us access to the RGB values
                return 'rgb(' + rgbModel.red() + ', ' + rgbModel.blue() + ', ' + rgbModel.green() + ')';
            }, this),
            initialize: function () {
                self = this;
                this._super();
                //call the incrementTime function to run on intialize
                this.incrementTime();
                this.subscribeToTime();
            },
            //increment myTimer every second
            incrementTime: function() {
                var t = 0;
                setInterval(function() {
                    t++;
                    self.myTimer(t);
                }, 1000);
            },
            subscribeToTime: function() {
                this.myTimer.subscribe(function() {
                    rgbModel.updateColour();
                });
            }
        });
    }
);

As you can see, splitting out the RGB values into a new model makes the code a lot cleaner and provides much cleaner code, making the component and the logic around changing the RGB values much easier to understand. We can also now access these values and the functions for generating a new colour from anywhere by simply injecting the model into whatever component where we might need them…cool!

Wrapping up

Magento 2 and KO JS provide us with a great deal of flexibility in order to create rich components which can really enhance the user experience of the end user. Hopefully this guide helps you understand Magento 2’s implementation of components and gives you a nice introduction to KO JS. If you want to read more about Knockout JS, visit the official site.