- Magento 2: Introducing UI Components
- Magento 2: Simplest UI Component
- Magento 2: Simplest UI Knockout Component
- Magento 2: Simplest XSD Valid UI Component
- Magento 2: ES6 Template Literals
- Magento 2: uiClass Data Features
- Magento 2: UI Component Data Sources
- Magento 2: UI Component Retrospective
- Observables, uiElement Objects, and Variable Tracking
- Magento 2: uiElement Features and Checkout Application
- Magento 2: Remaining uiElement Defaults
- Magento 2: Knockout.js Template Primer
- Magento 2 UI Component Code Generation
Today we need to take a small detour into ES6 template literals. Template literals are a newish javascript feature that Magento 2’s UI Component system ends up leaning on for some key pieces of functionality. They’re an important concept to understand by themselves, but it’s even more important to understand the extra abstractions Magento have built up on top of template literals, as well as how Magento’s uiClass
object system has these template literals baked into its bones.
Also, before we get started, a big thanks to “bassplayer7” over on the Magento StackExchange. This answer provided a key piece of information that helped me finish up this article.
ES6 Template Literals
For many of you, your first question is likely – what the heck is ES6?!
The ES is short for ECMAScript. The ECMA is short for (Ecma_International)[https://en.wikipedia.org/wiki/Ecma_International], the body that standardized the various javascript/jscript/actionscript programming languages into one unified standard.
ECMAScript 6 is the latest version of this standard. Browser makers (Apple, Google, Firefox) are implementing features from ES6 over time (similar to how HTML5 rolled out).
One of those features is something called template literals. Template literals provide javascript with a simple, built-in template language. We’re going to use the javascript console in Google Chrome to run through some template literal code samples, but you should be able to use these in any javascript enviornment that supports them.
In their simplest form, template literals are almost indistinguishable from strings. Consider the Hello World
text below.
> results = `Hello World`
> console.log(results);
Hello World
Notice we’ve placed the text “Hello World” between backticks (the ` character). By placing the text between backticks, we’re telling javascript this string is a template literal.
So far, there’s not much difference between a string and a template literal. Template literals are (from a userland perspective) immediately rendered as strings. Consider the following.
> var results = `Hello World`
> var type = typeof results;
> console.log(type)
string
To javascript, the results
variable looks like a string. Given what we’ve seen so far, template literals seem useless.
Of course, we wouldn’t be telling you about them if they were useless! Template literals support template variables/placeholders. Consider the following
> var salutation = "Goodbye"
> var results = `${salutation} World`
> console.log(results);
Above, ${salutation}
is a template literal variable (or placeholder). These variables read from javascript’s current scope. So, javascript will render the template variable ${salutation}
as the word Goodbye
. Javascript will do this because we assigned the string Goodbye
to the global variable salutation
.
While covering template literal’s functionality in full is beyond the scope of this article, The Mozilla developer network has more information on template literals, including advanced features like “tag” functions.
Browser Support and Magento 2
Like any new javascript feature, a client side developer needs to be careful before adopting template literals. Template literals are not supported across all browsers currently in use, and trying to use them in an old browser will likely result in javascript errors that crash your application.
Magento 2’s developers worked around this limitation by creating a RequireJS module for rendering template literals. This module’s identifier is mage/utils/template
, and you can use it like this.
//requires an enviornment bootstrapped with Magento 2
//javascript. i.e. open you debugger on a Magento page
> requirejs(['mage/utils/template'], function(templateRenderer){
window.salutation = 'Brave New';
var templateLiteral = '${salutation} World';
var results = templateRenderer.template(templateLiteral);
console.log(results);
});
Brave New World
Behind the scenes, the mage/utils/template
module will check for template literal support. If this support is there, the module uses the native browser implementation. If support’s not there, the module will use a (slower) pure userland javascript implementation. I believe the kids call this sort of abstraction a polyfill.
While this module is useful, there are a few caveats. First, you’ll notice we needed to assign the salutation
variable to the global namespace (“window
” in browser javascript).
window.salutation = 'Brave New';
var templateLiteral = '${salutation} World';
Native template literals will read from the current functional scope, but Magento 2’s module (or any userland code) doesn’t have automatic access to that scope. Therefore, the template literals can only read from global scope. The following code
> requirejs(['mage/utils/template'], function(templateRenderer){
var salutation = 'Brave New';
var templateLiteral = '${salutation} World';
var results = templateRenderer.template(templateLiteral);
console.log(results);
});
results in the following javascript error (unless, of course, you have a global variable named salutation defined)
VM1627:1 Uncaught ReferenceError: salutation is not defined
The second mage/utils/template
caveat, and likely a direct result of the above scope problem, is Magento 2’s template literals have extra abilities that go above and beyond those defined in the standard.
Binding View Variables to a Template Literal
Magento 2’s template literals allow you to bind a specific object to a specific template literal, and then reference those variables with a special syntax. The syntax for binding the object looks like this.
> requirejs(['mage/utils/template'], function(templateRenderer){
var viewVars = {
'salutation':'What a Crazy'
};
var templateLiteral = '${salutation} World';
var results = templateRenderer.template(templateLiteral, viewVars);
console.log(results);
});
That is, the template
method has a second parameter that accepts an object.
templateRenderer.template(templateLiteral, viewVars);
However, the above program still results in an error. That’s because of the aforementioned special syntax. You need to reference this object with a second $
symbol inside a ${}
variable/placeholder. In other words, this
var templateLiteral = '${salutation} World';
needs to be this
var templateLiteral = '${$.placeholder} World';
It’s a slightly awkward syntax, but easy enough to mentally parse once you understand that $.
is just a place holder for your passed in object. If you try running this corrected program, you should see the correct What a Crazy World
output.
> requirejs(['mage/utils/template'], function(templateRenderer){
var viewVars = {
'salutation':'What a Crazy'
};
var templateLiteral = '${$.salutation} World';
var results = templateRenderer.template(templateLiteral, viewVars);
console.log(results);
});
What a Crazy World
Connection to UI Components
You may be wondering why we’re covering template literals in a series focused on Magento 2’s UI Components. It turns out template literals are baked into the view model constructor object system we’ve discussed in previous articles.
As we know, the Knockout.js view model constructor objects Magento uses with its UI Components are based on the uiElement
/Magento_Ui/js/lib/core/element/element
RequireJS module. We’re going to take a step away from Knockout and try using this object system via pure javascript code to better understand it. Here’s a simple example that creates a new view model constructor, and uses that constructor to create a view model.
> requirejs(['uiElement'], function(Element){
viewModelConstructor = Element.extend({});
viewModel = new viewModelConstructor;
console.log(viewModel);
});
UiClass {_super: undefined, ignoreTmpls: Object, ...}
One of the features of this object system (implemented in the uiClass
/Magento_Ui/js/lib/core/class
module that uiElement
extends from) is the ability to have your instantiated object include “default” values. If you provide a defaults
object for your view model constructor
viewModelConstructor = Element.extend({
'defaults':{
'ourDefaultValue':'Look at our value!'
}
});
then any object instantiated from this constructor will have a default value for its ourDefaultValue
property.
> requirejs(['uiElement'], function(Element){
viewModelConstructor = Element.extend({
'defaults':{
'ourDefaultValue':'Look at our value!'
}
});
viewModel = new viewModelConstructor;
console.log(viewModel.ourDefaultValue);
});
Look at our value!
It turns out Magento’s object system will scan any default
string value for a template literal, and automatically render those template literals. Consider the following program.
> requirejs(['uiElement'], function(Element){
window.salutation = 'Hello';
viewModelConstructor = Element.extend({
'defaults':{
'message':'${salutation} World. ',
'salutation':'Goodbye'
}
});
viewModel = new viewModelConstructor;
console.log(viewModel.message);
});
Hello World.
Here we’ve provided the view model constructor with a default message
'defaults':{
'message':'${salutation} World. '
}
and Magento 2’s javascript objects automatically expand that literal in the instantiated viewModel
object, using the globally set salutation
variable.
Hello World.
These defaults
template literals can also take advantage of Magento’s “bound object” feature. The $.
object accessor will read from any other defaults
variable.
requirejs(['uiElement'], function(Element){
viewModelConstructor = Element.extend({
'defaults':{
'message':'${$.salutation} World. ',
'salutation':'Goodbye'
}
});
viewModel = new viewModelConstructor({
'salutation':'This is still a crazy'
});
console.log(viewModel.message);
});
As well as an object passed to the view model constructor.
requirejs(['uiElement'], function(Element){
viewModelConstructor = Element.extend({
'defaults':{
'message':'${$.salutation} World. ',
'salutation':'Goodbye'
}
});
viewModel = new viewModelConstructor({
'salutation':'This is still a crazy'
});
console.log(viewModel.message);
});
As you can see from our final program, the values passed to the constructor have precedence over those passed in as defaults
. i.e. The This is still a crazy
salutation wins out over the Goodbye
salutation.
Wrap Up
Today’s tutorial is a good example of the challenges facing a developer who wants to adopt Magento 2 as a platform. Not only are there bleeding edge javascript concepts to pickup, but you also need to understand how Magento has extended these concepts in non-standard ways (binding view variables), and also understand how Magento has composed these objects into their systems (the uiClass
object system). Without the concepts above, a developer who encounters code like this
return Element.extend({
defaults: {
clientConfig: {
urls: {
save: '${ $.submit_url }',
beforeSave: '${ $.validate_url }'
}
}
},
will be left scratching their head.
Fortunately, we now have Magento’s ES6-like template literals in our tool-belt. We’re one step closer to being able to explore how Magento gets backend data into a view model constructor’s defaults
array, as well as how Magento handles the UI Component generated data sources.