Magento 2: uiClass Data Features

magento-logo

Our last article covered Magento’s implementation of ES6 template literals. Near its end, we started to bump up against some features (the defaults array) of Magento’s UI Component focused classical-style object system. Before we can get to UI Component data sources, we’ll need to slog through a few more object system features.

We’ll be running all of today’s code via a browser’s (Google Chrome’s) javascript debugging console/REPL. Also, the specifics here are Magento 2.1.1, but the concept should apply across Magento versions.

The Story so Far

To start with, let’s review what we’ve learned about Magento 2’s front end systems. If any of the following sounds unfamiliar, you may want to review our Advanced Javascriptseries, as well as the previous articles in this UI Component series.

  • Magento uses Knockout.js to render front end interfaces.
  • By default, Knockout.js uses the entire HTML page as a view, and a javascript object for the view model.
  • To use Knockout.js, programmers create a javascript constructor function. Knockout.js uses this constructor function to instantiate a view model.
  • Magento added a custom Knockout.js binding named scope. Scope allows different areas of the page to use different view models.
  • These different view models are instantiated by the Magento_Ui/js/core/appRequireJS module. This module is embedded in the page via an x-magento-initscript, and this module uses a large data structure rendered via the backend’s UI Component classes and XML. This data structure configures a number of RequireJS modules called “components”.
  • These components are “view model constructor factories”. TheMagento_Ui/js/core/app application uses these RequireJS components to create view model constructors, and then uses the instantiated view model constructor function to instantiate view models. Finally, the Magento_Ui/js/core/appapplication registers each instantiated view model object, (by name), with theuiRegistry RequireJS module. The view model’s registered names come from the large data structure rendered via the backend’s UI Component classes and XML.
  • The view model constructor objects are based on a Magento built javascript object system. This object system uses RequireJS modules as classes, and usesunderscore.js for inheritance and method/property sharing. There’s also some Magento secret sauce in there. The base class is the uiClass module. TheuiElement class/module extends from the uiClass class/module. Most of Magento’s Knockout.js view model constructors are uiElement objects.
  • Above, we mentioned the uiRegistry, uiClass, and uiElement RequireJS modules. These are RequireJS “map aliases” that point to the real modules atMagento_Ui/js/lib/registry/registry, Magento_Ui/js/lib/core/class, and Magento_Ui/js/lib/core/element/element respectively.

Today we’re going to look at some features of the uiElement based objects.

Javascript History

For some of you (or at least, for myself) the first thing you’ll find intimidating about Magento’s object system is the use of javascript constructor functions.

In many ways, the dawn of the modern javascript era started with Douglas Crockford’s JavaScript: The Good Parts. This slim volume laid javascript’s lispy heart bare, and helped the world see the language as more than a janky scripting language. Or, depending on your point of view, it dressed up a janky scripting language as a tool for software engineers.

Either way, it’s the volume a lot of people look to when they’re trying to learn how to program javascript.

One of Crockford’s peccadillos was an aversion to the new keyword in javascript. He preferred developers create new objects directly with the object literal syntax

var foo = {};

rather than using constructor functions.

var FooConstructor = function(){
    this.message = "Hello World";
};

var foo = new FooConstructor;    

His reasons were myriad (you should really read The Good Parts – it’s a good book), but the primary one was the ambiguity of javascript constructors. A javascript constructor function is just a regular function. It becomes a constructor when used with the newkeyword – however, it’s still possible to call a constructor function without this keyword. This usually results in javascript just chugging along and your program doing something you likely did not intend. Further complicating things – when you use a constructor function with the new keyword, the magic variable “this” is bound to the object the constructor function is creating. i.e. in our above examples, the variable foo will have amessage property with the string "Hello World" inside. If invoked as a regular function,this is (per standard javascript) bound to the function itself.

This ambiguity creates a class of bugs that are hard to track down, which is why Crockford recommended eschewing javascript constructors and sticking to object literals, factories, and his module pattern. Much of the early “modern javascript” movement followed his lead.

