Categories


Archives


Recent Posts


Categories


Magento 2: Simplest UI Knockout Component

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!

Last time we created (with the help of some <preference/> hackery) the simplest possible Magento 2 UI Component. If you made it all the way through, I bet you were a little disappointed that we left out the javascript. Today we’ll try to sooth that disappointment.

There’s two big reasons we didn’t discuss javascript last time.

The first was complexity — between the XML, the fact UI Component files are a new domain specific language (DSL), the need to side step XSD validation, and the strangeness of an XHTML template system, adding the new javascript systems on top of that felt like too much.

The second reason was — it’s not 100% clear where Magento’s new RequireJS and Knockout.js systems start, and the UI Component systems begin.

As always, make sure your system is running in developer mode, and keep in mind the specifics here refer to Magento 2.1.1, but the concepts should apply across versions. You really should work your way through the previous article before continuing, but if you’re the reckless type we’ve put up our starting module on GitHub (you want the first-pass-unstable module).

An App for Knockout.js View Models

Articles two through six in our Magento 2 for PHP MVC developers series covered the basics of serving and using frontend files in Magento 2. In our short Advanced Javascript series, we covered Magento’s x-magento-init scripts, and the ground-up basics of Magento’s Knockout.js implementation. Back in July we also covered some of the strange tags you’ll find in Magento’s Knockout.js templates. You can probably get something out of this article without having read those previous articles, but they’ll help tremendously if you lose your footing here.

When we finished up our last article, we had managed to create a Pulsestorm_SimpleUiComponent module with a simple UI Component configuration. The rendered UI Component included an x-magento-init section that looked something like this.

