When working with Magento, you can stumble upon constructions like ${ $.provider }
here and there. In fact, you are as likely to encounter this kind of expression in JS as in XML configuration files. What are they for? Why are template literals in Magento needed?
In computer programming, the $
sigil symbol is pretty common. Usually, it adds a special meaning to a name. Magento is a PHP framework so obviously, you will find it mostly used as a PHP variable prefix. The most common frontend application of $
is probably as a jQuery alias. ${ $.provider }
does not look like jQuery, however. Chances are though that you have seen similar constructs in JavaScript. If you have and recognized them as template literals, you are correct. Read on though, because — as it frequently is the case in Magento — there’s more to it than meets the eye.
Template literals in Magento have been around for a while. They may not be a household name for an average web developer, but that is probably only because ES6 (ECMAScript 2015) is still not universally supported by browsers. This is also the reason why Magento could not just take the concept on board without any adjustments.
A template is more than a placeholder
Let’s start with a plain JavaScript example then:
let placeholder = 'World'
console.log(`Hello, ${placeholder}!`) // "Hello, World!"
This looks fairly simple. The backticks (``
) tell the browser that this is a template literal. Inside it, you can use the ${}
construct. Anything that goes between {
and }
will be evaluated. This can be any valid piece of JavaScript code. Something more complex below:
let aNumber = 27
console.log(`The cube root of ${aNumber} is ${Math.cbrt(aNumber)}`)
// "The cube root of 27 is 3"
Enter Magento
Atferward is time to examine Magento for template literal uses. A rudimentary search shows several results in Magento_Ui/view/base/web/js/lib/logger/message-pool.js. The file contains the following MESSAGES
object:
Magento_Ui/view/base/web/js/lib/logger/message-pool.js
var MESSAGES = {
templateStartLoading:
'The "${ $.template }" template requested by the "${$.component}" component started loading.',
templateLoadedFromServer:
'The "${ $.template }" template requested by the "${$.component}" component was loaded from server."',
(...)
requestingComponentIsLoaded: 'The requested "${$.component}" component was received.',
requestingComponentIsFailed: 'Could not get the requested "${$.component}" component.'
};
So what are these template literals adjustments Magento makes? Is there any difference compared to literals natively supported by browsers?
Template literals in Magento: how does it work?
The first thing you’ll notice in the code above is that the backticks are gone — template literals in Magento are just plain strings. On further investigation, lib/web/mage/utils/template.js turns out to be the file responsible for handling template literals. A quick glimpse inside can give you an idea of how Magento handles those strings. First, it uses feature detection to determine whether template literals in Magento are supported:
lib/web/mage/utils/template.js
hasStringTmpls = (function () {
var testString = 'var foo = "bar"; return `${ foo }` === foo';
try {
return Function(testString)();
} catch (e) {
return false;
}
})();
If they are, it then just wraps them in backticks. If they are not supported, it uses underscore.js to evaluate the expression.
What about the $ inside curly braces in 'The "${$.component}" component was loaded.'
? It represents the current context. To understand and ilustrate it better, let’s create an example.
Getting our hands dirty
To see a working example, I will create a new module Magently_Literals
. If you don’t know how to create a module, you can find the steps in this article. Next, I’ll add a new template, script.phtml, but before I start to play around with it I need some boilerplate code.
First, I’ll create a route:
Magently_Literals/etc/frontend/routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="magently_literals" frontName="magently_literals">
<module name="Magently_Literals"/>
</route>
</router>
</config>
Then a controller for that route:
Magently_Literals/Controller/Index/Index.php
<?php
namespace Magently\Literals\Controller\Index;
class Index extends \Magento\Framework\App\Action\Action
{
protected $resultPageFactory;
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\View\Result\PageFactory $resultPageFactory
) {
$this->resultPageFactory = $resultPageFactory;
parent::__construct($context);
}
public function execute()
{
return $this->resultPageFactory->create();
}
}
And finally, a layout file that adds a block with the template:
Magently_Literals/view/frontend/layout/magently_literals_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 class="Magento\Framework\View\Element\Template" name="magently.uicomponent"
template="Magently_Literals::script.phtml"/>
</referenceContainer>
</body>
</page>
With the boilerplate out of the way, I can now add the script.phtml template:
Magently_Literals/view/frontend/templates/script.phtml
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"example": {
"component": "Magently_Literals/js/example"
}
}
}
}
}
</script>
The component I use here will have the following code:
Magently_Literals/view/frontend/web/js/example.js
define([
'mage/utils/template'
], function (mageTemplate) {
'use strict';
var template = 'Hello from our ${ $.component } component!';
var templateText = mageTemplate.template(template, {component: 'super-simple'});
console.log('template text: ' + templateText);
});
I imported here the template.js utility as mageTemplate
and put it to action. The second argument passed to the template()
function is the context. It is this object that is referred to as $
inside the curly braces in the template. Running the example in a browser results in the following text being displayed in the console:
Knockout.js comes on the scene
Let’s make the example above a bit more involved and turn it into a Knockout component:
Magently_Literals/view/frontend/web/js/example.js
define([
'underscore',
'uiElement',
'mage/utils/template'
], function (_, Component, mageTemplate) {
'use strict';
var Customization;
Customization = _.extend({
initialize: function () {
this._super();
this.displayMessage();
return this;
},
displayMessage: function () {
var template = 'Hello from our ${ $.component } component!';
var templateText = mageTemplate.template(template, this);
console.log('template text: ' + templateText);
}
});
return Component.extend(Customization);
});
Note how this time instead of a custom object I pass this
to mageTemplate.template()
as the second argument. Now we can see the full name of the component in the console:
To make things more interesting, the first argument passed to template()
can be an object as well. Let’s modify the displayMessage
function in the following way:
Magently_Literals/view/frontend/web/js/example.js
displayMessage: function () {
var myObject = {
componentGreeting: 'Hello from our ${ $.$data.component } component!',
nameGreeting: 'The component name is ${ $.$data.name }.'
};
var processedObject = mageTemplate.template(myObject, this);
console.log(processedObject.componentGreeting);
console.log(processedObject.nameGreeting);
}
I am getting closer here to how Magento’s UI components use template literals. There is one more thing here that needs explaining.
A million dollar literal in Magento
I know the meaning of the first two $s
in ${ $.$data.component }
. The first opens a template literal, and the second is the context. What about the one in $data
? To answer this let’s briefly look at the signature of the function:
lib/web/mage/utils/template.js
template: function (tmpl, data, castString, dontClone)
Depending on what gets passed to it as tmpl
(remember, it can be a string, or an object), the data
that passed in the second argument is bound differently. In my first example I passed a string (containing a template literal) as tmpl
:
var template = 'Hello from our ${ $.component } component!';
var templateText = mageTemplate.template(template, {component: 'super-simple'});
In such cases, an object passed as data
is bound to $
. In my second example I changed the first argument to be an object:
var myObject = {
componentGreeting: 'Hello from our ${ $.$data.component } component!',
nameGreeting: 'The component name is ${ $.$data.name }.'
};
var processedObject = mageTemplate.template(myObject, this);
Here, on the other hand, the second argument is bound to $.$data
. At the end of the day, $data
is just a variable, and the $
in front is just a convention to show that this property is special.
Cutting out the middleman
The mageTemplate
utility works but do you really need to use it on every piece of data that may contain a template literal? Let’s find out by modifying the template in Magently_Literals/view/frontend/templates/script.phtml:
Magently_Literals/view/frontend/templates/script.phtml
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"example": {
"component": "Magently_Literals/js/example",
"componentGreeting": "Hello from our ${ $.component } component!",
"nameGreeting": "The component name is ${ $.name }."
}
}
}
}
}
</script>
The result is the same as before:
A more interesting thing happened in the JS file:
Magently_Literals/view/frontend/web/js/example.js
define([
'underscore',
'uiElement'
], function (_, Component) {
'use strict';
var Customization;
Customization = _.extend({
initialize: function () {
this._super();
this.displayMessage();
return this;
},
displayMessage: function () {
console.log(this.componentGreeting);
console.log(this.nameGreeting);
}
});
return Component.extend(Customization);
});
As you can see, I no longer use the template
function to process our template literals, I do not even import the mageTemplate
utility anymore. Yet, the messages look exactly as before! Remember that the component extends uiComponent
which in turn extends uiClass
. The initConfig
function of this latter file (uiClass
is an alias of Magento_Ui/js/lib/core/class) sheds some light on this conundrum. As you can see in the snippet below, the whole configuration of the component passes through the very same template
function I was using in my code:
Magento_Ui/view/base/web/js/lib/core/class.js
config = utils.template(config, this, false, true);
The front-end is here
Suppose I were to get rid of console messages altogether and display the greetings directly on the page. That’s how I would modify the script.phtml template:
Magently_Literals/view/frontend/templates/script.phtml
<div data-bind="scope: 'example'">
<h2 data-bind="text: componentGreeting"></h2>
<h2 data-bind="text: nameGreeting"></h2>
</div>
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"example": {
"component": "Magently_Literals/js/example",
"componentGreeting": "Hello from our ${ $.component } component!",
"nameGreeting": "The component name is ${ $.name }."
}
}
}
}
}
</script>
Template literals in UI components
At this point, I have what I would probably call a Knockout component. To complicate things further I could turn it into a UI component which would enable me to pass template literals through XML configuration. Magento does not make it easy, though. Still, we can find plenty of examples of using template literals in core UI components’ XML configuration. Here is one:
<imports>
<link name="customerId">${ $.provider }:data.customer.entity_id</link>
</imports>
Linking components’ properties by imports
, exports
, links
and listens is where you often find template literals.
I will focus on the imports property here. The other three revolve around a similar concept, so I will skip them here for brevity’s sake. If you want to know more, you can check Magento documentation for linking properties of UI components.
Putting it all together
Importing is a mechanism that allows for linking different UI components’ properties. To demonstrate how it works let’s remove our greeting template literals from the component’s configuration and use a greeting provider instead:
Magently_Literals/view/frontend/templates/script.phtml
<div data-bind="scope: 'example'">
<h2 data-bind="text: componentGreeting"></h2>
<h2 data-bind="text: nameGreeting"></h2>
</div>
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app": {
"components": {
"example": {
"component": "Magently_Literals/js/example",
"greetingProvider": "greeting_provider"
},
"greeting_provider": {
"component": "Magently_Literals/js/greeting-provider"
}
}
}
}
}
</script>
As you can see I added a new component greeting_provider
. It contains the following code:
Magently_Literals/view/frontend/web/js/greeting-provider.js
define([
'underscore',
'uiElement'
], function (_, Component) {
'use strict';
var Customization;
Customization = _.extend({
defaults: {
componentGreeting: "Hello from our ${ $.component } component!",
nameGreeting: "The component name is ${ $.name }."
},
initialize: function () {
this._super();
return this;
}
});
return Component.extend(Customization);
});
Our template literals now reside inside the component’s defaults
object. The only thing that remains is to use the new greeting provider inside the example
component:
Magently_Literals/view/frontend/web/js/example.js
define([
'underscore',
'uiElement'
], function (_, Component {
'use strict';
var Customization;
Customization = _.extend({
defaults: {
componentGreeting: 'default component greeting',
nameGreeting: 'default name greeting',
imports: {
componentGreeting: "${ $.greetingProvider }:componentGreeting",
nameGreeting: "${ $.greetingProvider }:nameGreeting"
}
},
initialize: function () {
this._super();
return this;
}
});
return Component.extend(Customization);
});
Let’s run the example in a browser:
It looks like the template literals are not working… or are they? A quick check in the console shows that componentGreeting
and nameGreeting
properties were updated — it’s the displayed view that’s out of date. To fix this, you need to make those properties observable:
Magently_Literals/view/frontend/web/js/example.js
define([
'underscore',
'uiElement',
'ko'
], function (_, Component, ko) {
'use strict';
var Customization;
Customization = _.extend({
defaults: {
componentGreeting: ko.observable(),
nameGreeting: ko.observable(),
imports: {
componentGreeting: "${ $.greetingProvider }:componentGreeting",
nameGreeting: "${ $.greetingProvider }:nameGreeting"
}
},
initialize: function () {
this._super();
return this;
}
});
return Component.extend(Customization);
});
This is better, but the result is different from what it was before. This confirms what I mentioned at the beginning of this article. Magento evaluates template literals in their respective contexts.
Context is everything
So first, "${ $.greetingProvider }:componentGreeting" resolves to "greeting_provider:componentGreeting"
. Note the colon in the string above. It separates a component’s name and its property from which the value is to be imported. This is an internal mechanism that Magento also uses in exports
, links
, and listens
. The line above could be translated as “get me the value of componentGreeting
from greeting_provider
component and save it in the current component’s componentGreeting
property”.
Then "Hello from our ${ $.component } component!"
, resolves to Hello from our Magently_Literals/js/greeting-provider component!
.
What if I wanted to turn off template rendering for a property? I can do it inside the defaults
object in the following way:
Magently_Literals/view/frontend/web/js/example.js
componentGreeting: "Hello from our ${ $.component } component!",
nameGreeting: "The component name is ${ $.name }.",
ignoreTmpls: {
componentGreeting: true
}
Other paths to explore
What I focused on — passing components’ properties through the template rendering mechanism — is not the only use of template literals in Magento. Moreover, another interesting one is a mechanism for debugging UI Components.
To see it in action let’s revisit the MESSAGES
object from the beginning of the article and see it in action. The messages in this object indicate that this is some sort of debugging mechanism for UI Components. Digging deeper, I found a message-pool.js file imported in Magento_Ui/view/base/web/js/lib/logger/console-logger.js. Its purpose is to display messages in the browser console. There’s one more thing though: console-logger.js extends Magento_Ui/view/base/web/js/lib/logger/logger.js and there we find the following piece of code:
Magento_Ui/view/base/web/js/lib/logger/logger.js
this.displayLevel_ = levels.ERROR;
To see all messages I would need to change it to this.displayLevel_ = levels.ALL;
. I can do it by extending console-logger.js. Now I will create a requirejs-config.js file in the view/base directory with the following content:
Magently_Literals/view/base/requirejs-config.js
var config = {
"map": {
"*": {
'Magento_Ui/js/lib/logger/console-logger': 'Magently_Literals/js/lib/logger/custom-console-logger'
}
}
};
My custom-console-logger.js file will look like this:
Magently_Literals/view/base/web/js/lib/logger/custom-console-logger.js
define([
'consoleLogger'
], function (ConsoleLogger) {
'use strict';
ConsoleLogger.setDisplayLevel(ConsoleLogger.levels.ALL);
return ConsoleLogger;
});
Now, we can see the stream of information about our components as they load:
In conclusion: as often happens with Magento, it is difficult to cover any topic in full. Certainly, core UI components add a layer of complexity by passing template literals through XML configuration. I hope this article will be a good entry point to explore these mechanisms further.