Today I’m going to show how you can link UI Component properties together and get them talking to each other. We will create an example using nested UI Components, we’ll have a parent component and a child component that will be closely linked together.
There are a handful of properties we can use to link properties together, each with their own usage for different scenarios. The Magento DevDocs has an article about each property, but to summarise:
exports
Used to copy a local value to another external component.
imports
Used to track changes of another external property to a local property.
links
Used for tracking property changes both ways. Both linked properties are tracked and the changing of one results in changing the other.
listens
Used to track the changes of a property, when that property changes set the property specified to the new value or run a method that receives the new value as an argument.
tracks
Used to track changes of a property. This can be used a few different ways…
First example is if you have a property set to a string, and you want to watch it for changes so it’s reactive, you can explicitly tell it to track the property for changes – it’s generally not used for this, a better approach is to use ko.observable('')
instead which tracks it for changes without the need for tracks.
Second example is if you’re importing a ko.observable
property to a local value, when that property changes your local value won’t update, you need to explicitly tell it to watch for changes by using tracks.
Even though technically it’s not used for linking properties per se it deserves a mention as we use it in our example later on.
Magento Template File
First create a new file somewhere in your theme and get the block to display via XML:
<referenceContainer name="content">
<block name="example" template="Magento_Theme::example.phtml" before="-" />
</referenceContainer>
Within this file add the following contents:
<ThemeName>/Magento_Theme/templates/example.phtml
<div id="selector" data-bind="scope: 'parentComponent'">
<!-- ko template: getTemplate() --><!-- /ko -->
</div>
<script type="text/x-magento-init">
{
"#selector": {
"Magento_Ui/js/core/app": {
"components": {
"parentComponent": {
"component": "Magento_Theme/js/parent-component",
"config": {
"provider": "parentComponent.childComponent",
"template": "Magento_Theme/person"
},
"children": {
"childComponent": {
"component": "Magento_Theme/js/child-component",
"config": {
"provider": "parentComponent"
}
}
}
}
}
}
}
}
</script>
First we are initialising our new UI Component using the data-bind=""
method and passing through our component name.
Next we have our component configuration within the text/x-magento-template
script type using regular JSON syntax – we will look at moving this into XML later on.
We have two components defined, the first one called parentComponent
which has a component file and some config options. Then we use the children object to nest a child component called childComponent
which again has a component file and config options.
Knockout Template File
Now we have the configuration in place we can look at creating the Knockout template which we supplied above to our parentComponent
under the template config option.
<ThemeName>/Magento_Theme/web/template/person.html
Add the following contents to the file, for now we’ll just enter some text and come back to it later:
<h1>Hello World!</h1>
This template file gets rendered in our .phtml file by using the Knockout JS syntax:
<!-- ko template: getTemplate() --><!-- /ko -->
JavaScript Component Files
Next we need to create the JavaScript files for each of our components, parentComponent
and childComponent
.
<ThemeName>/Magento_Theme/web/js/parent-component.js
define([
'uiComponent'
], function (Component) {
'use strict';
return Component.extend({
defaults: {},
initialize: function () {
this._super();
return this;
}
});
});
<ThemeName>/Magento_Theme/web/js/child-component.js
define([
'uiElement',
'ko'
], function (Component, ko) {
'use strict';
return Component.extend({
defaults: {},
initialize: function () {
this._super();
return this;
}
});
});
This will be the scaffold for the functionality which we’ll add in next.
Note: The parent component extends uiComponent
and the child component extends uiElement
– if the UI Component has children components (which ours does) you must extend from uiComponent
. The child component doesn’t have children components so we can extend from uiElement
instead.
At this stage you should have everything loading on the frontend, you can confirm this by adding a console.log in you JavaScript files.
# Magento .phtml template
<ThemeName>/Magento_Theme/templates/example.phtml
# Knockout template file
<ThemeName>/Magento_Theme/web/template/person.html
# Parent UI Component
<ThemeName>/Magento_Theme/web/js/parent-component.js
# Child UI Component
<ThemeName>/Magento_Theme/web/js/child-component.js
Child Component
We’re first going to start with the child component functionality, add the following code to the child-component.js
file we created above:
define([
'uiElement',
'ko'
], function (Component, ko) {
'use strict';
return Component.extend({
defaults: {
firstName: 'Jason',
role: 'Front End Engineer',
location: 'United Kingdom',
twitter: 'jasonujmaalvis',
status: ko.observable('Online'),
bio: ko.observable(''),
exports: {
firstName: '${ $.provider }:person.firstName',
role: '${ $.provider }:person.role',
location: '${ $.provider }:person.location',
twitter: '${ $.provider }:person.twitter',
},
listens: {
'${ $.provider }:status': 'statusChanged',
'${ $.provider }:bio': 'bioChanged'
},
},
initialize: function () {
this._super();
return this;
},
statusChanged: function (newValue) {
console.log('status changed to:', newValue);
},
bioChanged: function (newValue) {
console.log('bio changed to:', newValue);
}
});
});
defaults
: used to define our properties using key value pairs. The most basic are set as static strings with the exception of status and bio which are using the Knockout JS library to turn them into observables. This will watch for any changes to that property value and automatically update the frontend, making it reactive.
exports
: we are exporting some of our properties to the parent component which we’ll be building out next. The key is the local property name that we want to export e.g firstName
. The values look a little funky as we are using Magento template strings which is the '${ ... }'
syntax. During the component’s initialisation, values in this format are processed as template strings using ES6 templates. In browsers that do not support ES6 templates, these values are processed as underscore templates. Within the template string syntax we have $.provider
, the dollar symbol converts to this
meaning $.provider
is the equivalent to this.provider
which is the config value we setup in our .phtml file.
# Original
firstName: '${ $.provider }:person.firstName'
# Break down 1
firstName: '${ this.provider }:person.firstName'
# Break down 2
firstName: '${ parentComponent }:person.firstName'
# Break down 3
firstName: 'parentComponent:person.firstName'
The value on the right hand side of the colon is the external property it exports to. Putting all of this together… our local property firstName
is made available to our parentComponent
by assigning it to a property called person.firstName
.
listens:
we are listening for changes to the status and bio properties from our parentComponent
. Whenever these values get updated our methods will run with the new value passed as an argument, in this example we are logging to the console for demonstration purposes.
Parent Component
Now we have the child component in place we can add the functionality to the parent component.
Add the following code to the parent-component.js
file we created above:
define([
'uiComponent'
], function (Component) {
'use strict';
return Component.extend({
defaults: {
person: {},
imports: {
status: '${ $.provider }:status'
},
tracks: {
status: true,
bio: true
},
links: {
bio: '${ $.provider }:bio'
},
},
initialize: function () {
this._super();
return this;
},
getTwitterHandle: function () {
return '@' + this.person.twitter;
},
getTwitterUrl: function () {
return 'https://twitter.com/' + this.person.twitter;
},
getButtonText: function () {
return this.status === 'Online' ? 'Go Offline' : 'Go Online';
},
setStatus: function () {
return this.status === 'Online' ? this.status = 'Offline' : this.status = 'Online';
}
});
});
person
: set to an empty object. If we go back to the child component above you’ll see we exported the firstName
to a property called person.firstName
which gets added to this object.
imports
: we are importing the status property from our child component and assigning it to the status property of this component.
tracks
: we are using tracks because we want the frontend to update automatically whenever the value changes for these properties.
links
: we are linking this component bio property with the child component bio property so they are in sync with each other.
We then have some methods setup to render our data that get called from our Knockout template file which we’ll build next.
Knockout Template File
Now we have all of the JavaScript in place for our UI Components we can re-visit the Knockout template file that we touched on earlier.
Edit the following file:
<ThemeName>/Magento_Theme/web/template/person.html
Add the following contents:
<!-- ko if: person.firstName -->
<p>
<strong data-bind="i18n: 'Name:'"></strong>
<span data-bind="text: person.firstName"></span>
</p>
<!-- /ko -->
<!-- ko if: person.role -->
<p>
<strong data-bind="i18n: 'Role:'"></strong>
<span data-bind="text: person.role"></span>
</p>
<!-- /ko -->
<!-- ko if: person.location -->
<p>
<strong data-bind="i18n: 'Location:'"></strong>
<span data-bind="text: person.location"></span>
</p>
<!-- /ko -->
<!-- ko if: person.twitter -->
<p>
<strong data-bind="i18n: 'Twitter:'"></strong>
<a target="_blank" data-bind="
attr: {
title: $t('Follow me on Twitter'),
href: getTwitterUrl()
},
text: getTwitterHandle()">
</a>
</p>
<!-- /ko -->
<!-- ko if: status -->
<p>
<strong data-bind="i18n: 'Status:'"></strong>
<span data-bind="text: status"></span>
</p>
<!-- /ko -->
<p>
<strong data-bind="i18n: 'Bio:'"></strong>
<!-- ko if: bio -->
<span data-bind="text: bio"></span>
<!-- /ko -->
<!-- ko ifnot: bio -->
<span data-bind="i18n: 'Bio needs to be written.'"></span>
<!-- /ko -->
</p>
<fieldset class="fieldset">
<label class="label" for="bio">
<strong data-bind="i18n: 'Add you bio content:'"></strong>
</label>
<div class="control">
<textarea
id="bio"
cols="5"
rows="5"
data-bind="textInput: bio">
</textarea>
</div>
</fieldset>
<button type="button" class="action primary" data-bind="
click: setStatus,
text: getButtonText()">
</button>
Here we are rendering out our data using the various bind methods available to us. We are also wrapping our data in conditional checks to ensure the data is available and to prevent empty HTML elements on the page should our data change in the future.
Hopefully you should now have something like the below showing on the front end.

One last thing we can do is move the configuration in the example.phtml
file to our XML by using the JS layout which makes it a bit easier to maintain in the future.
<div id="selector" data-bind="scope: 'parentComponent'">
<!-- ko template: getTemplate() --><!-- /ko -->
</div>
<script type="text/x-magento-init">
{
"#selector": {
"Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout() ?>
}
}
</script>
<referenceContainer name="content">
<block name="example" template="Magento_Theme::example.phtml" before="-">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="parentComponent" xsi:type="array">
<item name="component" xsi:type="string">Magento_Theme/js/parent-component</item>
<item name="config" xsi:type="array">
<item name="provider" xsi:type="string">parentComponent.childComponent</item>
<item name="template" xsi:type="string">Magento_Theme/person</item>
</item>
<item name="children" xsi:type="array">
<item name="childComponent" xsi:type="array">
<item name="component" xsi:type="string">Magento_Theme/js/child-component</item>
<item name="config" xsi:type="array">
<item name="provider" xsi:type="string">parentComponent</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</block>
</referenceContainer>
Even though this is only a concept hopefully you can use this demo as an example in your future projects and link multiple components together.