Categories


Archives


Recent Posts


Categories


Magento 2: Simplest XSD Valid UI 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!

In our previous two articles we ran through creating a new UI Component from scratch. While we were successful, we needed to add a class <preference/> (i.e. a rewrite) that disabled Magento 2’s XSD validation. While this was useful as a learning exercise, it’s not that helpful in a real world.

It’s probably OK for a developer managing a single Magento system to use a class <preference/>, but those sorts of developers probably aren’t creating UI Components. For an extension developer, disabling XSD validation with a class preference seriously compromises the stability of the systems an extension will be deployed to.

This problem with the XSD files sheds light on an untrue thing the software industry likes to tell itself over and over. Namely, that it’s possible, purely through use of advanced design patterns, to crete a system that will be both flexible and stable for developers who didn’t create the system.

It’s clear that Magento 2 set out to create a flexible system for developers — plugins, dependency injection, RequireJS map features, etc. However, the XSD schema files (a tool meant to reign in the complexity of the XML files) ended up limiting the flexibility of the UI Component system.

Whatever role systems design, and gang-of-four style design patterns play, time and time again we rediscover that the only way to build flexible and stable systems for your end-user-programmers is to listen to them and adjust the system over time toward real world usage patterns. Even if (or especially if) this means abandoning whatever high minded ideals your project’s trying to bring to the table.

All that, however, is a topic for another time (and possibly for all-time). Today, carrying with us the lessons learned so far, we’re going to revisit our simple UI Component. This time though we’re going to do it with XSD validation on, and come up with a pattern we can reuse in real world projects.

The specifics here were tested against Magento 2.1.1, but the concepts should apply across all Magento 2 versions.

Setup

The first thing we’ll want to do is disable our Pulsestorm_SimpleUiComponent module. We’re doing this to reenable the XSD validation for XML files. Run the following command, and you should be all set. If you never created a Pulsestorm_SimpleUiComponent module, this step is not necessary.

$ php bin/magento module:disable Pulsestorm_SimpleUiComponent
The following modules have been disabled:
- Pulsestorm_SimpleUiComponent

Cache cleared successfully.
Generated classes cleared successfully. Please run the 'setup:di:compile' command to generate classes.
Info: Some modules might require static view files to be cleared. To do this, run 'module:disable' with the --clear-static-content option to clear them.

Once we’ve done that, we’ll want to create a new admin module for our UI Component. As usual, we’ll use pestle for this.

pestle.phar generate_module Pulsestorm SimpleValidUiComponent 0.0.1

pestle.phar generate_acl Pulsestorm_SimpleValidUiComponent Pulsestorm_SimpleValidUiComponent::top,Pulsestorm_SimpleValidUiComponent::menu_1

pestle.phar generate_menu Pulsestorm_SimpleValidUiComponent Magento_Backend::system_other_settings Pulsestorm_SimpleValidUiComponent::a_menu_item Pulsestorm_SimpleValidUiComponent::menu_1 "Hello Simple Valid Ui Component" pulsestorm_simplevaliduicomponent/index/index 1

pestle.phar generate_route Pulsestorm_SimpleValidUiComponent adminhtml pulsestorm_simplevaliduicomponent

pestle.phar generate_view Pulsestorm_SimpleValidUiComponent adminhtml pulsestorm_simplevaliduicomponent_index_index Main content.phtml 1column

php bin/magento module:enable Pulsestorm_SimpleValidUiComponent

php bin/magento setup:upgrade

With the above in place, clear your cache, and you should be able to navigate to a System -> Other Settings -> Hello Simple Valid Ui Component menu in your Magento backend.

With our boilerplate generated, lets get started!

A New UI Components

As we did earlier in this series, we’ll add a new <uiComponent/> to our layout handle XML file.

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/layout/pulsestorm_simplevaliduicomponent_index_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <referenceBlock name="content">
        <block template="content.phtml" class="Pulsestorm\SimpleValidUiComponent\Block\Adminhtml\Main" name="pulsestorm_simplevaliduicomponent_block_main" />

        <uiComponent name="pulsestorm_simple_valid"/>

    </referenceBlock>
