Categories


Archives


Recent Posts


Categories


Observables, uiElement Objects, and Variable Tracking

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!

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.observables 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 and foo 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.

Screenshot of setters/getters from javascript console

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 tracked 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.

Series Navigation<< Magento 2: UI Component RetrospectiveMagento 2: uiElement Features and Checkout Application >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 16th November 2016