Categories


Archives


Recent Posts


Categories


Magento 2: uiElement Standard Library Primer

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.

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

Today we’ll wrap up our javascript primer before diving head first into Magento’s uiElement internals. On the menu? A whirlwind tour of some Magento RequireJS helper modules.

We’ll be running today’s code from the javascript console on a loaded Magento 2 page. The specific code mentioned here is from a Magento 2.1.x installation, but the concepts should apply across Magento versions.

Underscore

The first part of Magento’s javascript library we’re going to talk about is Underscore. Underscore is a utility library that offers a number of useful utility functions for javascript developers.

Normally, this library is available as a global variable named _. However, you can’t rely on this global being set at all points in Magento’s bootstrap. Instead, you’ll need to include via a RequireJS dependency.

define(['underscore'], function(_){
    //access to `_` now here!
});

The underscore alias is not setup my Magento’s RequireJS configuration. Instead, the underscore source code itself is AMD/RequireJS aware, and knows how to register itself. The particulars of this aren’t important to us, but feel free to explore yourself if that sort of thing interests you.

Underscore is a big library, but there’s three particular methods we’re interested in today: isObject, each, and extend.

Underscore: isObject

The first underscore utility method we need to understand is isObject. This is a pretty simple method that can tell you if a variable contains an object or not.

var foo = {};
_.isObject(foo);    //returns true

However, like most languages that don’t require explicit typing, what is and isn’t an object is something up for debate. For example, in javascript a function is also an object

var foo = function(){};
_.isObject(foo);    //returns true        

However, a string literal is not an object, even though you can call methods on a string literal.

"one,two,three".split(',');  //calls the `split` method

_.isObject("one,two,three"); //but strings are not objects    

But if your code uses explicit string objects, isObject will treat them as such

foo =  new String("one,two,three");    
_.isObject(foo);            //returns true

Generally speaking, if you’re debugging code that deals with any sort of “isAType” method in any language, it’s always a good idea to understand exactly what the method doing.

Underscore: _each

The _.each method allows you to run through all the key/value pairs in an object, array, or “array like” things in javascript. As we mentioned in our javascript primer, because of javascript’s prototypical inheritance, foreaching over an object or array isn’t always a straight forward affair. The each method allows you to use a callback function to run through every element of an object without doing your own isOwnProperty checks.

_.each({a:1, b:2, c:3}, function(value, key){
    console.log("Key:"   + key);
    console.log("Value:" + key);
});

Make note that the arguments passed to the function are value and key, in that order. If you’re familiar with other frameworks that do this key first and then value, underscore will be a slight adjustment.

Because javascript isn’t confusing enough, each also has a third parameter. This third parameter allows you to bind a different value to this in your callback function.

var object = {hi:'there'};
_.each({a:1, b:2, c:3}, function(value, key){
    console.log("Key:"   + key);
    console.log("Value:" + key);
    console.log(this);
}, object);    

If you’re not familiar with binding this in callback functions, you may want to review our javascript primer.

Underscore: extend

The final underscore method we’ll cover today is extend. The extend method allows you to copy properties from a set of objects into another object.

var final = {a:1,b:2,c:3};

var firstSource =  {d:4,e:5,f:6}; 
var secondSource = {g:7,h:8,i:9};    

_.extend(final, firstSource, secondSource);

//contains all keys and values, up to `i`
console.log( final );

In the above code, the keys from firstSource and secondSource are copied into final. The extend method also returns a reference to the first argument

returnedValue = _.extend(final, firstSource, secondSource);     

console.log(returnedValue === final); //return true

If there are nested objects and arrays, these objects and arrays will be copied by reference. In other words, new objects are not cloned. This is known as a “shallow” copy.

It’s worth noting that this will copy both the object’s properties and the properties of any parent object (i.e. the isOwnProperty problem again). If you don’t want to copy the parent’s properties, underscore has the extendOwn method.

The mageUtils Module

The bit of standard library code we’re going to talk next about is the mageUtils module.

define(['mageUtils'], function(utils){
    //...
});

This is Magento’s own internal library with utility/helper functions. The mageUtils alias points to the mage/utils/main module.

vendor/magento/module-theme/view/base/requirejs-config.js
12:            "mageUtils": "mage/utils/main",

