Categories


Archives


Recent Posts


Categories


Magento 2: Javascript Primer for uiElement 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.

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

Before we can get started exploring Magento’s uiElement object system, there are some javascript fundamentals to cover. This may be review for some of you, and may be brand new content for others. While you don’t need to have mastery of these concepts to use Magento’s new object system, if you’re trying to reason programmatically about the uiElement internals you’ll want to understand the bedrock they’re built on.

Also, I should probably warn you, as someone who’s been programming with javascript since the Netscape 2 days, my perspective is going to be a little skewed when compared with a modern, or modernizing javascript developer. Feedback is more than welcome.

Finally, most of the code below can be run in your browser’s javascript console. In Google Chrome that’s View -> Developer -> Javascript Console. Any browser specific code, unless otherwise mentioned, is strictly accidental.

Javascript and this

The this variable in javascript is a little weird. If you’re coming from PHP, $this always refers to “the current object” in a class file.

class A
{
    function bar()
    {
        //this
    }

    function foo()
    {
        return $this->bar();
    }
}

The this variable in javascript is a much weirder thing. Depending on where and how you use it, this can refer to a bunch of different things.

First off, if you’re using this in the global scope (i.e. not in a function), then this refers to that global scope. In a browser, that means this and window are also the same thing (again, with this in the global scope).

foo = 'bar';
console.log(this.foo === foo);  //returns true
console.log(window.foo === foo);  //returns true

//equality for objects in javascript checks their identity
//i.e. is this the same object    
console.log(this === window);   //returns true

In a standard function or method call, this also (by default) refers to the window object;

var foo = function(){
    console.log(this === window);   //true
}
foo();

However, if you attach the function to an object as a method, this refers to the object you’ve attached the function to. Consider the output of the following program.

//define an object
window.object = {};

//define the function
var foo = function(){
    console.log(this === window);
    console.log(this === window.object);
}    

//assign function as method
window.object.myMethod = foo;

//call as function
foo();                      //results in true, false

//call as method    
window.object.myMethod();   //results in false, true

In the first function call, this points to window. In the second method call, this points to the object.

That’s tier 1 of the weirdness of this. Tier 2 is what happens when you call functions or methods dynamically in javascript.

Just like PHP has call_user_func and call_user_func_array, javascript has object methods which can call a method. These methods are named call and apply. PHP’s function calling functions exists to call named functions based on string values. The call and apply methods exist for a different reason.

Consider this slightly modified program

//define an object
window.object = {};

//define the function
var foo = function(){
    console.log(this === window);
    console.log(this === window.object);
}    

//assign function as method
window.object.myMethod = foo;

//call as function
foo.call();                      //results in true, false
foo.apply();                     //results in true, false

//call as method    
window.object.myMethod.call();   //results in true, false
window.object.myMethod.apply();  //results in true, false      

Here we’ve replaced the direct function and method calls with calls to call() and apply(). Our use of call and apply above are a little silly — they make no change in the program. That’s because we’re not using call and apply‘s main feature — the ability to bind a different object to this. Consider this modified program.

//define out new object we'll bind to this
var boundObject = {};
boundObject.foo = 'Hello!';

//define an object
window.object = {};

//define the function
var foo = function(){
    console.log(this === window);
    console.log(this === window.object);
    console.log(this);
}    

//assign function as method
window.object.myMethod = foo;

//call as function
foo.call(boundObject);                      //results in false, false
                                            //logs boundObject

foo.apply(boundObject);                     //results in false, false
                                            //logs boundObject

//call as method    
window.object.myMethod.call(boundObject);   //results in false, false  
                                            //logs boundObject

window.object.myMethod.apply(boundObject);  //results in false, false
                                            //logs boundObject

The first argument to both call and apply is the object you want bound as this during the function call. By passing in boundObject we’ve ensured that this points to neither the window or window.object objects. Instead, it points to the boundObject object.

There’s a third method that can bind a different value to this, and that’s bind. The bind method allows you to create a new function with a different value bound to this. Consider the following

var boundObject = {};
boundObject.foo = 'Hello!';

var foo = function() {
    console.log(this);
}

var newFunction = foo.bind(boundObject);

foo();              //logs `window` object
newFunction();      //logs boundObject

In the call to foo, this points to the window object. In the call to newFunction, it points to the boundObject.

The main thing we’d like you to take away from all of this is — when you’re looking at a function in javascript and see the this variable, don’t assume you know what’s in there. While javascript’s dynamic nature makes fertile ground for framework developers, every framework likes to do different, clever things with binding this. Learn the assumptions of your framework, but remember that one small bit of extra cleverness may mean this isn’t as it seems.

