Introduction
Getting used to the Magento way of handling Knockout.js via XMLs is not for the faint of heart. Especially considering the often inadequate documentation when dealing with rarely performed and custom adjustments.
This guide provides an example of how to add Knockout.js variables to the Magento 2 minicart. It covers the process for both the minicart content and a nested area, such as the subtotal region.
Getting an overview
Since we are dealing with the minicart, the bulk of the layout is defined in the vendor/magento/module-checkout/view/frontend/layout/default.xml
file.
<referenceContainer name="header-wrapper">
<block class="Magento\Checkout\Block\Cart\Sidebar" name="minicart" as="minicart" after="logo" template="Magento_Checkout::cart/minicart.phtml">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="types" xsi:type="array"/>
<item name="components" xsi:type="array">
<item name="minicart_content" xsi:type="array">
<item name="component" xsi:type="string">Magento_Checkout/js/view/minicart</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">Magento_Checkout/minicart/content</item>
</item>
<item name="children" xsi:type="array">
<item name="subtotal.container" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="config" xsi:type="array">
<item name="displayArea" xsi:type="string">subtotalContainer</item>
</item>
<item name="children" xsi:type="array">
<item name="subtotal" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">Magento_Checkout/minicart/subtotal</item>
</item>
</item>
</item>
</item>
<item name="extra_info" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="config" xsi:type="array">
<item name="displayArea" xsi:type="string">extraInfo</item>
</item>
</item>
<item name="promotion" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="config" xsi:type="array">
<item name="displayArea" xsi:type="string">promotion</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
<container name="minicart.addons" label="Mini-cart promotion block"/>
</block>
</referenceContainer>
The header-wrapper
in line 2 is the <referenceContainer>
of interest, as it holds the minicart as an <block>
element of Sidebar
type.
Now onto our regions, which we would like to modify: The content and the subtotal regions. The content component is defined in line 8 and its template in line 10. But what does that mean now?
Well it means that Magento_Checkout/js/view/minicart
is the uiComponent
, which is used to extent its parent. The data within this component will then be shared to its template Magento_Checkout/minicart/content
.
Now keep in mind that Magento_Checkout/js/view/minicart
does not have to be a path strictly speaking. It could also be “rerouted” or “mapped” in the requirejs-config.js
file, to another actual file location.
When everything’s resolved, these “labels” then refer to these files:
vendor/magento/module-checkout/view/frontend/web/js/view/minicart.js
and vendor/magento/module-checkout/view/frontend/web/template/minicart/content.html
The same logic applies to the subtotal component defined in line 20 and its template in line 22. Now here it is important to note, that there is no dedicated uiComponent
specified as a path, but simply a string that says “uiComponent
“.
You can understand “uiComponent
” as a base component where other files can then extend from. If there is no specific file mentioned, the base component is then used here.
Alright, with this basic knowledge out of the way, let’s get onto extending both uiComponents
and using the extended data in their respective templates.
Preparing our work area
But before extending the uiComponents
, we first have to create a “platform” from where we can extend from. For that, please set up your own own module with the following base structure:
app
└── code
└── <VENDOR>
└── <MODULE>
├── registration.php
├── etc
│ └── module.xml
└── view
└── frontend
├── web
│ ├── js
│ │ └── view
│ │ └── minicart.js
│ └── template
│ └── minicart
│ └── content.html
└── requirejs-config.js
The registration.php
:
<?php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
"<VENDOR>_<MODULE>",
__DIR__
);
The module.xml
:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="<VENDOR>_<MODULE>" setup_version="1.0.0">
</module>
</config>
The remaining requirejs-config.js
, minicart.js
and content.html
files will be be dealt with in the next step.
Extending the content region
Alright. We’ll be starting with the easier component first.
For the content component, I’d like to introduce two variables, which will then be used in the template as onclick
attributes for buttons. It basically boils down to a simple passing of variables like this:
uiComponent ──► HTML Template
Instead of using the previous native content component and its template (remember line 8 and 10 from default.xml
?), we now have to use custom ones. There is a need to extend the Magento_Checkout/js/view/minicart
component, and this is precisely where Mixins come into play. To proceed, continue editing the requirejs-config.js
file:
var config = {
config: {
"mixins": {
"Magento_Checkout/js/view/minicart": {
"<VENDOR>_<MODULE>/js/view/minicart": true
}
}
}
};
Here we define <VENDOR>_<MODULE>/js/view/minicart
as our own component which is going to extend the native Magento_Checkout/js/view/minicart
one.
Again, this naming could also be used as a label for a completely new path using mapping.
var config = {
config: {
"mixins": {
"Magento_Checkout/js/view/minicart": {
"<VENDOR>_<MODULE>/js/view/minicart": true
}
}
},
map: {
"*": {
"<VENDOR>_<MODULE>/js/view/minicart": "<VENDOR>_<MODULE>/assets/js/default/minicart",
}
}
};
Using the map
keyword, <VENDOR>_<MODULE>/js/view/minicart
now resolves to the path of view/frontend/web/assets/js/default/minicart.js
. But that’s only a optional sidenote to keep in mind.
Now that our custom minicart.js
is being invoked, let’s proceed with it:
define([
"uiComponent"
], function(Component) {
"use strict";
return function(Component) {
return Component.extend({
initialize: function() {
this._super();
this.checkout_main_page = "window.location.href = '/checkout'";
this.checkout_cart_page = "window.location.href = '/checkout/cart'";
return this;
}
});
}
});
Here, you can see that the uiComponent
is called as a dependency and is extended within the returning function. Also, take note of the two variables, checkout_main_page
and checkout_cart_page
, that I mentioned earlier, as these variables are now available in our HTML template:
<some_HTML_before>
<button class="btn" data-bind="attr: {onclick: checkout_main_page}, i18n: 'Go to Checkout'"></button>
<button class="btn" data-bind="attr: {onclick: checkout_cart_page}, i18n: 'View Shopping Cart'"></button>
<some_HTML_after>
And that’s essentially the simplest way to pass custom variables into your Knockout.js templates. However, as you may have noticed, we are defining our variables directly within the uiComponent
rather than fetching them dynamically from an external source. So, how can we handle this situation?
Extending the subtotal region
Let’s address this scenario using the subtotal example. In this case, I want to retrieve a setting from the Magento adminhtml
area using a dedicated class and then pass that value to the uiComponent
for the template to render. Essentially, the process will look like this:
Plugin Class ──► Document Body ──► JS Variable ──► uiComponent ──► HTML Template
Again, the same rules for extending the uiComponent
apply here. However, there is a hurdle:
<item name="children" xsi:type="array">
<item name="subtotal" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="config" xsi:type="array">
<item name="template" xsi:type="string">Magento_Checkout/minicart/subtotal</item>
</item>
</item>
</item>
Instead of a clear component name that we can reference in the requirejs-config.js
file, we are now dealing with a generic base uiComponent
. This leaves us with nothing specific to latch onto. But fear not, there is a simple solution: We simply define our own component which is extending from the base uiComponent
directly.
To make this work, we need to open our custom default.xml
layout file, as we need to modify the XML structure accordingly.
<?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="header-wrapper">
<referenceBlock name="minicart">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="minicart_content" xsi:type="array">
<item name="children" xsi:type="array">
<item name="subtotal.container" xsi:type="array">
<item name="children" xsi:type="array">
<item name="subtotal" xsi:type="array">
<item name="component" xsi:type="string"><VENDOR>_<MODULE>/js/view/minicart/subtotal</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</referenceContainer>
</body>
</page>
Note that we use the usual <referenceContainer>
and <referenceBlock>
tags to traverse the structure down to our target that we would like to override. In this case, it’s the subtotal component. Instead of dealing with a generic uiComponent
, we now define a specific component subtotal
that we can work with directly.
The subtotal component is now referenced as <VENDOR>_<MODULE>/js/view/minicart/subtotal
, but it can also be mapped to a different location using the requirejs-config.js
file if needed. Just note, that in this case you don’t need to create a mixin, as we are extending from the base uiComponent
directly this time.
Proceed now with creating the subtotal
component:
define([
"uiComponent",
"ko"
], function(Component, ko) {
"use strict";
return Component.extend({
initialize: function() {
this._super();
this.shipping_terms_page = ko.observable(this.getShippingTermsPage());
return this;
},
getShippingTermsPage: function() {
return window.checkout.shippingTermsPage || "#";
}
});
});
The variable available to the template is called shipping_terms_page
, and it is of the Knockout.js observable
type. It is retrieved from the internal getShippingTermsPage()
function, which, in turn, fetches the value from the window
JavaScript context.
Onto our subtotal
template then:
<div class="subtotal">
<div class="total-text">
<span class="label">
<!-- ko i18n: 'Total:' --><!-- /ko -->
</span>
<span>
<span data-bind="i18n: 'excl.'"></span>
<span></span>
<a target="_blank" data-bind="attr: {href: shipping_terms_page}, i18n: 'Shipping Fees'"></a>
</span>
</div>
<!-- ko foreach: elems -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- /ko -->
</div>
Great! Now the shipping_terms_page
variable is successfully fetched from an external source and placed inside a custom template. So far, so good. But you might be wondering where the value of window.checkout.shippingTermsPage
(our custom data) comes from, since it isn’t normally available.
The shipping_terms_page
variable actually originates from a setting in the Magento adminhtml area. A Plugin fetches this setting by hooking into the method of the Sidebar
class, where the original data is assembled. Keep in mind that Sidebar
serves as the layout block class for our minicart block.
<referenceContainer name="header-wrapper">
<block class="Magento\Checkout\Block\Cart\Sidebar" name="minicart" as="minicart" after="logo" template="Magento_Checkout::cart/minicart.phtml">
<...>
</block>
</referenceContainer>
It’s also important to note that we use a JavaScript variable within the window
context, because uiComponents
are inherently tied to frontend logic. It’s all just sparkling JavaScript really. Therefore, a uiComponent
cannot fetch data from the backend on it’s own (excluding AJAX wizardry of course) and is dependent for a block to place the payload into the document for it to work with it.
Since discussing how to work with Plugins and how to inject their data into the document is somewhat off-topic, I’ll just show you this data flow illustration to help you understand the process.
Alright. And that’s basically it. That’s how you can integrate Knockout.js variables into custom uiComponents
and also retrieve data from an external source.