Categories


Recent Posts


Archives


Magento 2: Using the uiClass Object Constructor

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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.