If we look at this module’s source, we’re in for a bit of a surprise.

#File: vendor/magento/magento2-base/lib/web/mage/utils/main.js

define(function (require) {
    'use strict';

    var utils = {},
        _ = require('underscore');

    return _.extend(
        utils,
        require('./arrays'),
        require('./compare'),
        require('./misc'),
        require('./objects'),
        require('./strings'),
        require('./template')
    );
});

Strangely, rather than listing them as module dependencies, this module includes in 7 other modules via direct calls to the require function. This is — not the best. It’s unclear why it doesn’t look more like

define(['./arrays',
        './compare',
        './misc',
        './objects',
        './strings',
        './template'], function (Arrays, Compare, Misc, Objects, Strings, Template) {
    'use strict';

    var utils = {},
        _ = require('underscore');

    return _.extend(
        utils, Arrays, Compare, Misc, Objects, Strings, Template
    );
});

While we’ve used direct calls to to require in our tutorials, this will only work in the global scope module’s only been loaded. It turns out that RequireJS will, during module loading, scan for these direct require calls and ensure the modules are loaded correctly. This usage, while technically valid, is discouraged going forward.

The next thing that might look foreign in these modules is the relative path syntax in the modules themselves

require('./template')

These work like file path operators in unix. Since this is the mage/utils/main module, these modules actually resolve to the following calls.

require('mage/utils/arrays'),
require('mage/utils/compare'),
require('mage/utils/misc'),
require('mage/utils/objects'),
require('mage/utils/strings'),
require('mage/utils/template')

The final weird thing here is what the module is actually doing. First, it defines an empty utils object.

var utils = {}

and then, using the _.extend method we just learned about, merges in all the methods from the six listed RequireJS modules, and returns the resulting object.

return _.extend(
    utils,
    require('./arrays'),
    require('./compare'),
    require('./misc'),
    require('./objects'),
    require('./strings'),
    require('./template')
);      

This pattern gives the mageUtils module all the methods from mage/utils/arrays, mage/utils/compare, mage/utils/misc, mage/utils/objects, mage/utils/strings, and mage/utils/template. It’s a clever pattern, although it does make finding mageUtils method definitions a bit trickier, and also creates the possibility of method collision if any of these modules export a function with the same name.

Covering all the methods of this utility module goes beyond the scope of this article, but there are few we’d like to highlight.

mageUtils: extend Method

The first method we’ll cover today is the mageUtils.extend method.

requirejs(['mageUtils'], function(utils){
    console.log(utils.extend);
});

This method might be a little confusing to you, since we just covered _.extend. You can find the definition for mageUtils.extend in the mage/utils/objects library.

#File: vendor/magento/magento2-base/lib/web/mage/utils/objects.js
/**
 * Performs deep extend of specified objects.
 *
 * @returns {Object|Array} Extended object.
 */
extend: function () {
    var args = _.toArray(arguments);

    args.unshift(true);

    return $.extend.apply($, args);
},

Remember how we mentioned _.extend makes a shallow copy of any nested objects or arrays? Well, mageUtils.extend serves the same purpose as _.extend — except it makes a deep copy. This means it clones any nested arrays and objects so the resulting merged object contains no references to the original.

The mageUtils.extend method achieves this by wrapping jQuery’s extend method.

The first line turns the function arguments into an array (the arguments variable is an array-like thing in javascript that contains all of the arguments passed to a function, regardless of listed parameters)

var args = _.toArray(arguments);

Then, the value true is added as the first item of that array

args.unshift(true);

Finally, Magento calls jQuery’s extend method using apply.

return $.extend.apply($, args);