</page>

and then we’ll create an XML file for this named UI Component.

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<pulsestorm_simple_valid xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
</pulsestorm_simple_valid>

If we clear our cache and reload the page, we’ll end up with an error something like this.

Exception #0 (Magento\Framework\Exception\LocalizedException): Element
'pulsestorm_simple_valid': No matching global declaration 
available for thevalidation root.

As previously mentioned, this error happens because there’s no pulsestorm_simple_valid in Magento’s vendor/magento/module-ui/view/base/ui_component/etc/definition.xml file, and we can’t add one because Magento’s XSD schema validation file for UI Component files doesn’t allow root nodes named <pulsestorm_simple_valid/>.

Unfortunately, there’s no way we can work around this. The schema is the schema. Even if we removed the xsi:noNamespaceSchemaLocation from our XML files, Magento’s merging our nodes into XML trees that use this XSD file. As of Magento 2.1, there’s no way for third party developers to distribute UI Components without some half-baked module that disables or modifies the XSD validation routines. This is disappointing.

However, we can still take advantage of the <uiCompnent/> tag if we change our root node to the following.

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
</container>

The <container/> node is a valid root level node. If we reload with the above in place, we’ll end up with the following error instead.

Fatal error: Method Magento\Ui\TemplateEngine\Xhtml\Result::__toString()
must not throw an exception, caught Error: Call to a member function
getConfigData() on null in 
/path/to/magento/vendor/magento/module-ui/
Component/Wrapper/UiComponent.php on line 0

In other words, our schema validation errors are gone, and Magento’s just complaining about a missing dataSource node. We can fix that with a new dataSource node

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <dataSource name="pulsestorm_simple_valid_data_source">                        
        <argument name="dataProvider" xsi:type="configurableObject">
            <!-- the PHP class that implements a data provider -->
            <argument name="class" xsi:type="string">Pulsestorm\SimpleValidUiComponent\Model\DataProvider</argument>    

            <!-- redundant with the `dataSource` name -->
            <argument name="name" xsi:type="string">pulsestorm_simple_valid_data_source</argument>

            <!-- required: means ui components are meant to work with models -->
            <argument name="primaryFieldName" xsi:type="string">entity_id</argument>

            <!-- required: means ui components are meant to work with URL passing -->
            <argument name="requestFieldName" xsi:type="string">id</argument>
        </argument>        
    </dataSource>
</container>

and a new data provider class

#File: app/code/Pulsestorm/SimpleValidUiComponent/Model/DataProvider.php  
<?php
namespace Pulsestorm\SimpleValidUiComponent\Model;
class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider
{
}    

Reload your page with the above in place, and you should see your page rendered again, sans errors.

What Did We Render?

If we take a look at our raw page source (i.e. not from a DOM Inspector, but the pre-javascript “View Source” source), we’ll see we’ve rendered the following

