- Magento 2: Defaults, uiElement, Observables, and the Fundamental Problem of Userland Object Systems
- Magento 2: Javascript Primer for uiElement Internals
- Tracing Javascript’s Prototype Chain
- Magento 2: uiElement Standard Library Primer
- Magento 2: Using the uiClass Object Constructor
- Magento 2: uiClass Internals
Today, after our string of javascript primer articles, we’re ready to really start. While we’ve called this series “uiElement
Internals”, before we can talk about uiElement
objects, we need to talk about uiClass
objects.
The uiClass
module returns a javascript constructor function. However, this is no ordinary constructor function. The uiClass
constructor function is the top level object in Magento’s javascript userland object system. It provides support for new
-less object creation, template literal parsing, and constructor function inheritance. These are all features the uiElement
objects inherit. Today we’ll explore using these features stand-alone with a uiClass
object.
The specifics here are from Magento 2.1.x, but the concepts should apply across versions. You can run the sample programs in your browser’s javascript console on a loaded Magento 2 page.
Creating Objects
Here’s a small RequireJS program that uses the uiClass
object constructor function to create a new object.
requirejs(['uiClass'], function(UiClass){
var object = new UiClass({foo:'bar'});
console.log(object); //logs the object
console.log(object.foo); //logs the string 'bar'
});
The above program lists uiClass
as a RequireJS dependency, importing it with the local name UiClass
. As a side note, Magento core code usually imports this class via the local name Class
— but given Magento’s history with Prototype.js and the newish javascript class
keyword, we think it’s clearer to use UiClass
.
Looking at the code above, it’s not immediately clear what benefit using the uiClass
constructor function to create objects brings to the table. The above code seems equivalent to javascript’s standard Object
constructor
var object = new Object({foo:'bar'});
console.log(object); //logs the object
console.log(object.foo); //logs the string 'bar'
For this basic example, that’s a valid criticism. However, uiClass
objects start to earn their keep when we use them to create constructor functions that inherit from one another.
requirejs(['uiClass'], function(UiClass){
var OurFirstConstructorFunction = UiClass.extend({
'defaults':{
'message':'Hello World'
},
hello:function(){
console.log(this.message);
}
});
var object = new OurFirstConstructorFunction;
object.hello(); //calls hello method of our object
});
The above program creates a new constructor function (OurFirstConstructorFunction
) that inherits from the UiClass
constructor function. The UiClass
‘s extend
method allows end-user-programmers (i.e. us!) to create a new constructor function. Objects created via that constructor function will have default values and methods that we specify via extend
. The extend
method accepts a single object as an argument.
{
'defaults':{
'message':'Hello World'
},
hello:function(){
console.log(this.message);
}
}
The defaults
key is an object of key/value pairs that will determine default properties for any objects constructed with our new object constructor. Magento will add any other key (hello
above) as a method to our constructed objects. If a non-defaults
key contains an object that is not a function, the system will ignore it.
The above program gives us an object constructor we can instantiate objects from like this
var object = new OurFirstConstructorFunction;
Then we can call our defined methods on that object.
object.hello(); //calls hello method of our object
This sort of thing is possible with raw javascript, but the syntax is a little different.
var OurJavascriptConstructorFunction(){
this.message = 'Hello World';
this.hello = function(){
console.log(this.message);
}
}
var object = new OurJavascriptConstructorFunction;
object.hello();
The extend
method gives you something that resembles the boilerplate class definitions of java, c#, and other “classical” languages. Javascript requires you to be comfortable with a more dynamic idea of object definition. Neither one is better or worse — however, javascript’s does contain this subtle trap.
var object = OurJavascriptConstructorFunction();
object.hello();
If a programmer accidentally leaves out the new
keyword when creating objects, javascript will call the constructor function as though it were any other javascript function. Since a call without the new
keyword won’t properly bind the this
variable, the above mistake creates hard to track down problems in programs.
Magento’s object system fixes this. With uiClass
based objects you can generate a new object using the new
keyword, or just by calling the constructor function. Consider the following program
requirejs(['uiClass'], function(UiClass){
var OurFirstConstructorFunction = UiClass.extend({
'defaults':{
'message':'Hello World'
},
hello:function(){
console.log(this.message);
}
});
var object = OurFirstConstructorFunction();
object.hello(); //calls hello method of our object
});
The above code uses the new
-less form (var object = OurFirstConstructorFunction();
), but still behaves the same. By creating their own javascript based object system, Magento have avoided an entire class of bugs.
Template Literals
Now that we know about defaults
, we can talk about the uiClass
‘s template literal feature. Consider the following program.
requirejs(['uiClass'], function(UiClass){
var OurFirstConstructorFunction = UiClass.extend({
'defaults':{
'noun' :'World',
'verb' :'Hello',
'message':'${$.verb} ${$.noun}'
},
hello:function(){
console.log(this.message);
}
});
var object = OurFirstConstructorFunction();
object.hello(); //calls hello method of our object
});
Here we have another variation on the Hello World program. However, if you look at the message
property we’re outputting, we don’t see the text “Hello World”. Instead we see the following
{
/* ... */
'message':'${$.verb} ${$.noun}'
/* ... */
}
What we’re looking at is a template literal, represented as a javascript string. We’ve written extensively about template literals already in our UI Components series. What you need to know here is a uiClass
based constructor function will parse every defaults
as a template literal string before assigning its value to the instantiated object. That’s why the above program produces the Hello World
output.
If, for some reason, you don’t want template literal parsing for a defaults
property, just include the ignoreTmpls
directive in your defaults
. For example, the following program
requirejs(['uiClass'], function(UiClass){
var OurFirstConstructorFunction = UiClass.extend({
'defaults':{
'noun' :'World',
'verb' :'Hello',
'message':'${$.verb} ${$.noun}',
'ignoreTmpls':{
'message': true
}
},
hello:function(){
console.log(this.message);
}
});
var object = OurFirstConstructorFunction();
object.hello(); //calls hello method of our object
});
will produce an unparsed message
property
${$.verb} ${$.noun}
That’s because we listed the message
property here
'ignoreTmpls':{
'message': true
}
With the above configuration, we’ve told the constructor function to not parse .message
for template literals when it instantiates a new object.
Object Inheritance
You can also use Magento’s uiClass
objects to create longer inheritance chains for Magento objects. In this way, they act more like classes do (and hence the name, uiClass
).
It’s possible to have constructor functions set the javascript-parent-prototype of the objects it creates in native javascript
var ourPrototypeObject = {'foo':'bar'};
var OurConstructorFunction = function(){
};
OurConstructorFunction.prototype = ourPrototypeObject;
object = new OurConstructorFunction;
This means it’s theoretically possible to create long object chains of constructor functions — but the requirements we do this in executable code means complex/deep object hierarchies often end up creating impenetrable and inconsistent class definitions.
With extend
, defining hierarchies becomes a bit more consistent.
requirejs(['uiClass'], function(UiClass){
var OurFirstConstructorFunction = UiClass.extend({
'defaults':{
'message':'Hello World'
},
hello:function(){
console.log(this.message);
}
});
var OurSecondConstructorFunction = OurFirstConstructorFunction.extend({
'defaults':{
'message':'Hello World! You look great!',
'message2':'How can we help you today'
},
greetings:function(){
this.hello();
console.log(this.message2);
}
});
var object = new OurSecondConstructorFunction();
object.greetings(); //calls hello method of our object
});
Above we’ve created an object constructor, OurSecondConstructorFunction
, that has OurFirstConstructorFunction
as a parent. The OurFirstConstructorFunction
class has the base uiClass
object as a constructor. You’ll notice that our class has redefined one of the object defaults, and calls a method (hello
) defined on its parent.
The uiClass
objects give programmers not familiar with the tricky internals of javascript’s prototype based object system access to easy inheritance. Whether you think that’s a good thing or bad thing will depend on your programming background, your team, and what you think of large, complex object hierarchies.
Regardless though, this is how Magento’s javascript object system works, and you’d be wise to work within its constraints when writing your own UI Component code.
Calling Parent Object Methods
In addition to this simplified inheritance pattern, uiClass
based objects also give you the ability to call into parent methods when your new constructor function is defining a method.
Consider the following program
requirejs(['uiClass'], function(UiClass){
var OurFirstConstructorFunction = UiClass.extend({
'defaults':{
'message':'Hello '
},
hello:function(thing){
console.log(this.message + thing);
}
});
var object = new OurFirstConstructorFunction;
object.hello("World"); //calls hello method of our object
});
Here we have a simple program that defines a new constructor function (OurFirstConstructorFunction
), instantiates an object from it, and then calls its hello
method.
Now, consider this modified program
requirejs(['uiClass'], function(UiClass){
var OurFirstConstructorFunction = UiClass.extend({
'defaults':{
'message':'Hello '
},
hello:function(thing){
console.log(this.message);
}
});
var OurSecondConstructorFunction = OurFirstConstructorFunction.extend({
hello:function(thing){
console.log("Our Redefined Hello Method");
}
});
var object = new OurSecondConstructorFunction;
object.hello("World"); //calls hello method of our object
});
Here we’ve defined a second constructor function (OurSecondConstructorFunction
) that inherits from the first (OurFirstConstructorFunction
). This function also redefines the hello
method.
So far, there’s nothing much new here. However, lets say you want to call the original hello
function from OurFirstConstructorFunction
. Consider this final program.
requirejs(['uiClass'], function(UiClass){
var OurFirstConstructorFunction = UiClass.extend({
'defaults':{
'message':'Hello '
},
hello:function(thing){
console.log(this.message);
}
});
var OurSecondConstructorFunction = OurFirstConstructorFunction.extend({
hello:function(thing){
this._super(thing);
console.log("Our Redefined Hello Method");
}
});
var object = new OurSecondConstructorFunction;
object.hello("World"); //calls hello method of our object
});
This program is almost identical to the previous program, except in our redefined hello
method
var OurSecondConstructorFunction = OurFirstConstructorFunction.extend({
hello:function(thing){
this._super(thing);
console.log("Our Redefined Hello Method");
}
});
Here you’ll notice a call to this._super(thing)
. This is how you call a parent function in Magento’s uiClass
based objects. When Magento’s object system sees a call to this._super
, it will look to the parent object for a method with the same name, and then call that method.
Initialize and initConfig
There’s two last features of uiClass
objects we want to talk about, and that’s the initialize
, and initConfig
methods. These are two methods automatically defined on any object created from a uiClass
constructor function (or any constructor function that inherits from the uiClass
constructor). You can see these functions with the following small program
requirejs(['uiClass'], function(UiClass){
var OurConstructorFunction = new UiClass;
console.log(object.initialize.toString());
console.log(object.initConfig.toString());
});
These methods are part of the uiClass
internal implementation. However, you can also redefine them in child objects. You can see an example of this in the following program.
requirejs(['uiClass'], function(UiClass){
var OurConstructorFunction = UiClass.extend({
initialize:function(options){
this._super(options);
console.log('Initialized!');
},
initConfig:function(options){
this._super(options);
console.log("Config Inited!");
}
});
object = new OurConstructorFunction;
});
This program produces the output
Config Inited!
Initialized!
Magento will call the initialize
method whenever an object is instantiated. It’s similar to the _construct
method in Magento 1’s old PHP based Models and Blocks. The initConfig
method is a method called by the base initialize
method, which means it’s called whenever an object is instantiated.
If you are going to redefine these methods it’s important that you include a call to this._super. Otherwise, your objects will not have their configurations inited, which means your defaults
won’t be defined.
One Constructor Function, One Module
Finally, we need to talk about code organization. In the above programs, we defined multiple constructor functions in a single RequireJS module. While this is valid code, Magento tends to follow a pattern of having a single RequireJS module define and return a single constructor function. For example, if you take a look at the uiCollection
module you’ll see it returns a new constructor function by extending the uiElement
constructor function
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
define([
'underscore',
'mageUtils',
'uiRegistry',
'uiElement'
], function (_, utils, registry, Element) {
'use strict';
/* ... */
return Element.extend({/*...*/});
});
While this pattern can make it a little difficult to track down the source for some methods, by following this “one constructor function, one module” rule, Magento have ensured all developers working in the system will have access to these constructor functions. To push the classical inheritance metaphors a little further, these are like PHP’s single class definition files.
Wrap Up
You now know how to instantiate and use Magento’s uiClass
based constructor functions. However, for long time javascript developers, you may be wondering how all this works. Without understanding how this user-land object system is implemented, we’ll always be left wondering if that weird edge case bug is a problem with our code, or just some unexamined system edge case.
Next time we’ll remedy this lack of knowledge by jumping deep into the implementation of the uiClass
module. While not necessary for every Magento developer, we look forward to seeing everyone who self-selects for this sort of system programming.