Introducing UI Components

Contents

  1. Introduction
  2. What is a UI Component
  3. Technologies You Need to Understand
  4. Javascript, KnockoutJS + Magento and UI Components
  5. UI Component Process Flow
  6. Magento Core UI Component Examples

Introduction

Over the course of this article I will give you a whistle stop tour of UI Components.

What is a UI Component?

In a nutshell a UI Component is an easily reusable collection of PHP, Javascript and HTML code that provides one user interface functionality. A UI component can easily be used anywhere within your layout files using only this small snippet.

<uiŠ”omponent name="some_ui_component_instance_name"/>

This will make more sense later, but it also looks like UI Components add the M (model) to KnockoutJS’s MVVM (or really VVM) architecture. This architecture is used to build all of Magento 2’s front end.

Structure

UI Components are made up of ‘Basic Components’ and ‘Secondary components’. The Basic Components are:

The Secondary Components can be used to extend the functionality of the Basic Components. Each Basic Component has it’s own set of Secondary Components. The latest devdocs (2.1) have a partial list of all components here. You can also find all the available UI Components listed in this definition.xml (more on that later).

Important to Note

You Can’t Create Custom UI Components

According to the official devdocs, extension developers cannot create custom UI Components. You can only customise existing ones. However, Alan Storm does have a solution (hack) for getting around this.

UI Components Are Only Really for the Admin

The devdocs state that UI Components can be used on the storefront as well as within the admin. However, you will need to provide all your own styling and the Magento core team have not themselves used any UI Components on the storefront.

Technologies You Need to Understand

Before I dive into any UI Component details there are a number of technologies you must understand first.

XML, XHTML and XSD

What Are These?

If you’ve already worked with Magento 1 then you’ll already be (very) familiar with XML. If not, then here’s a short description from Wikipedia:

Extensible Markup Language (XML) is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable.

XML is used for configuration files as well as the XHTML templates used by UI Components.

So what’s XHTML? From Wikipedia:

Extensible Hypertext Markup Language (XHTML) is part of the family of XML markup languages. It mirrors or extends versions of the widely used Hypertext Markup Language (HTML) … XHTML documents are well-formed and may therefore be parsed using standard XML parsers, unlike HTML, which requires a lenient HTML-specific parser.

The TL;DR here is that unlike standard HTML, XHTML is strict. You can’t use anything that isn’t defined within it’s XSD schema. UI Component templates are all defined using XHTML.

So what’s an XSD schema? From Wikipedia:

XSD (XML Schema Definition), a recommendation of the World Wide Web Consortium (W3C), specifies how to formally describe the elements in an Extensible Markup Language (XML) document. It can be used by programmers to verify each piece of item content in a document.

So the XSD file defines the valid XML nodes and their structure within a given document. This is used to validate the XHTML templates and XML configuration files within Magento 2 as well as create a DSL (Domain Specific Language) for defining UI Components.

Further reading

KnockoutJS

From the KnockoutJS site:

Knockout is a JavaScript library that helps you to create rich, responsive display and editor user interfaces with a clean underlying data model.

What is KnockoutJS?

KnockoutJS uses an MVVM (model–view–view-Model) architecture to create interfaces that update when an underlying data model is changed.

Knockout’s MVVM looks like this:

KnockoutJS MVVM

The Javascript view-model is bound to the HTML view using Knockouts custom data-bind attribute. An example of this can be seen here. Now, when the Javascript data model within the view-model changes any HTML elements which are observing this view-model will be updated. The view-model itself is triggered to update by the same HTML elements. The view-model will update it’s temporary data model and pull data from / trigger updates within the Magento Model as required.

What’s It Used For?

KnockoutJS is used to power the UI Component interfaces (and all other Magento 2 interfaces). It handles templating as well as user interface updates.

KnockoutJS Customisations made for Magento 2

Custom Templating Engine

The standard KnockoutJS templating engine expects the HTML templates to be present within the current page like this:

<script type="text/html" id="template_id">
    ...
</script>

The Magento core team have customised the KnockoutJS templating engine so that it can now load templates from a remote source.

This feature makes it easier to reuse templates as they are no longer embedded within .phtml files.

Scope