For those interested in learning more, The Mozilla Developer Network (MDN) has more information (including how to use these methods with function arguments) on this, call, apply, and bind.

Javascript Objects

Javascript is an object oriented language without classes. While ES6 introduced a class keyword, this is syntactic sugar born of a compromise between folks who want to keep javascript a language without classes and those who think javascript needs classes. We’re going to sidestep the issue and ask that you consider javascript as a class-less language since ES6 isn’t evenly distributed, and it’s important to understand where a language came from.

Traditionally there are two ways to create objects in javascript. The first is via object literal assignment

object = {foo:'bar'};

The second is via a constructor function and the new keyword.

var ConstructorFunction = function(){
    this.foo = 'bar';
}

object = new ConstructorFunction();     

A constructor function returns no value. When you use a constructor function with the new keyword, the this variable is bound to the object you’re instantiating. This is a weird, non obvious, design decision.

This is usually where a javascript tutorial links off to a historical account of how javascript was created in 10 days, so we’ll do the same. (we’d be remiss to point out that we think javascript was created in 10 days plus the entire length of Brendan Eich’s career up to that point)

We call these object creating functions “constructor functions”, but to javascript they’re just regular functions. If you call a constructor function without the new keyword, javascript will treat it as a regular function call.

//returns null -- whoops!
object =  ConstructorFunction();  

One last thing before we move on. It’s an old convention (unenforced by the language) in javascript that, if you intend other programmers to use a function as a constructor function, that you name the function with LeadingCase vs. naming it with lowerCase. If you ever see code like the above — a leading case function call without new, there may be a bug in your program. Or maybe someone on the team didn’t know about that convention.

Javascript Object Parent/Child Relationships

Parent/child relationships do exist in javascript, but they’re hard to see for the average javascript client programmer. Javascript uses a “prototype” based object system. Consider two nominal objects, a and b. In a class based (or classical) system, we might have two classes

class A
{
}

class B extends A
{
}

$a = new A;
$b = new B;

Here we’d say A is a parent of B.

In a prototype based object system, (i.e. javascript), we’d say Object b’s prototype points to Object a, or Object a is Object b’s parent.

There’s only one major consequence to an object being a child of another object in javascript. If you attempt to access an undefined property on an object

console.log( b.someUndefinedProperty );

Javascript will check the parent object(s) for some someUndefinedProperty. If the property is anywhere in the prototype inheritance chain, javascript will return the value from the prototype.

Earlier we said it’s hard for the average javascript client programmer to see javascript’s prototype inheritance in action. That’s because the traditional methods of object creation in javascript

object = {};
object = new SomeConstructorFunction;

lack any direct API for creating parent/child relationships. Early javascript pioneers like Douglas Crockford came up with workarounds for this. Fortunately, more modern versions of javascript have addressed this shortcoming with a third way to create objects — the Object.create method.

Object.create is a method on the global Object object. It creates a new object for you. It requires a single argument — the object you want to use as your new object’s prototype. If you give the following program a try, you can see the discussed parent/child fallback behavior we previously discussed.

var a = {};
a.foo = 'Bar, set on A';          
b = Object.create(a);    
console.log(b.foo);     //logs "Bar, set on A"

Above we’ve defined an object a, then told javascript to instantiate an object b that uses a as a prototype.

One last thing before we move on. This fallback behavior leads to an interesting question — when iterating (foreaching) over a javascript object’s keys/values, should the iterator include keys from the parent object(s)? This is why methods like hasOwnProperty exists, and why every javascript framework has its own method for iterating over a javascript object’s keys and values.

Javascript and “use strict”

Folks trying to improve javascript face the same problem that all language people face.

How do we fix early mistakes without breaking the world

One tool modern javascript developers have in their toolkit is a strict mode. Strict mode removes certain language constructs and behaviors that make optimizing javascript difficult, or that have bene proven to lead to bugs. It’s a way for programmers to opt-in to “better” javascript, but leaves old code to run as is.

For example, with strict mode on, this remains unbound in global functions instead of being bound to window.

function foo(){
    "use strict";
    console.log(this);
}
foo(); //results in "undefined"

