- Magento 2: Introducing UI Components
- Magento 2: Simplest UI Component
- Magento 2: Simplest UI Knockout Component
- Magento 2: Simplest XSD Valid UI Component
- Magento 2: ES6 Template Literals
- Magento 2: uiClass Data Features
- Magento 2: UI Component Data Sources
- Magento 2: UI Component Retrospective
- Observables, uiElement Objects, and Variable Tracking
- Magento 2: uiElement Features and Checkout Application
- Magento 2: Remaining uiElement Defaults
- Magento 2: Knockout.js Template Primer
- Magento 2 UI Component Code Generation
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.
- 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/app
RequireJS module. This module is embedded in the page via anx-magento-init
script, 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”. The
Magento_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, theMagento_Ui/js/core/app
application 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 uses underscore.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 theuiClass
class/module. Most of Magento’s Knockout.js view model constructors areuiElement
objects. -
Above, we mentioned the
uiRegistry
,uiClass
, anduiElement
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
, andMagento_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 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.