Magento UI components are used to represent distinct UI elements, such as tables, buttons, dialogs, and others. It uses the Knockout JS library to bind data to certain DOM elements on the page which then gets displayed to the user. Magento UI components are used in various areas across the store front, the main area (and most well known) has to be the Checkout page, but it can be used anywhere to create custom functionality.
What is Knockout JS?
As briefly mentioned above Knockout JS is a library that makes it easier to create rich, responsive user interfaces with JavaScript and HTML. It uses the MVVM pattern to bind data to certain DOM elements on the page, when that data changes it automatically updates on the page to the user, thus making it reactive – if you’re familiar with Vue.js it feels like a very early adoption of that.
Within Magento there are a couple of different files involved. First we have a view-model which is our JavaScript file and then we have the template which is a .html file (not .phtml). The data in the HTML file is bound to the view-model which means when the data changes it updates in the template file.
Basic Example
First step is to create our .phtml file that will house our UI component. This is the same as regular frontend templating, nothing new here.
Within your theme create a new file:
<ThemeName>/Magento_Theme/templates/example.phtml
Then include the template via an xml block, in this example I will add it to the homepage but feel free to add it anywhere you want.
<ThemeName>/Magento_Cms/layout/cms_index_index.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 name="example" before="-" template="Magento_Theme::example.phtml" />
</referenceContainer>
</body>
</page>
It’s a good time to check the template is loading on the frontend before continuing. You could add some text to see if it displays and debug accordingly.
Now we have the template in place add the contents to the file:
<div id="selector">
<h1>Hello World!</h1>
</div>
<script type="text/x-magento-init">
{
"#selector": {
"Magento_Ui/js/core/app": {
"components": {
<!-- add ui component -->
}
}
}
}
</script>
This is a very basic scaffold for our UI component. Within the script we first start with our selector (asterisk also works if you don’t want to use a selector) and then we load the Magento UI component library and add in the nested components object. From here we add in our UI components.
Now you have the basic understanding of the structure amend the code to read the following:
<div id="selector" data-bind="scope: 'knockout-example'">
<h1 data-bind="text: heading"></h1>
<!-- ko template: getTemplate() --><!-- /ko -->
</div>
<script type="text/x-magento-init">
{
"#selector": {
"Magento_Ui/js/core/app": {
"components": {
"knockout-example": {
"component": "Magento_Theme/js/knockout-view-model",
"config": {
"template": "Magento_Theme/knockout-template",
"heading": "Hello World!"
}
}
}
}
}
}
</script>
Within the script we have now added our example component called knockout-example
and setup the view-model, template file and static heading.
Finally we added the data-bind=""
Knockout JS syntax to scope our component to our DOM element by passing our component name and then by using the Knockout JS syntax we include the HTML template file.
The template
is optional and isn’t necessary with some smaller UI components but we have included it to demonstrate its use along with the heading
. The component
view-model is required.
Note: the scope binding is not built into Knockout JS – this is Magento specific and is something they have added in. The scope
binding looks for and loads a registered view-model and applies this to the DOM node where you have the scope
binding.
Create view-model
Now we have the block template in place we can create the view-model JavaScript file.
<ThemeName>/Magento_Theme/web/js/knockout-view-model.js
Add the contents to the file:
define([
'uiComponent',
'ko'
], function (Component, ko) {
'use strict';
return Component.extend({
<!-- add functionality -->
});
});
This is a very basic scaffold to demonstrate how the file should be setup. We are loading uiComponent and Knockout JS as dependencies and then extending the uiComponent library to add in our functionality.
Now amend the code to read the following:
define([
'uiComponent',
'ko'
], function (Component, ko) {
'use strict';
return Component.extend({
defaults: {
template: 'Magento_Theme/knockout-template',
heading: 'Default Heading Text',
},
counter: ko.observable(0),
initialize: function () {
this._super();
this.initCounter();
},
initCounter: function () {
var self = this;
var num = 0;
setInterval(function () {
self.counter(num++);
}, 1000);
}
});
});
The initialize
method is automatically called when the UI component gets loaded, within this method we are calling our own custom method initCounter
as we want it to run immediately.
The initCounter
method updates the counter variable every second to demonstrate how Knockout JS reactivity works. For something to be reactive we have to tell Knockout JS to watch it for changes, to do this we can use ko.observable()
and set a default value of 0.
You can read more about observables in their documentation.
Also notice we have added a default template and heading, if this gets missed in the script configuration it will fallback to these defaults.
Create template
Finally we can create the Knockout JS HTML file.
<ThemeName>/Magento_Theme/web/template/knockout-template.html
Add the contents to the file:
<div>
Counter: <span data-bind="text: counter"></span>
</div>
Here we are using the data-bind
syntax to bind the counter variable to our DOM element. Now if you refresh the browser you should see the counter incrementing by itself every second.
You have now successfully created a UI component!
To recap the UI component itself only needed two files but in total we used 4 files so that it renders onto the page:
# XML block to render .phtml file
<ThemeName>/Magento_Cms/layout/cms_index_index.xml
# Magento .phtml template file
<ThemeName>/Magento_Theme/templates/example.phtml
# Knockout JS View Model
<ThemeName>/Magento_Theme/web/js/knockout-view-model.js
# Knockout JS template
<ThemeName>/Magento_Theme/web/template/knockout-template.html
XML Configuration
To improve things we can move the UI component configuration into the XML file by using getJsLayout()
which is a common method used across Magento.
First amend the .phtml file to the following:
<div id="selector" data-bind="scope: 'knockout-example'">
<h1 data-bind="text: heading"></h1>
<!-- ko template: getTemplate() --><!-- /ko -->
</div>
<script type="text/x-magento-init">
{
"#selector": {
"Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?>
}
}
</script>
Then within your XML file edit the block to the following:
<?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 name="example" before="-" template="Magento_Theme::example.phtml">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="knockout-example" xsi:type="array">
<item name="component" xsi:type="string">Magento_Theme/js/knockout-view-model</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">Magento_Theme/knockout-template</item>
<item name="heading" xsi:type="string">Hello World!</item>
</item>
</item>
</item>
</argument>
</arguments>
</block>
</referenceContainer>
</body>
</page>
The jsLayout
argument is used to specify the component details. This will work in the same manner as before but I find it keeps the code a bit cleaner and more maintainable.
This was a brief introduction into UI components and the Knockout JS library. The reactivity of Knockout JS is very powerful, we only touched on observables but there is also observable arrays and computed observables for more complex functionality – you can see the relativeness to Vue.js as they have something similar called computed properties!