<script type="text/x-magento-init">
    {
        "*": {
            "Magento_Ui/js/core/app": {
                "types": {
                    "dataSource": [],
                    "html_content": {
                        "extends": "pulsestorm_simple",
                        "component": "Magento_Ui\/js\/form\/components\/html"
                    },
                    "pulsestorm_simple": {
                        "extends": "pulsestorm_simple"
                    }
                },
                "components": {
                    "pulsestorm_simple": {
                        "children": {
                            "pulsestorm_simple": {
                                "type": "pulsestorm_simple",
                                "name": "pulsestorm_simple",
                                "children": {
                                    "example_content": {
                                        "type": "html_content",
                                        "name": "example_content",
                                        "config": {
                                            "component": "Pulsestorm_SimpleUiComponent\/js\/pulsestorm_simple_component_child",
                                            "content": null
                                        }
                                    }
                                },
                                "config": {
                                    "component": "uiComponent"
                                }
                            },
                            "hello_world_data_source": {
                                "type": "dataSource",
                                "name": "hello_world_data_source",
                                "dataScope": "pulsestorm_simple",
                                "config": {
                                    "data": {
                                        "foo": "bar"
                                    },
                                    "params": {
                                        "namespace": "pulsestorm_simple"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }    
</script>

This x-magento-init script will pass the JSON object into the RequireJS program located in the Magento_Ui/js/core/app module. If we take a look at that module’s source file (info on deriving the file name is over here).

//File: vendor/magento//module-ui/view/base/web/js/core/app.js
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
define([
    './renderer/types',
    './renderer/layout',
    '../lib/knockout/bootstrap'
], function (types, layout) {
    'use strict';

    return function (data, merge) {
        types.set(data.types);
        layout(data.components, undefined, true, merge);
    };
});

This seemingly simple program is actually one of the most important javascript files in Magento’s UI Component system. This code (or more specifically, the code in the Magento_Ui/js/core/renderer/types and Magento_Ui/js/core/renderer/layout modules) is responsible for creating and registering any and all Knockout.js view model constructor objects.

A view model is the javascript object that Knockout.js’s inline javascript uses to fetch data and/or perform complex actions. If you’ve worked your way through the official Knockout.js tutorials and our Advanced Javascript Tutorial series you should have a solid understanding of view model basics.

Less familiar though will be the registration of these view model constructor objects. We’re not going to cover this registration in full — just know that after Magento_Ui/js/core/app runs Magento will have added a number of view model constructor objects to a global registry.

The quickest way to understand this is to take a look at the registry on a core grid page. Navigate to the product grid listing at Products -> Catalog and open up your browser’s javascript debugger (View -> Developer -> Javascript Console in Google Chrome).

The uiRegistry

Magento registers each Knockout.js view model constructor into the object returned by the uiRegistry RequireJS module. I wasn’t deeply involved in the javascript world when the AMD specification came to life, so I’m not sure if this storing of global state in a module is considered a good practice or not, but it’s what Magento does so it’s best to accept it and move on.

The uiRegistry is a key in a RequireJS map.

// File: vendor/magento/module-ui/view/base/requirejs-config.js
var config = {
    paths: {
        'ui/template': 'Magento_Ui/templates'
    },
    map: {
        '*': {
            uiElement:      'Magento_Ui/js/lib/core/element/element',
            uiCollection:   'Magento_Ui/js/lib/core/collection',
            uiComponent:    'Magento_Ui/js/lib/core/collection',
            uiClass:        'Magento_Ui/js/lib/core/class',
            uiEvents:       'Magento_Ui/js/lib/core/events',
            uiRegistry:     'Magento_Ui/js/lib/registry/registry',
            uiLayout:       'Magento_Ui/js/core/renderer/layout',
            buttonAdapter:  'Magento_Ui/js/form/button-adapter'
        }
    }
};

This key points to the actual module — Magento_Ui/js/lib/registry/registry, defined in vendor/magento/module-ui/view/base/web/js/lib/registry/registry.js. The registry object functions similarly to a dictionary or hash map — you can use the registry set method to set a value, and use the registry’s get method to fetch a value. Let’s give that a try. First, load the uiRegistry module/object in your debugger.

> reg = requirejs('uiRegistry');
Registry {}

You won’t be able to see the items in the debugger. Magento’s core team programmed the registry in such a way that the data properties are private — the get method is how you’ll want to fetch a registered value. Give the following a try

> reg.get('product_listing.product_listing');
UiClass {_super: undefined, ignoreTmpls: Object, _requesetd: Object, 
    containers: Array[0], exports: Object…}

Here we’ve fetched the Knockout.js view model registered with the name product_listing.product_listing.

Where the uiRegistry differs from your average dictionary or hash map is its get method supports a query syntax for fetching items. You can find a brief description of this query language in the vendor/magento/module-ui/view/base/web/js/lib/registry/registry.js definition file. We’ll skip to the chase though and let you know there’s support for a callback method that will fetch every object in the registry. Give the following a try

> reg.get(function(item){
    console.log(item.name);
    console.log(item);
});
//long list of view model constructor and names snipped

This callback query lets us work around the private data member problem, and peek at all the registered view models.

Configuring a View Model Constructor

The product listing grid contains a wealth of view models, but let’s return to our simpler model. Navigate back to our page at System -> Other Settings -> Hello Simple UI Component. If we try the javascript debugging method here

> reg = requirejs('uiRegistry');
reg.get(function(item){
    console.log(item.name);
    console.log(item);
});    
undefined      

We’ll get no results. The UI Component system does not automatically create view models. We need to configure our UI Component with a RequireJS module, and then program that module to return a view model constructor.

To start, we’ll need to add the following configuration node to our definition.xml file.

#File: app/code/Pulsestorm/SimpleUiComponent/view/base/ui_component/etc/definition.xml
<components xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_definition.xsd">
    <pulsestorm_simple class="Pulsestorm\SimpleUiComponent\Component\Simple">
        <argument name="data" xsi:type="array">
            <!-- ... -->
            <item name="config" xsi:type="array">
                <item name="component" xsi:type="string">Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component</item>
            </item>            
        </argument>        
    </pulsestorm_simple>
</components> 

Here we’ve added an item node named config and given it a sub-node named component. The value of this node, Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component, is the name of our RequireJS module. If you clear your cache, reload with the above in place, and view the rendered source of the UI Component, you’ll see the following in that blob of JSON.

"components": {
    "pulsestorm_simple": {
        "children": {
            "pulsestorm_simple": {
                "type": "pulsestorm_simple",
                "name": "pulsestorm_simple",
                "config": {
                    "component": "Pulsestorm_SimpleUiComponent\/js\/pulsestorm_simple_component"
                }
            },
//...

However, you’ll also see the following error in your javascript console.

error message screen shot

The Magento_Ui/js/core/app tried to load our Pulsestorm_SimpleUiComponent\/js\/pulsestorm_simple_component, but failed. Let’s fix that. Add the following file, clear the cache, and reload the page

//File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/web/js/pulsestorm_simple_component.js
define([], function(){
    console.log("Called");
});         

With the above in place, you’ll see a new error in the console

Called    
Uncaught TypeError: Constr is not a constructor    

This is progress. We know Magento loaded our RequireJS module — the Called text makes this clear. However, our module failed to return a view model constructor. Let’s fix that — make your javascript file match the following

//File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/web/js/pulsestorm_simple_component.js    
define(['uiElement'], function(Element){
    viewModelConstructor = Element.extend({
        defaults: {
            template: 'Pulsestorm_SimpleUiComponent/pulsestorm_simple_template'
        }
    });

    return viewModelConstructor;
});  

Here we’re importing the uiElement RequireJS module, using that module’s extend method to create a new object with some data, and then we return that new object. This object is our view model constructor.

The uiElement module (a RequireJS map key to Magento_Ui/js/lib/core/element/element) is part of Magento’s custom class based javascript object system, built for the UI Component system. It’s beyond the scope of this article to cover this object system in full, but it’s based on underscore JS, and this quickie is a good place to start if you’re the curious type.

The template property of the defaults object above defines the Magento 2 Knockout.js remote template our view model should use.

Hooking up the View Model

With the above in place, if we clear our Magento cache, reload the page, and enter the following in the javascript debugger

reg = requirejs('uiRegistry');
//hold your questions on pulsestorm_simple.pulsestorm_simple
//we'll get there in a second
viewModelConstructor = reg.get('pulsestorm_simple.pulsestorm_simple')

we’ll see a single returned view model.

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

Our next step is linking this view model constructor with a DOM node in our HTML page. This is where Magento’s special Knockout.js scope binding comes into play. Edit your UI Component’s XHTML template so it matches the following.

<!-- File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/ui_component/templates/different.xhtml -->
<?xml version="1.0" encoding="UTF-8"?>
<div>
    <h1>Hello Brave New World</h1>
    <div data-bind="scope: 'pulsestorm_simple.pulsestorm_simple'" class="entry-edit form-inline">
        <!-- ko template: getTemplate() --><!-- /ko -->
    </div>    
</div>       

Here we’ve done two things. First, we’ve added the following attribute: data-bind="scope: 'pulsestorm_simple.pulsestorm_simple'" This attribute invokes Magento’s Knockout.js scope binding. The scope binding takes a single argument (pulsestorm_simple.pulsestorm_simple above). Magento will use this argument to lookup a view model in the uiRegistry, and make this view model the current Knockout.js view model for every inner node. The scope data binding allows you to have different Knockout.js view models used on different parts of the page.

The second thing we’ve done is include the following “tag-less” Knockout.js binding: <!-- ko template: getTemplate() --><!-- /ko -->. This will render the current view model’s template. The getTemplate method is one of the methods we get “for free” by basing our view model on the uiElement class above.

With the above in place, if we clear our cache and reload the page, we’ll see the following error.

Unable to resolve the source file for ‘adminhtml/Magento/backend/enUS/PulsestormSimpleUiComponent/template/pulsestormsimpletemplate.html’ #0 /path/to/magento/vendor/magento/framework/App/StaticResource.php(97): Magento\Framework\View\Asset\File->getSourceFile() #1 /path/to/magento/vendor/magento/framework/App/Bootstrap.php(258): Magento\Framework\App\StaticResource->launch() #2 /path/to/magento/pub/static.php(13): Magento\Framework\App\Bootstrap->run(Object(Magento\Framework\App\StaticResource)) #3 {main}

Whoops! Back when we configured a template

//File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/web/js/pulsestorm_simple_component.js    
defaults: {
    template: 'Pulsestorm_SimpleUiComponent/pulsestorm_simple_template'
}

we forgot to create the template file. Let’s do that now. If you create a file for the Pulsestorm_SimpleUiComponent/pulsestorm_simple_template template URN

<!-- File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/web/template/pulsestorm_simple_template.html -->
<h1>Rendered with Knockout.js</h1>

and then clear your cache and reload the page, you should see a rendered template.

Congratulations, you just created your first Knockout.js based Magento UI Component.

Using Knockout

Of course, all of this seems like a lot of work to render a static HTML template. If you want to really take advantage of Knockout.js, you’ll need to import Knockout into your RequireJS module.

For example, to get knockout-observable data into the following data-bind="text: message" binding

<!-- File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/web/template/pulsestorm_simple_template.html -->
<h1>Rendered with Knockout.js</h1>
<strong data-bind="text: message"></strong>

You’ll create your view model constructor like this

//File: vendor/magento//module-ui/view/base/web/js/core/app.js
define(['uiElement','ko'], function(Element, ko){
    viewModelConstructor = Element.extend({
        defaults: {
            template: 'Pulsestorm_SimpleUiComponent/pulsestorm_simple_template'
        },
        message: ko.observable("Hello Knockout.js!")    
    });

    return viewModelConstructor;
});

Above, we’ve imported the ko module into our module. This ko module is a replacement for the global ko normally available in Knockout.js. We’ve also added a message property to our view model constructor, and set this property to a ko.observable object. This is nuts and bolts Knockout.js coding. If you reload the page, you should see the Hello Knockout.js text rendered in the strong tag.

Since the message is a Knockout.js observable, we can change it with the following (try it out via the javascript debugger)

> reg = requirejs('uiRegistry');
> reg.get('pulsestorm_simple.pulsestorm_simple').message("Change Me");

The above code snippet uses the uiRegistry to fetch our view model (with the name pulsestorm_simple.pulsestorm_simple), and then call the observable message property.

Modern Javascript and the Browser Debugger

One of the challenges in working with Magento 2’s (and a lot of other modern) javascript is keeping track of what is and isn’t loaded. It’s no longer as simple as viewing your page source and looking for a <script/> tag.

For Google Chrome’s debugger, if you’re looking for your RequireJS module(s), the Source tab is what you want

If you’re looking for your Knockout.js remote template, Network -> XHR is your friend

You’ll want to pay particular attention to the actual text these debugging tools report. Between Magento’s own cache and some aggressive headers set by Magento’s custom front-end file serving application, the files you’re working with on disk may not be the files your browser sees. In addition to clearing our your Magento cache, a full browser cache refresh is another good sanity check to use during development.

Why the Double Name

One thing that may be bothering you is the “double naming” of the view model constructor.

product_listing.product_listing
pulsestorm_simple.pulsestorm_simple

This name comes from the invoked name in our layout handle XML file/ui_component/*.xml filename

<!-- File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/ui_component/pulsestorm_simple.xml -->        
<uiComponent name="pulsestorm_simple"/>    

However — based on the examples we’ve provided so far, it’s not 100% clear why we need to use the name twice, and in a fashion that implies a hierarchy of some kind. This is where the final features of the UI Component system come into play, and the features that will let us fully understand the listing and form components that ship with Magento.

First, let’s go back to our definition.xml file and change the configured component.

#File: app/code/Pulsestorm/SimpleUiComponent/view/base/ui_component/etc/definition.xml
<components xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_definition.xsd">
    <pulsestorm_simple class="Pulsestorm\SimpleUiComponent\Component\Simple">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <!-- <item name="component" xsi:type="string">Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component</item> -->
                <item name="component" xsi:type="string">uiComponent</item>
            </item>            
        </argument>        

    </pulsestorm_simple>
</components>  

Here we’ve replaced our Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component component/view model constructor with a uiComponent. This is another RequireJS map key that points to the Magento_Ui/js/lib/core/collection module.

If we clear our cache and reload with the above in place, we’ll see our template is no longer rendered. This makes sense — different view model, different template. If we take a look at the view model’s template URN in the javascript debugger.

> reg = requirejs('uiRegistry');
> viewModelConstructor = reg.get('pulsestorm_simple.pulsestorm_simple')
> viewModelConstructor.getTemplate()
ui/collection

We’ll see the template’s URN is a ui/collection. This corresponds to the following file.

<!-- File: vendor/magento//module-ui/view/base/web/templates/collection.html -->
<!--
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<each args="data: elems, as: 'element'">
    <render if="hasTemplate()"/>
</each>

If you’re new to Magento 2’s frontend code, the tags probably confuse you. These tags are a Magento 2 extension to the Knockout.js rendering engine — Magento expands these into Knockout.js tag-less bindings. We wrote about this a bit over on Magento Quickies. In plain Knockout.js, the above looks like the following

<!-- ko foreach: {data: elems, as: 'element'} -->
    <!-- ko if: hasTemplate() --><!-- ko template: getTemplate() --><!-- /ko --><!-- /ko -->
<!-- /ko -->

This Knockout.js template foreachs over an elems property of our view model, and if the object inside elems has a template, it renders that template. If we look at elems

> reg = requirejs('uiRegistry');
> viewModelConstructor = reg.get('pulsestorm_simple.pulsestorm_simple')
> viewModelConstructor.elems()
[]

We see its an empty array. So how can we populate this array? Via UI Component configuration!

Let’s add the following to our UI Component.

<!-- File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/ui_component/pulsestorm_simple.xml -->
<pulsestorm_simple xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <!--  ... -->
    <htmlContent name="first_ever_child">
        <argument name="block" xsi:type="object">Magento\Framework\View\Element\Text</argument>
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="component" xsi:type="string">Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component</item>
            </item>            
        </argument>         
    </htmlContent>
</pulsestorm_simple>

Here we’ve added an <htmlContent/> sub-node to our pulsestorm_simple.xml file. This is a stock UI Component node Magento provides in definition.xml. The specific UI node isn’t important — what’s important is we’ve configured this node with our Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component component, and it’s a sub-node of pulsestorm_simple.

Clear your cache, and reload the page. You should see the rendered template from Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component again!

screenshot

More interesting to us is the uiRegistry.

> reg = requirejs('uiRegistry');
> reg.get(function(item){
    console.log(item.name);
})
undefined
pulsestorm_simple.pulsestorm_simple
pulsestorm_simple.pulsestorm_simple.first_ever_child
undefined

Here we see a hierarchy of components defined. If we go back to our UI Component XML and add another node.

<!-- File: app/code/Pulsestorm/SimpleUiComponent/view/adminhtml/ui_component/pulsestorm_simple.xml -->    
<pulsestorm_simple xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <!--  ... -->
    <htmlContent name="first_ever_child">
        <argument name="block" xsi:type="object">Magento\Framework\View\Element\Text</argument>
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="component" xsi:type="string">Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component</item>
            </item>            
        </argument>         
    </htmlContent>

    <htmlContent name="second_ever_child">
        <argument name="block" xsi:type="object">Magento\Framework\View\Element\Text</argument>
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="component" xsi:type="string">Pulsestorm_SimpleUiComponent/js/pulsestorm_simple_component</item>
            </item>            
        </argument>         
    </htmlContent>        
</pulsestorm_simple>

and clear cache/reload, we’ll see the template rendered twice

and another node added to the hierarchy.

> reg = requirejs('uiRegistry');
> reg.get(function(item){
    console.log(item.name);
})
pulsestorm_simple.pulsestorm_simple
pulsestorm_simple.pulsestorm_simple.first_ever_child
pulsestorm_simple.pulsestorm_simple.second_ever_child

While there are many ways you could use the UI Component system to compose your Magento frontend code, in the end this is how it’s primarily used in Magento 2. The uiComponent/Magento_Ui/js/lib/core/collection module collects and renders a series of Knockout.js view models.

The root level UI Component node is responsible for rendering an XHTML template, but if the configuration for this code includes a uiComponent component attribute, and the XHTML template invokes this component via a scope binding, the sub-nodes of the UI Component tree become named view models in the uiRegistry. Somewhat confusingly, the root node is also registered as a view model constructor, which is where the pulsestorm_simple.pulsestorm_simple comes from.

Wrap Up

Phew! It was quite a journey, but you should now have a better understanding of one of Magento 2’s more mysterious new systems. That said, there’s still plenty to explore in the UI Component system. In our next few articles, we’re going to dive even deeper, and discuss how UI Components access the data created by the <dataProvider/> node, and revisit our “simplest” UI Component to see if there’s a way to use the system without resorting to a class <preference/>.

Originally published September 16, 2016
Series Navigation<< Magento 2: Simplest UI ComponentMagento 2: Simplest XSD Valid UI Component >>