There are, however, programers who think “don’t do that” is bad advice, and started experimenting with javascript constructor functions. There are also programmers who never heard of Douglas Crockford. As time went on, more frameworks started eschewing his advice, and javascript constructor functions remained a thing.

The Knockout.js framework’s view models are based on end-user-programers creating view model constructor functions, and Magento’s object system follows that lead.

Creating uiElement Objects

History lesson out of the way, we’re going to start by instantiating a Magento uiElementobject. Browse to a Magento 2 page, open your browser’s debugging console/REPL, and type the following

var Element = requirejs('uiElement');

Here we’re using the RequireJS shorthand to load a module directly into the current namespace. Normally you would use this inside a RequireJS program or module definition

define(['uiElement'], function(Element){
    //... use Element here ...
});    

What we’ve done is load the uiElement module. The Element variable is our javascript constructor function (i.e. view model constructor). To instantiate a javascript object from this constructor, we’d do the following.

var viewModel = new Element;
console.log(viewModel);

UiClass {_super: undefined, ignoreTmpls: Object, _requesetd: Object, containers: Array[0], exports: Object…}

You may wonder why this object looks like a UiClass object to javascript. That’s beyond the scope of this article, but if you’re curious you can start debugging in uiElement‘s source file.

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

At this point, we now have a uiElement object that’s ready to use as a simple view model. This object also has a number of default properties and methods that Magento’s Knockout.js scope binding parameter, as well as the uiRegistry, expect to find.

Defaults Feature and Data Properties

We’ve already discussed the defaults array in previous articles, but it’s worth reviewing.

With code like the following (give it a try!)

var Element = requirejs('uiElement');
var viewModelConstructor = Element.extend({
    defaults:{
        'message':'Hello World'}
    }
);

viewModel = new viewModelConstructor;
console.log(viewModel.message);
Hello World

we can ensure our instantiated view models have default values. When we use code like this

var viewModelConstructor = Element.extend({...});

It’s sort of like saying the following in PHP.

class viewModelConstructor extends uiElement
{
}

We’re sub-classing the uiElement object/class. We say sort of because, despite some dressing up with classical inheritance, we’re still writing javascript code and using javascript’s object system. Additionally, the extend method comes from the underscore.jslibrary, which is most definitely not a classical inspired object-system. Again, it’s a little beyond the scope of this article, but Magento 2’s javascript object system is its own weird thing. Today we’re going to try and stay concentrated on using this object system instead of getting bogged down in its implementation details.

In addition to setting defaults values in your constructor, you can also set properties at instantiation time. Consider the following program.

var Element = requirejs('uiElement');
var viewModelConstructor = Element.extend({
    defaults:{
        'message':'Hello World'}
    }
);

viewModel = new viewModelConstructor({
    'message':'Goodbye World'
});
console.log(viewModel.message);

Here we’ve set a default message to Hello World. However, at instantiation time we’ve passed in a new object literal as a parameter. Magento will use this object to set data properties on the newly instantiated object — overriding any defaults set via the constructor function.

We’ve reviewed defaults because the last two features we’ll explore today are similar, in that they related to setting property values on Magento’s uiElement view model objects at instantiation time.

A uiRegistry Review

Before we can talk about the imports and exports feature of the defaults array, let’s review the Magento 2 uiRegistry. Navigate to the Customer -> All Customers page in Magento 2’s backend, and then enter the following code in your javascript console.

reg = requirejs('uiRegistry');
var viewModel = reg.get('customer_listing.customer_listing');
console.log(viewModel);

The uiRegistry object is where Magento registers its instantiated view model objects. Above we have fetched the view model registered with the namecustomer_listing.customer_listing. The following code

reg = requirejs('uiRegistry');    
var viewModel = reg.get('customer_listing.customer_listing_data_source');
console.log(viewModel.data.items);

fetches the data source view model, which contains the row data Magento’s grid component needs to render.

The instantiation and registration of these view models happens in theMagento_Ui/js/core/app module.

While this instantiation is beyond the scope of today’s article, behind the scenes Magento’s running code that looks something like this.