Covering every change with “use strict” is beyond the scope of this article, but the MDN use strict article has you covered. What you need to know for Magento 2 is

  1. You invoke strict mode by putting the string “use strict” at the top of a function
  2. When the function finishes executing, strict mode is exited.
  3. If you call another function in a strict context, the called function will not be in strict context (unless it too has "use strict" at the top

It’s possible to have an entire script (i.e. all the code on a page) run in strict mode by making the "use strict" string the first line of javascript your browser sees. So far Magento 2 hasn’t done this, and they likely won’t. This would be an impractical step in a system relying on so much legacy javascript code.

RequireJS Module Loading System

We’ve already covered RequireJS in our PHP for MVC Developers series. You’ll need to be conversant with how Magento and RequireJS work together to get the most out of this new series. There’s also few things worth calling out w/r/t RequireJS and this series.

Consider the following nominal RequireJS module.

// Package_Module/js/lib/some/module/name
define([/* dependencies *]], function([/* dependency arguments*/]){
    //START: javascript code that runs once
    //...
    //END:   javascript code that runs once

    return /* some object, array, function, or string */
});

One thing that wasn’t 100% clear to me when I started using RequireJS was the fact that, if another module lists Package_Module/js/lib/some/module/name as a dependency, the code before the return value only runs once. That is, if other modules also include Package_Module/js/lib/some/module/name as a dependency, RequireJS will have a cached-in-memory version of the returned object, array, function, or string ready to go.

If you’re following RequireJS best practices, the only thing that should wind up in the javascript code that runs once section are private variables and methods/functions (via closure, see below). However, it’s possible that a developer may drop some state changing code here, so you’ll want to be aware that state changing code runs once, and only once.

RequireJS public/private Patterns

Next, consider another nominal module

// Package_Module/js/lib/some/module/name
define([/* dependencies *]], function([/* dependency arguments*/]){
    var somePrivateVariable;

    var somePrivateFunction = function(){
        //...
    }        

    return {
        somePublicVariable:'',
        somePublicFunction:function(){
            console.log(this.somePublicVariable);
            console.log(somePrivateVariable);
            console.log(somePrivateFunction);
        }
    }
});

//later on 
requirejs(['Package_Module/js/lib/some/module/name'], function(moduleName){
    moduleName.somePublicVariable = 'Hello World';
    moduleName.somePublicFunction();    //works        
    moduleName.somePrivateFunction();   //doesn't work, because a method
                                        //was never defined on the object
});

Javascript, as mentioned, does not have classes. Javascript also doesn’t have the concept of public and private access levels. However, thanks to javascript’s implementation of closure, you can simulate public and private methods with the above pattern.

Without getting too deeply into it, all closure means is when you return a function or object from another function, that returned function or object remembers all the variables you defined in the returning function. This means a method inside the object gets to access those “private” variables, but no one from the outside can do the same.

This can be a little difficult to understand the first time you encounter it, because unlike classical OOP the private variables aren’t really part of the object (which is what, ironically, makes them private). However, describing this pattern in terms of classical public/private access levels is common in the javascript world, so you’ll want to get your head around the idea.

RequireJS Aliasing

The next RequireJS feature we’ll want to review is aliasing. Thanks to a number of RequireJS configuration features (map, etc.), it’s possible that the module name you request won’t be the module you get. Magento takes advantage of this feature to give their more commonly used RequireJS modules short names

define(['uiElement', 'mageUtils'], function(UiElement, MageUtils){

});

For simplicity’s sake, unless it matters due to context, we’re going to call these aliases irrespective of the RequireJS config feature that’s doing the swapping. We’ll also assume you’re conversant in finding an alias’s true module name. Here’s the unix one liner we use

$ find vendor/magento/ -name 'requirejs-config.js' | xargs ack 'uiElement'
vendor/magento/module-ui/view/base/requirejs-config.js
12:            uiElement:      'Magento_Ui/js/lib/core/element/element'

Above you can see the uiElement alias corresponds to the Magento_Ui/js/lib/core/element/element module.

Also, as one last parting bit of commentary, it’s important you start thinking about RequireJS as a module system, and not a system for loading javascript files. When you’re programming in a language like python and you want a bit of functionality from the fibo module, you just say

import fibo

and you don’t really worry about where fibo exists on the file system. The same should be true of RequireJS based systems. However, due to javascript’s legacy and Magento’s choices, module names that include file path like portions (js, lib, etc.) aren’t helping us make this mental transition.

Wrap Up

This (almost!) concludes our tour of javascript features, both ancient and modern. While this all can seem intimidating, like most programming topics once you start working with the system on a regular basis, the assumptions and concepts we’ve covered today will start to gel in your mind and you’ll be reasoning about Magento’s systems in no time.

We say almost, because there’s one last javascript feature we want to talk about, but it’s in-depth enough to warrant it’s own article. Next time we’re going to explore methods for debugging the prototype chain in javascript, and explain why it’s probably more complicated than it seems.

Series Navigation<< Magento 2: Defaults, uiElement, Observables, and the Fundamental Problem of Userland Object SystemsTracing Javascript’s Prototype Chain >>

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 5th December 2016

email hidden; JavaScript is required