Categories


Archives


Recent Posts


Categories


Magento 2: uiClass Internals

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!

Last time we described Magento’s uiClass constructor function. We covered how to use it, and what sort of extra features it has above and beyond javascript’s built in constructor functions. These features include

Today we’re going to dive into the source of the uiClass module, and figure out how Magento’s core team implemented these features.

As per usual, a few caveats before we begin. The specifics here are Magento 2.1.x, but the concepts should apply across versions. Also, we’re going to assume you’re passingly familiar with javascript’s object system. If you’re not we’ve written a few tutorials to get you up to speed. Related to that, when talking about “prototypes” in javascript, we’ve tried to adopt the “.prototype property” and “javascript-[[prototype]]” convention in order disambiguate these two related, but very different, features.

The uiClass Module

Magento’s uiClass alias points at the Magento_Ui/js/lib/core/class module.

$ find vendor/magento/ -name 'requirejs-config.js' | xargs ack uiClass
vendor/magento/module-ui/view/base/requirejs-config.js
15:            uiClass:        'Magento_Ui/js/lib/core/class',

Let’s take a look at this module’s source from a high level

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js

define([
    'underscore',
    'mageUtils',
    'mage/utils/wrapper'
], function (_, utils, wrapper) {
    'use strict';

    var Class;
    /*... lots of stuff ... */
    return Class;        
});

This module imports three RequireJS dependencies — underscore (as _), mageUtils (as utils), and mage/utils/wrapper (as wrapper). We’ve covered these modules previously.

We also see this module is running in strict mode

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js    
'use strict';

and defines a Class variable, which it ultimately returns

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js    

var Class;
/*... lots of stuff ... */
return Class;         

This Class variable will contain the base requirejs('uiClass') constructor function.

If we look at the lots of stuff section (again, from a high level), we see that there’s two local/private functions defined (getOwn and createConstructor)

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js    

function getOwn(obj, prop) {
    /*...*/
}

/*...*/

function createConstructor(protoProps, consturctor) {
    /*...*/
}

Then we see the module populates the Class variable by calling createConstructor.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js    

Class = createConstructor({
    /*...*/    
});

Finally, the module adds a few properties and methods to the Class variable via _.extend before returning Class

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js    

_.extend(Class, {
    /* ... */
});

return Class;    

Given the varying code styles in Magento’s RequireJS modules, I’ve found it’s useful to look at a module at this high level to get an idea of what it’s doing. While the code inside is complex, the module itself isn’t. All that’s really happening is

  1. The module creates a constructor function
  2. The module adds some new properties to that constructor function
  3. The module returns the constructor function

Now that we understand the module at this high level, we have a map for continuing our exploration.

Understanding createConstructor

The private createConstructor function has a few jobs. It

  1. Automates the creation of javascript object constructor functions with automatic .prototype and .prototype.constructor assignments.

  2. Offers up a default constructor function that implements the “new-less” feature, and the “call initialize on instantiation” feature

That is, in native javascript, when you define a constructor function

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js    

var Foo = function(){
    this.message = "Hello";
}
console.log(Foo.prototype);

this function’s .prototype property is automatically set to a blank object. The createConstructor method allows you to assign your own object to this .prototype property, and have that same object’s .constructor property properly reflect the uiClass inheritance/instantiation chain. The code related to this is here

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js    

function createConstructor(protoProps, consturctor) {
    var UiClass = consturctor;

    /* ... */

    UiClass.prototype = protoProps;
    UiClass.prototype.constructor = UiClass;

    return UiClass;
}

The more important section of this function, however, is the standard default constructor. When Magento does its initial population of the Class variable

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

Class = createConstructor({/* ... */});

there’s only a single argument passed. This means the constructor parameter

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