requirejs(['Magento_Ui/js/form/components/html', 'uiRegistry'], function(viewModelConstructorFormComponentHtml, registry){
    var viewModel = new viewModelConstructorFormComponentHtml;     
    registry.set('the_name_of_the_view_model', viewModel);
});

That is, Magento uses the configured component (or “view model constructor factory” —Magento_Ui/js/form/components/html above) to fetch a view model constructor, uses that view model constructor to instantiate an object, and then registers that object in the registry. The actual code is, of course, much more complicated, and involves many of the rendered data properties from the ui_component XML. That said, at its heart, view model registration is as simple as the above two line RequireJS program.

The uiRegistry ties in heavily to the features of the uiElement objects we’re exploring today.

Default Imports

The imports feature of Magento 2’s object system allows you to, at the time of instantiation, link a property on your instantiated object with a property in a registereduiRegistry object. Remember our code from above?

var viewModel = reg.get('customer_listing.customer_listing_datasource');
console.log(viewModel.data.items);

With the import feature, we can ensure our objects are linked to thecustomer_listing.customer_listing_datasource object’s data.items row, giving our object access to the grid component’s data. Let’s give it a try.

var Element = requirejs('uiElement');
var viewModelConstructor = Element.extend({
    defaults:{
        'imports':{
            ourLinkedRows:'customer_listing.customer_listing_data_source:data.items'
        }
    }
});         

viewModel = new viewModelConstructor;
console.log(viewModel.ourLinkedRows);

Run the above program, and you’ll see our instantiated object now contains anourLinkedRows property, and that property contains the data source’s data object.

The imports property of the defaults object is an object of key/value pairs.

'imports':{
    ourLinkedRows:'customer_listing.customer_listing_data_source:data.items'
}

The key (ourLinkedRows above) is the property of your instantiated object that you want to populate with data. The value (customer_listing.customer_listing_data_source:data.items above) is a special string that Magento will use to pull data out of the registry. The string is colon (:) separated. The left side of the string (customer_listing.customer_listing_data_source) is the registry key, and the right side of the string (data.items) is the data property.

Using imports, you can give your viewModel easy access to any data currently in theuiRegistry, which means (in turn) your Knockout view can have access to any data from the entire UI Component tree.

Default Exports

The exports feature works similarly, but in reverse. Consider the following program

reg = requirejs('uiRegistry');
var Element = requirejs('uiElement');
var viewModelConstructor = Element.extend({
    defaults:{
        'message':'Hello World',
        'exports':{
            message:'customer_listing.customer_listing_data_source:theMessagePropertyFromExport'
        }
    }
});         

viewModel = new viewModelConstructor({
    'message':'Goodbye World'
});

viewModelObject = reg.get('customer_listing.customer_listing_data_source');
console.log(viewModelObject.theMessagePropertyFromExport);

Here, the exports object

'exports':{
    message:'customer_listing.customer_listing_data_source:theMessagePropertyFromExport'
}

has a key of message, and value ofcustomer_listing.customer_listing_data_source:theMessagePropertyFromExport. With this exports configuration, when our object is instantiated, the uiElement system will look at the message property of the instantiated object, and link it with thetheMessagePropertyFromExport property of the uiRegistry object registered to the keycustomer_listing.customer_listing_data_source.

In other words, the exports feature allows you modify objects that already exist in theuiRegistry.

Wrap Up

The imports and exports features are interesting ones. On the server side, Magento’s core engineering teams seem — obsessed? — with using Gang of Four style design patterns to thinly slice any possible dependency an object may have. However, on the client side, Magento’s core team have built an entire object system that has a huge glaring dependency (the uiRegistry) baked right into their classes, and imports and exports seem like dependency factories. Right or wrong, it’s easy to see why real certified Engineers roll their eyes whenever programmers call themselves Software Engineers.

Regardless, these are the patterns we have to work with in Magento 2, and you’d be wise to learn their ins and outs. With imports and exports covered, and our toes dipped into Magento’s UI Component data source, we’re finally ready to dive deep on how Magento 2 gets server side data into its javascript programs.

Leave a Reply

Your email address will not be published. Required fields are marked *