<div>
    <div data-bind="scope: 'pulsestorm_simple_valid.areas'" class="entry-edit form-inline">
        <!-- ko template: getTemplate() --><!-- /ko -->
    </div>
    <script type="text/x-magento-init">
    {
        "*": {
            "Magento_Ui/js/core/app": {
                "types": {
                    "dataSource": [],
                    "container": {
                        "extends": "pulsestorm_simple_valid"
                    },
                    "html_content": {
                        "component": "Magento_Ui\/js\/form\/components\/html",
                        "extends": "pulsestorm_simple_valid"
                    }
                },
                "components": {
                    "pulsestorm_simple_valid": {
                        "children": {
                            "pulsestorm_simple_valid": {
                                "type": "pulsestorm_simple_valid",
                                "name": "pulsestorm_simple_valid",
                                "config": {
                                    "component": "uiComponent"
                                }
                            },
                            "pulsestorm_simple_valid_data_source": {
                                "type": "dataSource",
                                "name": "pulsestorm_simple_valid_data_source",
                                "dataScope": "pulsestorm_simple_valid",
                                "config": {
                                    "params": {
                                        "namespace": "pulsestorm_simple_valid"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }    
    </script>
</div>

If we take a look at the <container/> node’s definition.xml configuration.

<!-- File: vendor/magento//module-ui/view/base/ui_component/etc/definition.xml -->

<!-- ... -->
<container class="Magento\Ui\Component\Container">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="component" xsi:type="string">uiComponent</item>
        </item>
        <item name="template" xsi:type="string">templates/container/default</item>
    </argument>
</container>
<!-- ... -->

We see its XHTML template is templates/container/default, and its component is the RequireJS map alias uiComponent (Magento_Ui/js/lib/core/collection).

The reason we chose the <container/> component is two fold. First, it’s one of the few generic components we can use at the root of our ui_compnent file without tripping a Magento XSD validation error. Second though, the uiComponent javascript component is exactly what we want. You’ll remember from last time a uiComponent‘s Knockout.js template (ui/collection) will run through all its child elements and render their templates – similar to a layout update XML <container/> node, or a Magento 1 core/text_list block.

The templates/container/default XHTML template, however, does not suit our needs.

#File: vendor/magento/module-ui/view/base/ui_component/templates/container/default.xhtml
<div xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../Ui/etc/ui_template.xsd">
    <div data-bind="scope: '{{getName()}}.areas'" class="entry-edit form-inline">
        <!-- ko template: getTemplate() --><!-- /ko -->
    </div>
</div>

It’s not 100% clear what this template is for. Magento’s core ui_component files use <container/> as a sub-node, which means the XHTML template is never rendered. It’s likely this is some legacy cruft left over from earlier days when container was used by Magento as a root level node. Or maybe it’s something forward looking. Whatever the reason, this is probably why we can use <container/> as a root level node in the first place. It’s hard to say if this “feature” will stick around, but for now it’s the best we have.

Changing the Template

So if this XHTML template is no good for us, are we stuck? Of course not — we can just configure a new template in our pulsestorm_simple_valid.xml file

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="template" xsi:type="string">templates/pulsestorm_simple_valid/default</item>
    </argument>    
    <!-- ... -->

</container>

Remember, the definition.xml file is where the default sub-nodes for a particular node are set, but each individual file in the ui_component folder can override these values.

We’ll also want to create the xhtml file for our templates/pulsestorm_simple_valid/default template.

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/templates/pulsestorm_simple_valid/default.xhtml
<div    
    data-bind="scope: '{{getName()}}.{{getName()}}'"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="../../../../../../Ui/etc/ui_template.xsd">

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

The code above is based on the default XHTML for a listing grid (vendor/magento/module-ui/view/base/ui_component/templates/listing/default.xhtml). While it performs the same Knockout.js scope kickoff as the template from our previous articles, there’s a few more things going on here that are worth mentioning.

First, let’s start with what we know. We have our standard Knockout tag-less template binding.

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

We also know that Knockout’s view model for this section is set by the outer scope binding. However, if we look at that scope binding.

<div data-bind="scope: '{{getName()}}.{{getName()}}'" ...>

We see the first unfamiliar bit. Instead of a hard coded scope, we have {{getName()}}.{{getName()}}. The text inside the {{...}} brackets are part of the XHTML template language, and will call through to the underlying UI Component object’s getName() method. This name will be the name we used in the layout handle XML file — <uiComponent name="pulsestorm_simple_valid"/>. Meaning the above will render as

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

We talked more about these template tags a few articles ago.

The next confusing bit is this

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../../Ui/etc/ui_template.xsd"

We have an XML namespace declaration in the root level <div/>, as well as a schema validation file (xsi:noNamespaceSchemaLocation). Remember — these are XHTML, not HTML templates. They behave like XML files. This also means you’re only allowed one top level node in an XHTML template.

While these attributes aren’t strictly necessary, they’re used in the Magento core XHTML templates so it’s best to use them here for consistency.

If you’re curious why these attributes aren’t rendered in the final HTML, Magento removes them before rendering here

#File: vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php
    public function __toString()
    {
        //...
        foreach ($templateRootElement->attributes as $name => $attribute) {
            if ('noNamespaceSchemaLocation' === $name) {
                $this->getDocumentElement()->removeAttributeNode($attribute);
                break;
            }
        }
        $templateRootElement->removeAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi');
        //...
    }

Finally, it’s worth taking a peek at the ui_template.xsd file

#File: vendor/magento/module-ui/etc/ui_template.xsd
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
    <xs:element name="form">
        <xs:complexType >
            <xs:sequence>
                <xs:any minOccurs="0" maxOccurs="unbounded" processContents="lax" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="div" >
        <xs:complexType >
            <xs:sequence>
                <xs:any minOccurs="0" maxOccurs="unbounded" processContents="lax" />
            </xs:sequence>
            <xs:anyAttribute processContents="lax" />
        </xs:complexType>
    </xs:element>
</xs:schema>

Covering the entire xs:schema language is a task beyond the scope of this article, but the above says that our xhtml files must have a root node of div, or form. Also, leaving the xsi:noNamespaceSchemaLocation out of our file won’t skip validation, as these .xhtml files are merged into an XML tree that includes this schema.

Adding to the Collection

Alright, if we clear our cache and reload the page with the above in place, we won’t see anything changed on the page. However, using the Sources tab of Chrome’s debugger, we can see Magento has included collection.js (from Magento_Ui/js/lib/core/collection, which is the RequireJS library uiCollection points at).

We can also (via XHR debugging) see that Magento has downloaded the collection.html Knockout.js template.

We can also pop into the javascript console, and we’ll see a registered view model constructor named pulsestorm_simple_valid.pulsestorm_simple_valid.

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

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

If you’re unsure of the significance here, you probably want to review our last article. Our next step is adding some child components to render.

We’re going to use an htmlContent component/node. Add the following to your pulsestorm_simple_valid.xml file

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<container>
    <!-- ... -->
    <htmlContent name="our_first_content">
        <argument name="block" xsi:type="object">Pulsestorm\SimpleValidUiComponent\Block\Example</argument>
    </htmlContent>           
    <!-- ... -->
</container>

The htmlContent node allows you to render the contents of a Magento block object into our x-magento-init script, and then have those contents rendered onto the page via Knockout.js. The above example will render the block named Pulsestorm\SimpleValidUiComponent\Block\Example. If we look at the definition.xml file for <htmlContent/>

<htmlContent class="Magento\Ui\Component\HtmlContent">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="component" xsi:type="string">Magento_Ui/js/form/components/html</item>
        </item>
    </argument>
</htmlContent>

We see this rendering happens via the RequireJS Magento_Ui/js/form/components/html module. Or, said more completely, the Magento_Ui/js/form/components/html module returns a Knockout.js view model constructor with a Knockout.js “Magento remote” template of ui/content/content.

//File: vendor/magento/module-ui/view/base/web/js/form/components/html.js
//...
return Component.extend({
    defaults: {
        content:        '',
        showSpinner:    false,
        loading:        false,
        visible:        true,
        template:       'ui/content/content',
        additionalClasses: {}
    },  
//...

We’ll leave the specifics of this rendering as an advanced exercise for the user.

If we clear our cache and reload with the above in place, we’ll get the following error.

Exception #0 (ReflectionException): Class Pulsestorm\SimpleValidUiComponent\Block\Example does not exist

Whoops! We forgot to create our Pulsestorm\SimpleValidUiComponent\Block\Example class. Lets do that now.

#File: app/code/Pulsestorm/SimpleValidUiComponent/Block/Example.php     
<?php
namespace Pulsestorm\SimpleValidUiComponent\Block;

use Magento\Framework\View\Element\BlockInterface;

class Example extends \Magento\Framework\View\Element\AbstractBlock
{
    public function toHtml()
    {
        return '<h1>Hello PHP Block Rendered in JS</h1>';
    }
}    

These are standard block classes rendered via the current area’s layout, and need to extend the Magento\Framework\View\Element\AbstractBlock class. Normally these are phtml template blocks, but we’re using a block with a hard coded toHtml method for simplicity’s sake.

If we reload with the above in place, we should see our block rendered.

Hijacking htmlContent

While the htmlContent nodes are interesting, if only for their amusing “render some server side code that renders some front-end code that renders some more server side code” pattern, we’re not interested in them today for their core functionality. We chose htmlContent nodes because

  1. They’re “XSD allowed” as children of <container/> elements
  2. Their base functionality is relatively simple/uncomplicated
  3. They’re generic, and not likely to imply a specific piece of functionality (vs., say, <listingToolbar/>)

This makes them ideal blocks to hijack. By hijack, we mean we’re going to take advantage of the UI Component’s XML merging to make our htmlContent blocks

  1. Use a different component class
  2. Use a different RequireJS view model constructor factory
  3. Have that RequireJS view model constructor factory point to a new Knockout.js template

Regarding Use a different component class, all we need to do is add a new class attribute to our htmlContent XML node

#File: app/code/Pulsestorm/SimpleValidUiComponent/view/adminhtml/ui_component/pulsestorm_simple_valid.xml
<htmlContent class="Pulsestorm\SimpleValidUiComponent\Component\Simple" name="our_first_content">
</htmlContent>   

We’ve also removed the block argument, as this was required by the Magento\Ui\Component\HtmlContent class our Pulsestorm\SimpleValidUiComponent\Component\Simple replaces. We’ll also (of course) want to create our Pulsestorm\SimpleValidUiComponent\Component\Simple class (confused by this? Checkout our previous article for the server side functionality of UI Components).

#File: app/code/Pulsestorm/SimpleValidUiComponent/Component/Simple.php
<?php
namespace Pulsestorm\SimpleValidUiComponent\Component;
class Simple extends \Magento\Ui\Component\AbstractComponent
{
    const NAME = 'html_content_pulsestorm_simple';    
    public function getComponentName()    
    {
        return self::getName();
    }
}

Regarding Use a different RequireJS view model constructor factory, all we need to do is add a new @name="data"/@name="config"/@name="component" argument

<htmlContent class="Pulsestorm\SimpleValidUiComponent\Component\Simple"  name="our_first_content">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="component" xsi:type="string">Pulsestorm_SimpleValidUiComponent/js/pulsestorm_simple_component</item>
        </item>
    </argument>
</htmlContent>

and then, (conveniently covering our third Have that RequireJS view model constructor factory point to a new Knockout.js template point), have that RequireJS module return a view model constructor with a new Knockout.js remote template.

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

    return viewModelConstructor;
});

and then create the Pulsestorm_SimpleValidUiComponent/pulsestorm_simple_template template.

<!-- File: app/code/Pulsestorm/SimpleValidUiComponent//view/adminhtml/web/template/pulsestorm_simple_template.html -->
<h1>Our Remote Knockout Template!</h1>

With the above in place, clear your cache and reload the page

Congratulations! You’ve just successfully created a UI Component, fully under our programmatic control, without violating Magento’s XSD schema validations.

Wrap Up

Whether or not this is a good idea or not remains to be seen. While we’ve taken every step possible to keep our htmlContent node under our control (custom PHP component class, custom RequireJS component, custom Knockout.js template), it’s still theoretically possible that a future change by the core Magento engineering team might break what we’ve done here. Right now, a fundamental problem with UI Components is all the programmatic and political evidence points to them being for the core team only, and only time will tell if third party developers are meant to, or will be able to, incorporate them stably into their extensions.

Originally published September 21, 2016
Series Navigation<< Magento 2: Simplest UI Knockout ComponentMagento 2: ES6 Template Literals >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 21st September 2016