The Magento core team have also added a custom scope binding. This binding allows you to easily bind a view-model to a template (HTML) element on a per template basis. For example:

<div data-bind="scope: 'some_view_model.some_view_model'">
    ...
</div>

This knockout view-model (listed under the "components" key within the JSON configuration) is instantiated using the Magento_Ui/js/core/app Javascript component:

<script type="text/x-magento-init">
{
    "*": {
        "Magento_Ui/js/core/app": {
            "components": {
                "some_view_model": {
                    "component": "<Namespace>_<Module>/js/view/someViewModel"
                }
            }
        }
    }
}
</script>

Further reading

RequireJS

From the RequireJS site:

RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code.

What is RequireJS?

RequireJS uses the AMD (Asynchronous Module Definition) standard to provide dependency management and modules. RequireJS uses lazy loading to only load a module when it’s required, this can speed up page load times. By implementing the AMD standard it can also load dependencies asynchronously and provides no guarantees about the order in which a modules dependencies will be fetched.

The asynchronous nature of RequireJS can be an issue for non AMD compliant modules. For example, JQuery plugins that depend on JQuery itself. In these cases RequireJS can be configured to load one (or more) of a modules dependencies after a subset of the other dependencies have loaded using the shim configuration value.

What’s It Used For?

All Magento 2 Javascript code (modules) make use of RequireJS to load their dependencies.

Further reading

Pestle

If you don’t want to spend your time creating module boiler plate code / configuration then you’ll want to check out Pestle. Pestle is a static code generator which provides a number of templates / helpful functions for Magento 2.

$ php pestle list
  ...
  magento2:generate:acl
  magento2:generate:command
  magento2:generate:config_helper
  magento2:generate:crud_model
  magento2:generate:di
  magento2:generate:install
  magento2:generate:menu
  magento2:generate:module
  magento2:generate:observer
  magento2:generate:plugin_xml
  magento2:generate:preference
  magento2:generate:psr_log_level
  magento2:generate:registration
  magento2:generate:route
  magento2:generate:theme
  magento2:generate:ui:add-column-actions
  magento2:generate:ui:add-column-sections
  magento2:generate:ui:grid
  magento2:generate:view
  ...

Further Reading

Javascript, KnockoutJS + Magento and UI Components

Did you know there are three types of component used within the Magento 2 front end?

What is a Javascript (JS) Component?

A JS Component is a subtype of RequireJS module. It’s a RequireJS module that is loaded using a text/x-magento-init script tag and that always returns a function. This function is then called by core Magento code which passes any configuration values you set within the text/x-magento-init script tag as arguments. Magento_UI/js/core/app that we saw above is a good example of this.

<script type="text/x-magento-init">
{
    "*": {
        "Magento_Ui/js/core/app": { <---- Module to be instantiate
            "components": {       ^------ Object with all values to be passed as function arguments
                "some_view_model": {
                    "component": "<Namespace>_<Module>/js/view/someViewModel"
                }
            }
        }
    }
}
</script>

Further Reading

What is a KnockoutJS + Magento component?

A plain KnockoutJS component is the grouping of a KnockoutJS template (HTML) and view-model (JS). This component is then registered with KnockoutJS and provided a unique key. This key can then be used within your standard HTML markup to instantiate the component.

<div data-bind="component:'some-component-key'"></div>

Due to the KnockoutJS customisations listed above this process is not quite the same in Magento. Within Magento the template is loaded via a URL and the view-model is pulled from the registered view-models using the scope binding. These are then linked together and registered as a component.

What’s the difference between the three?

The naming is unfortunate here but to keep this straight in your head it’s probably best to remember:

Further Reading

UI Component Process Flow

The Players

Layout

This is your standard <handle>.xml layout file. You add the UI Component to the layout using:

<uiŠ”omponent name="some_ui_component_instance_name"/>

Module Agnostic Configuration

The definition.xml file contains all default configuration for all the UI Components available in Magento 2. It also defines the PHP classes used to generate any JSON configuration which eventually gets passed to KnockoutJS. Any configuration set within the definition.xml are global and effect all modules.

...
<form class="Magento\Ui\Component\Form">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="component" xsi:type="string">Magento_Ui/js/form/form</item>
        </item>
        <item name="template" xsi:type="string">templates/form/default</item>
    </argument>
