- 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
After last time’s observables primer, we’re ready to jump back into some related features of the UI Component system and uiElement
based objects.
There’s no getting past it — this is an advanced tutorial. We’ll do our best to point you in the right direction, but you’ll need to have a grounding in Magento’s Advanced Javascript features as well as the UI Component system. If this doesn’t make sense it’s not because you’re not intelligent, it’s just because it’s new, confusing, and the systems we’re discussing aren’t fully baked.
We’ll run all our code examples in the javascript console (View -> Developer -> Javascript Console
in Google Chrome) of a loaded Magento 2 page. Our examples will also load modules via the requirejs
function — normally we would load these modules as part of a define
or require
dependency list. Finally, the specifics here were tested against Magento 2.1.1, but the concepts should apply across all Magento 2 versions.
Using Observables as Object Properties
We’re going to start with a common pattern Magento uses with uiElement
derived objects. First, let’s create a uiElement
derived object.
//get the constructor function
UiElement = requirejs('uiElement');
//create the object
ourViewModel = new UiElement;
Our object created, give the following a try
UiElement = requirejs('uiElement');
ourViewModel = new UiElement;
//get the knockout library object
ko = requirejs('ko');
//create a "observable object" and assign it to the `title`
//property of our object
ourViewModel.title = ko.observable('Default Title');
//view the value of the observable object
console.log(ourViewModel.title());
//set a new value for the observable object
ourViewModel.title('A new Title');
//view the new value of the observable object
console.log(ourViewModel.title());
This is pretty standard observable stuff. If a Knockout template accesses the title
value via a data-binding, any later call to our observable’s “setter” will trigger a UI re-render.
Magento’s uiElement
derived objects (including the important uiComponent
and uiCollection
constructor functions) make heavy use of ko.observable
s as data properties. This makes sense, as these objects are the view models Magento’s custom scope
binding uses to access data from template. Magento is so fond of observables, that there’s some extra observables features baked into uiElement
derived objects.
Subscriber Wrapper
Give the following a try
ourViewModel.on('title', function(value){
console.log("Someone just set the title to: " + value);
});
ourViewModel.title("A Third Title")
You should see the output
Someone just set the title to: A Third Title
displayed on your screen. The on
method is a special event handling method that every uiElement
has (and comes from the Magento_Ui/js/lib/core/events
module). As in “on this event (the first argument), call this function (the second argument)”.
If you pass (as the first argument, title
above) the name of an object property, and that property contains an observable, the uiElement
class will automatically setup a Knockout.js subscriber for that observable. If you’re not familiar with what a subscriber is, you may want to read our Knockout Observables for Javascript Programmers article.
Tracking any Property
The uiElement
derived objects also have the ability to automatically setup any property for tracking. In order to take advantage of this feature, we’ll need to create a new constructor function from uiElement
(i.e. create a new UI Component class with uiElement
as its ancestor). Give the following a try
UiElement = requirejs('uiElement');
//define a new constructor function based on uiElement
OurClass = UiElement.extend({
defaults:{
tracks:{
title: true,
foo: true
}
}
});
//create a new object
ourViewModel = new OurClass;
//set a default value
ourViewModel.title = 'a default value';
//setup a callback
ourViewModel.on('title', function(value){
console.log("Another callback: " + value);
});
//set the value (notice the normal javascript assignment)
//and you should see the "Another callback" output
ourViewModel.title = 'a new value';
If you run the above program, you should see your callback function (from ourViewModel.on
) called.
While this is similar to using an observable, there are a few differences. The first difference is the tracks
parameter in the defaults
object (if you’re not familiar with defaults
, you should review the UiComponent series, in particular The uiClass Data Features article)
OurClass = UiElement.extend({
defaults:{
tracks:{
title: true,
foo: true
}
}
});
The tracks
object is a set of key/value pairs. The key
is the object property name you want to track, the value is a boolean true
. This must be an actual boolean, not just a truth-y value. When you configure a tracks
default, you’re telling Magento
Hey, if someone uses this constructor function to create an object, make the following properties (
title
andfoo
above) trackable
The other difference? If you look at how we’re setting a value
ourViewModel.title = 'a new value';
you’ll see we’re using a regular assignment instead of calling an observable function.
The tracks
default is a neat, and powerful, feature. Once the novelty wears off, you may be left wondering how this even works.
The knockout-es5 Library
To talk about the tracks
feature, we’ll need to talk about Steve Sanderson’s knockout-es5 plugin. The knockout-es5 plugin adds support for ECMAScript 5 properties. Magento includes the knockout-es5 plug as part of its front end enviornment.
This plugin adds a ko.track
function that lets you track properties on an object without setting up observables for each property. Or, to be more accurate, without manually setup observers. When you call track
, the plugin will automatically create observables for the object’s properties, and swap these observables in as ES5 setters and getters. The theory is this offers a cleaner syntax than the sometimes awkward observable objects.
Behind the scenes, the uiElement
‘s tracks
feature uses knockout-es5 to implement its tracking. If you take a look at our object in a debugger, we’ll see that title
has both a setter, and a getter, and that these are an observable function/object.
Update: This article previously contained a warning against directly using the track
function that powers the tracks
default.
track: function (properties) {
this.observe(true, properties);
return this;
},
This warning was off-base. The problem I was seeing with track was not a bug in the uiElement
object, but my being forgetful about observables only firing when a value changes. i.e.
object.foo = 'Hello';
object.track('foo');
object.on('foo', function(value){
console.log("Fired!");
});
//won't fire, because same value
object.foo = "Hello";
//will fire
object.foo = "Hello Again";
So, rather than a cautionary tale about systems level APIs being public, instead this should serve as a warning about the difficulties in trusting an undocumented system.
Tracked Variables are Observables. Mostly.
Because they’re hidden behind setters and getters, and not part of the official Knockout.js API (i.e. they come from a plugin and custom Magento code), these tracked variables can sometimes produce strange results. For example, they’ll fail a Knockout isObservable
test
> ko.isObservable(ourViewModel.title);
false
But you can still use Knockout’s getObservable
method to fetch the underlying observable object
observableFunctionObject = ko.getObservable(ourViewModel, 'title')
console.log(observableFunctionObject());
Also, in case it’s not obvious, if you data-bind
(via Knockout.js) a DOM node to a track
ed variable, Knockout.js will treat the DOM node as an observable and re-render your UI whenever the variable is updated.
Wrap Up
That’s it for our whirlwind tour of the uiElement
observable event/subscriber on
method and related variable tracking features. These two competing approaches (properties as observables vs. knockout-es5 tracked variables) are yet another sign that point to Magento’s javascript frameworks being rushed out the door in a not fully baked way.
We don’t have any clear recommendations on “the right” way to develop your own javascript based UIs in Magento 2 — but regardless of your own approach, you’ll need to be aware that Magento’s core code uses both, and plan your debugging accordingly.