Using KnockOut JS in Magento 2
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:
- Create a new block template file (phtml) in your module of choice (our example will use the
Magento_Catalog
module) - Update your module XML file to place the block template you created in step 1 into you Magento 2 store
- 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
- Create the JS component file
- 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.