- 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 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, foreach
ing 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
.