The first argument to apply is the jQuery object, which means jQuery will be bound as this for this call of extend. I assume this is jQuery’s convention. The second argument is an array of parameters to pass to extend (as per standard use of the apply method. If you look at extend‘s prototype on the jQuery documentation site

jQuery.extend( [deep ], target, object1 [, objectN ] ) 

deep
Type: Boolean
If true, the merge becomes recursive (aka. deep copy)    

If the first argument to extend is a true boolean, jQuery will make a deep copy of the object.

This is a useful utility function to have around — just make sure you take a close look whenever you see an extend method. It may be mageUtil‘s, or it may be _‘s.

mageUtils: nested and omit

The next two utility methods we’ll discuss are nested and omit.

requirejs(['mageUtils'], function(utils){
    console.log(utils.nested);
    console.log(utils.omit);        
});

These are helper methods that make dealing with nests objects of values much easier. First, you can use the nested method to fetch or set a deeply nested object value. First, consider the following program

requirejs(['mageUtils'], function(utils){
    base = {}
    utils.nested(base, 'a.b.c', 'easy as one two three');
    console.log(base);
});

This program will result in output that looks like the following

Object
    a: Object
        b: Object
            c: "easy as one two three"

That is, by passing in the string a.b.c, we told nested to set the following value.

base.a.b.c = 'easy as one two three';                    

The main value of nested is that it can create objects in the hierarchy that might not exist yet. For example, the following two code chunks are equivalent, but one is much more succinct, compact, and less prone to bugs.

//this
utils.nested(base, 'a.b.c', 'easy as one two three'); 

//or this?
if(!base['a'])
{
    base.a = {};
}

if(!base['a']['b'])
{
    base.a.b = {};
}    

base.a.b.c = 'easy as one two three';     

You can also use nested to fetch values from a nested hierarchy

requirejs(['mageUtils'], function(utils){
    //...
    console.log( 
        utils.nested(base, 'a.b.c',);
    )
    //...
});    

again, the advantage being you don’t need to check for the existence of each object along the hierarchy chain. i.e., you avoid javascript errors like this

base = {};
if(base.a.b.c)  //results in a Cannot read property 'b' of undefined error
{
}    

The omit method serves a similar purpose, except that it removes nodes from a nested hierarchy. The following program

requirejs(['mageUtils'], function(utils){
    base = {};

    //create the nodes
    utils.nested(base, 'a.b.c', 'easy as one two three');
    utils.nested(base, 'a.b.d', 'that\'s not how the song goes');

    //remove some
    utils.omit(base, 'a.b.c');

    console.log(base);
});

will result in the following output.

Object
    a: Object
        b: Object
            d: "that's not how the song goes"

In other words, omit removed the nodes at “a.b.c“.

mageUtils: template method

The final mageUtils method we’ll mention today is mageUtils.template.

requirejs(['mageUtils'], function(utils){
    console.log(utils.template);      
});                

This method is an abstraction to bring ES6 template literal support to browsers that don’t support them.

We’ve already covered ES6 template literals in our UI Component series. Your main takeaway here is utils.template (which come from mage/utils/template) is where Magento’s template literal abstraction lives.

The Wrapper Utility

There’s one last utility module we’d like to cover today, although it’s not part of mageUtils. It’s the mage/utils/wrapper module. We talked a bit about this module in our “Not Really Mixins” article, but today we want to talk about the module’s wrapSuper method.

Through a combination of the the wrap method and some function binding, the wrapSuper method gives you the ability to add a this._super call to your wrapped function that will allow you to call the original, (some might say parent) function.

If that didn’t make sense, here’s a code sample that should clarify things

requirejs(['mage/utils/wrapper'], function(wrapper){

    //define a function
    var hello = function (noun) {
        noun = noun ? noun : "World";
        console.log("Hello " + noun);
        console.log(this);
    };

    //create a function with `wrapper.wrapSuper` based on
    //the `hello` function
    var obj = {
        goodbye: wrapper.wrapSuper(hello, function (noun) {
            noun = noun ? noun : "World";

            //call the original `hello` function
            this._super();
            console.log("Goodbye " + noun);                        
        })
    };  

    obj.goodbye("Planet");
});

The above program results in the following output

Hello Planet
[Object]        //console.log(this)
Goodbye Plant

The above program creates a function named hello, and then, via wrapper.wrapSuper, creates a new method on our object named goodbye. The key bit we’re interested in here is the following call

this._super();      

The this._super() call will invoke the hello function. It will also ensure that all of the arguments passed to goodbye get passed along to hello, and that the this variable will be bound to our original object.

Wrap Up

OK! With that whirlwind tour complete, we’re ready to begin our deep dive into Magento’s uiElement object system. Next time we’ll start with Magento’s own “top level” object — the uiClass.

Series Navigation<< Tracing Javascript’s Prototype ChainMagento 2: Using the uiClass Object Constructor >>

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 6th December 2016

email hidden; JavaScript is required