</form>
...

The XML above basically says; instantiate the Magento\Ui\Component\Form class by passing the array of arguments contained within <argument> to it’s constructor.

The first argument:

...
<item name="component" xsi:type="string">Magento_Ui/js/form/form</item>
...

Says use magento/module-ui/view/base/web/js/form/form.js as the KnockoutJS view-model for this component.

The second argument:

<item name="template" xsi:type="string">templates/form/default</item>

Means use magento/module-ui/view/base/templates/form/default.phtml as the default template for this UI Component.

Top Level Configuration

The <component_name>.xml file contains any configuration you wish to set on a UI Component at the module level.

This will usually include the arguments to be passed to the UI Components PHP constructor <arguments>, the data source to be used by the UI Component <dataSource> and any Secondary Components to be included.

<form xmlns:xsi="..." xsi:noNamespaceSchemaLocation="...">
    <argument name="data" xsi:type="array">
        ...
    </argument>
    <dataSource name="customer_form_data_source">
        <!-- Covered in more detail below -->
    </dataSource>
    <fieldset name="customer">
        <!-- Secondary Component -->
    </fieldset>
    <fieldset name="address">
        ...
    </fieldset>
</form>

Any nodes added within the <form> node here which are not either <argument> or <dataSource> are considered to be a new Secondary Component instance. These can be recursively nested so a Secondary Component can contain another etc.

Any arguments set here will override the default values set within definition.xml.

AbstractComponent

Each UI Component is backed by a PHP class extending from Magento\Ui\Component\AbstractComponent. This class processes both the top level and module agnostic <argument> values and generates the JSON configuration later used to instantiate a KnockoutJS view-model and assign a template.

View-Model Constructor

In the module agnostic configuration above we configured a view-model. This view-model is a RequireJS module that extends from uiElement. This extended uiElement is then returned from the module to be used as a view-model constructor.

Default values can be set within this constructor; top level and module agnostic configuration values take precedence over these.

If you require KnockoutJS you can also start adding state for your templates to observe and functions to update / manage that state.

define([
    'uiElement',
    'ko'
    ], function(Element, ko){
    return Element.extend({
        "defaults": {
            template: '<Namespace>_<ModuleName>/path/to/template'
        },
        "some_value": ko.observable("value")
    });
});

Template

UI Component templates like collapsible.xhtml are set within either the view-model constructor or the module agnostic or top level configurations.

For the template to be rendered correctly it also needs to include the following comment:

<!-- ko template: getTemplate() --><!-- /ko -->

This calls the getTemplate() function on our view-model. This is inherited from uiElement.

uiRegistry

The uiRegistry is a Magento 2 RequireJS module that’s used (as global state) to store all the currently instantiated view-models. These models are stored as key value pairs. The key here maps to a KnockoutJS scope binding value.

engine.js (Magento_UI/js/lib/knockout/template/engine)

This module implements Magento 2’s custom KnockoutJS templating engine.

bootstrap.js (Magento_UI/js/lib/knockout/bootstrap)

This module requires Magento 2’s custom templating engine and sets it as KnockoutJS’s current templating engine.

layout.js (Magento_UI/js/core/renderer/layout)

This module is required by the the app module (see below) and handles the instantiation of view-model objects. It adds these view-models to the uiRegistry.

app.js (Magento_UI/js/core/app)

This module requires the bootstrap module as well as the layout module. It calls the function returned by the layout module starting the instantiation of view-models.

scope.js (Magento_UI/js/lib/knockout/bindings/scope)

This final module implements Magento 2’s custom scope binding. This module handles the linking of view-models and views using it’s applyComponents() function.

Process Flow Chart


UI Component Process Flow

Video

Here’s a video of me stepping through the above flow in PHPStorm and Chrome.

I’ve extracted the PHPStorm breakpoints and added them to this GitHub repo (just in case you want to have a go yourself).

Further Reading

Magento Core UI Component Examples

Here are some links to the Magento 2 GitHub repo for you to investigate further.

Form

This is a <form> used in the Magento_Theme module.

Listing

This is a <listing> used in the Magento_Customer module.


Tags: magento2xmlxsdxhtmlknockoutjsreactjs