Categories


Archives


Recent Posts


Categories


Magento 2: uiClass Data Features

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!

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 Javascript series, as well as the previous articles in this UI Component series.

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 new keyword – 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 a message 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 uiElement object. 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.js library, 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 name customer_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 the Magento_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 registered uiRegistry 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 the customer_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 an ourLinkedRows 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 the uiRegistry, 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 of customer_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 the theMessagePropertyFromExport property of the uiRegistry object registered to the key customer_listing.customer_listing_data_source.

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

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.

Originally published October 3, 2016
Series Navigation<< Magento 2: ES6 Template LiteralsMagento 2: UI Component Data Sources >>