function createConstructor(protoProps, consturctor) {

is undefined. This means Magento creates a default constructor in the conditional block here

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

var UiClass = consturctor;
if (!UiClass) {
    UiClass = function () {
        var obj = this;

        if (!_.isObject(obj) || Object.getPrototypeOf(obj) !== UiClass.prototype) {
            obj = Object.create(UiClass.prototype);
        }

        obj.initialize.apply(obj, arguments);

        return obj;
    };     
}

This default constructor does two things. First,if it detects it was called without the new keyword, this function instantiates a new object in a way that mimics Magento’s standard constructor function, and returns that object. i.e. it implements the new-less behavior we’ve previously discussed.

Second, this constructor function calls the initialize method on the just instantiated object. This is what gives uiClass objects the _construct like behavior with the initialize method.

Using createConstructor

If we take another, fuller, look at Magento’s first use of the createConstructor method.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

Class = createConstructor({
    initialize: function (options) {
        /* ... */
    },

    initConfig: function (options) {
        /* ... */
    }
});

we see its sole argument is an object with two methods — initialize and initConfig. The system will assign this object as Class‘s .prototype property. This means all objects instantiated from this Class function will have an initialize and initConfig method available via their parent .prototype object. This does not give Class these methods. This only gives objects created with Class these methods.

At this point, we could use the Class function to create objects. These new objects could be created with, or without new. The system would automatically call the initialize method on these objects.

However, Class does not have the ability to create objects that inherit from one another. To understand where the inheritance functionality comes from, we need to move on to the _.extend call

Adding Methods to the Class Objects

Next up, our module adds two properties to the Class constructor function

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

_.extend(Class, {
    defaults: {
        ignoreTmpls: {
            templates: true
        }
    },

    extend: function (extender) {
        /* ... */
    }
});

These are properties that will exist on the constructor function itself — and not on objects the constructor function creates. Remember, at this point in the code Class is a javascript function, but javascript functions are also objects, and function-objects can have properties added to them just like any other object.

The first property, defaults, is the defaults that all inheriting constructor functions will inherit. The only value here is an ignoreTmpls object that ensures the property named templates will not, by default, be scanned for template literals.

Of more interest to us at the moment is the extend property. This adds an extend method to our Class constructor function. As we learned last time, if we pass this extend method a specially formatted object, Magento will return a new constructor function that inherits from another constructor function. This is the extend method all Magento uiClass based constructor functions share, and the method we’ll be walking through next.

The Parent, The Child, and the Extender

There are three main objects involved in setting up constructor function inheritance. There’s the parent object. This is the original constructor function we called extend on. There’s the child object. This is the new constructor function that extend will return. Finally, there’s the extender object. This is the object we pass to extend with a new set of defaults, and new methods for objects instantiated with our new constructor function.

We also need to be aware of two related objects — the parent constructor object’s .prototype property, and the child object’s .prototype property. These are the objects the constructor function will assign as the javascript-[[prototype]] for any object instantiated with these constructors.

With the above in mind, let’s take a look at the first few lines of extend, where the code performs some initialization and variable setup.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

extend: function (extender) {
    var parent      = this,
        parentProto = parent.prototype,
        childProto  = Object.create(parentProto),
        child       = createConstructor(childProto, getOwn(extender, 'constructor')),

    /* ... */

    return _.extend(child, {
        __super__:  parentProto,
        extend:     parent.extend
    });        
}

The object we pass to extend (the extender) becomes the variable named extender via a function parameter. Magento assigns the value in this to the parent variable, which is the original constructor function we’re trying to extend.

Next, Magento assigns the value of parent.prototype to parentProto. Remember, this is not the parent object’s javascript-[[prototype]]. The .prototype property is the property a constructor function will assign as the javascript-[[prototype]] when instantiating objects.

Next, Magento creates a childProto variable by creating a new object via Object.create, specifying the parentProto object as this new object’s javascript-[[prototype]].

Then, the code uses createConstructor to create a new constructor function object. This call is worth examining in greater detail, since it’s the child constructor function that extend eventually returns.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

child       = createConstructor(childProto, getOwn(extender, 'constructor')),

By passing the childProto in as the first argument, our new constructor function’s .prototype property will be set to an object with parentProto set as its javascript-[[prototype]]. (i.e. the parent’s .prototype property).

The second argument is also interesting

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

getOwn(extender, 'constructor')

Magento checks the extender object for a property named constructor. The getOwn method ensures Magento is checking the object itself and not following its prototype-chain all the way back (since there’s always a .constructor property at the top of the prototype chain).

If there’s no constructor property, we end up passing a value of false to createConstructor and createConstructor acts as it did previously. However, if our extender object does have a constructor property, the createConstructor method will use this function as the basis for the returned constructor function. You’ll want to be very careful with this feature. While powerful, if your new constructor function doesn’t contain a similar new-less and initialize implementation, your objects will behave differently than standard uiClass objects.

Finally (putting aside the stuff that happens in /* ... */ comment, which we’ll cover next), extend uses _.extend to add a few properties to child before returning it.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

return _.extend(child, {
    __super__:  parentProto,
    extend:     parent.extend
});  

The first property is __super__, which will point back to the parent constructor function’s prototype. It’s not 100% clear why this is here — no other Magento javascript code references it. However, the javascript backbone framework appears to have a similar object extending system, and this system also creates a __super__ property. Perhaps __super__ is here for compatibility with backbone. Or perhaps the Magento core team was inspired by backbone and has future plans for this property.

The second property, extend, ensures our new constructor function also has the very same extend method assigned to it. This allows new constructor functions created with extend to be further extended.

With the code discussed above, Magento takes care of ensuring the javascript-[[prototype]] chain will be property setup for objects created via our extended constructor function. Next, let’s take a look at how the extend method ensures that the new defaults and methods from the extender object will also be available on objects created via our new constructor function.

Assigning New Methods and Defaults

The middle chunk of extend

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

extender = extender || {};
defaults = extender.defaults;

delete extender.defaults;

_.each(extender, function (method, name) {
    childProto[name] = wrapper.wrapSuper(parentProto[name], method);
});

child.defaults = utils.extend({}, parent.defaults || {});

if (defaults) {
    utils.extend(child.defaults, defaults);
    extender.defaults = defaults;
}

handles processing the extender object

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

//the object passed to extend is the extender
UiClass.extend({
    defaults:{
        /*... new defaults here */
    },
    /* ... new method here ... */
})

Let’s look at the start of this block.

extender = extender || {};

Here we have some simple parameter validation. If someone calls extend without an object, Magento ensures that extender is assigned a blank object.

Then, curiously, Magento removes the defaults property from the extender (after storing it in another variable)

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

defaults = extender.defaults;
delete extender.defaults;

This seems weird, but starts to make more sense if we consider the next code block

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

_.each(extender, function (method, name) {
    childProto[name] = wrapper.wrapSuper(parentProto[name], method);
});

Here, using _.each, Magento loops over the remaining properties of the extender object (which, assuming a well behaved client-programmer, should be only methods at this point), and

  1. Grabs a method with the same name from parentProto and wraps the methods with wrapper.super (we previously discussed wrapSuper here)

  2. Assigns those methods to the childPrototype variable

This step ensures that the new constructor function’s .prototype property will be assigned any new function from the extender. If the extender contains a method name that already exists on the parent object’s .prototype property, this is the code that “wraps” the function so we can call the parent with this._super. If parentProto[name] is null/undefined, wrapper.wrapSuper just returns the original function.

That takes care of the methods from extender. The only thing left is the defaults object.

First, Magento copies the defaults object from the parent constructor function to the child constructor function (or assigns an empty object if parent.defaults isn’t there).

http://alanstorm.com/magento-2-uielement-standard-library-primer/
child.defaults = utils.extend({}, parent.defaults || {});

This copy uses utils.extend, which does a deep copy of the object. Then, if the extender object contained a .defaults property, Magento merges those values into the new child.defaults object.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

if (defaults) {
    utils.extend(child.defaults, defaults);
    extender.defaults = defaults;
}

This means the child constructor function will inherit the defaults from the parent constructor function, but anything in the extender will replace the parent default.

Also, this is where Magento re-adds the defaults property to the extender object, ensuring that extender remains unchanged if end-user-programmers do something like

var extender = {...};
UiClass.extend(extender);    
SomeotherThingThatNeedTheDefaulsProperty(extender);

This deletion and re-adding does introduce a small possibility of race conditions if the extender object is used is something like an ajax callback, which could fire at any time.

Once extended, Magento finally returns the new constructor function, ready for the end-user-programmer to use as they want.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

if (defaults) {
    utils.extend(child.defaults, defaults);
    extender.defaults = defaults;
}

return _.extend(child, {
    __super__:  parentProto,
    extend:     parent.extend
});

Running Through Object Instantiation

Now that we’ve worked our way through the entire module, let’s consider what happens when a user instantiates an object from a uiClass constructor function.

When we instantiate an object like this

requirejs(['uiClass'], function(UiClass){
    var object = new UiClass({foo:"bar"});        
});

Javascript will jump to the anonymous function defined in createConstructor

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

function () {
    var obj = this;

    if (!_.isObject(obj) || Object.getPrototypeOf(obj) !== UiClass.prototype) {
        obj = Object.create(UiClass.prototype);
    }

    obj.initialize.apply(obj, arguments);

    return obj;
}; 

The this variable, and therefore the obj variable, will be an empty javascript object whose javascript-[[prototype]] points to the requirejs('uiClass').prototype object, since it’s this function that is this object’s constructor function.

Since obj is already an object, the if block will be skipped.

Then, the constructor function calls the initialize method, with this inside initialize bound to obj. All the arguments to the constructor are passed along.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

obj.initialize.apply(obj, arguments);

The initialize method is not defined directly on obj, but instead is part of its javascript-[[prototype]].

The return obj line will be executed, but is irrelevant when the constructor function is called in new context.

If we consider the same call without the new keyword

requirejs(['uiClass'], function(UiClass){
    var object = UiClass({foo:"bar"});        
});

Again, javascript will jump to the function defined in createConstructor

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

function () {
    var obj = this;

    if (!_.isObject(obj) || Object.getPrototypeOf(obj) !== UiClass.prototype) {
        obj = Object.create(UiClass.prototype);
    }

    obj.initialize.apply(obj, arguments);

    return obj;
};

However, since we called the requirejs('uiClass') function without the new keyword, the obj variable will either be undefined, or equal to the window object, (depending on whether your javascript engine properly passes on the "use strict" context to this anonymous function).

In either case, this time the if block will run, and the obj will be redefined, with its javascript-[[prototype]] pointing at the UiClass.prototype.

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

obj = Object.create(UiClass.prototype);    

This is the same sort of object new UiClass would have created.

At this point the constructor function behaves the same as before

//File: vendor/magento//module-ui/view/base/web/js/lib/core/class.js        

obj.initialize.apply(obj, arguments);
return obj;    

except this time the return obj is important. Since we invoked the constructor function without the new keyword, the value returned by the function is the actual value returned.

The same thing happens for other constructor functions, such as requirejs('uiElement') or requirejs('uiCollection'). All these objects use this default constructor function.

The only objects this would not happen for are those defined with a constructor in their defaults. For what it’s worth, Magento doesn’t appear to do this anywhere in their core code.

Wrap Up

While tricky, now that we understand uiClass objects from head to toe, we’re much better equipped to debugged common problems like data loading and Knockout.js template rendering.

Next time, we’ll round the home stretch and give uiElement objects the same sort of in depth look.

Series Navigation<< Magento 2: Using the uiClass Object Constructor