Categories


Archives


Recent Posts


Categories


Magento 2: UI Component Retrospective

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!

Today we’re going to wrap up our UI Component series with some odds, ends, and thoughts on how we got here. Let’s kick things off with some blatant self promotion.

Commerce Bug 3.2

Pulse Storm has just released Commerce Bug 3.2. This is relevant to us because 3.2 brings with it UI Component debugging features — check out the screencast for more information

It’s features like these that make Commerce Bug 3 the world’s best Magento developer toolbar. When Magento 2 was first released a number of free competitor toolbars popped up, but none of those have kept the pace with subsequent Magento 2 releases, and none of them provide debugging features for Magento 2’s newer systems. Even a powerhouse like Zend’s Z-ray can’t keep up, with no stand-alone support for PHP 7 and zero support for modern Magento front end features. We know you probably didn’t chose Magento as your platform, but once you have a copy of Commerce Bug installed you’ll be cutting through Magento’s red tape like a hot knife through butter.

If marketing hyperbole isn’t your thing, remember that revenue from Commerce Bug, along with Pulse Storm’s other products, is what allows me to spend time digging into Magento 2’s features as deeply as I do. If I’ve ever bailed you out of a jam please consider a purchase, or if you don’t think any of Pulse Storm’s products are for you please consider donating to my patreon.

Chrome Knockout Debugger

Flipping PT Barnum mode off, another tool that’s useful for debugging UI Components is Google Chrome’s Knockout.js context debugger.

This tool will let you debug a specific node’s Knockout.js context — i.e. you’ll be able to see the data the view has access to. While this tool doesn’t have access to Magento’s deep view models tree, it’s often useful to quickly peek at data without adding a <pre data-bind="text: ko.toJSON($data, null, 2)"></pre> to your nodes.

UI Component Critique

Even with these tools, the UI Component system can be difficult to get your head around, or even identify as a single system. You have

  1. A new PHP XHTML template layer
  2. A domain specific language (DSL) for creating JSON in a particular format
  3. A RequireJS application for instantiating and registering objects from that particular format
  4. An extension to Knockout.js to use those models as multiple view models on a single page
  5. An extension to Knockout.js to allow remote template loading
  6. An extension to Knockout.js to replace the Knockout.js “tag-less” syntax with XHTML based tags and attributes

While Magento’s backend UI grids and forms make full use of this tenuously threaded together UI Component system, even the Magento core front end development team have abandoned/ignored large parts of this system. For example, the “mini-cart” application appears on most Magento frontend pages. Here’s part of its UI Component tree as viewed via Commerce Bug 3

image of UI Component debugger

While the mini-cart is a Magento_Ui/js/core/app application,

<a class="action showcart" href="http://magento-2-1-1.dev/checkout/cart/"
   data-bind="scope: 'minicart_content'">
    <span class="text">My Cart</span>
    <span class="counter qty empty"
          data-bind="css: { empty: !!getCartParam('summary_count') == false }, blockLoader: isLoading">
        <span class="counter-number"><!-- ko text: getCartParam('summary_count') --><!-- /ko --></span>
        <span class="counter-label">
        <!-- ko if: getCartParam('summary_count') -->
            <!-- ko text: getCartParam('summary_count') --><!-- /ko -->
            <!-- ko i18n: 'items' --><!-- /ko -->
        <!-- /ko -->
        </span>
    </span>
</a>
<!-- ... -->
<script type="text/x-magento-init">
{
    "[data-block='minicart']": {
        "Magento_Ui/js/core/app": {
            "components": {
                "minicart_content": {
                    "children": {
                        "subtotal.container": {
                            "children": {
                                "subtotal": {
                                    "children": {
                                        "subtotal.totals": {
                                            "config": {
                                                "display_cart_subtotal_incl_tax": 0,
                                                "display_cart_subtotal_excl_tax": 1,
                                                "template": "Magento_Tax\/checkout\/minicart\/subtotal\/totals"
                                            },
                                            "children": {
                                                "subtotal.totals.msrp": {
                                                    "component": "Magento_Msrp\/js\/view\/checkout\/minicart\/subtotal\/totals",
                                                    "config": {
                                                        "displayArea": "minicart-subtotal-hidden",
                                                        "template": "Magento_Msrp\/checkout\/minicart\/subtotal\/totals"
                                                    }
                                                }
                                            },
                                            "component": "Magento_Tax\/js\/view\/checkout\/minicart\/subtotal\/totals"
                                        }
                                    },
                                    "component": "uiComponent",
                                    "config": {
                                        "template": "Magento_Checkout\/minicart\/subtotal"
                                    }
                                }
                            },
                            "component": "uiComponent",
                            "config": {
                                "displayArea": "subtotalContainer"
                            }
                        },
                        "item.renderer": {
                            "component": "uiComponent",
                            "config": {
                                "displayArea": "defaultRenderer",
                                "template": "Magento_Checkout\/minicart\/item\/default"
                            },
                            "children": {
                                "item.image": {
                                    "component": "Magento_Catalog\/js\/view\/image",
                                    "config": {
                                        "template": "Magento_Catalog\/product\/image",
                                        "displayArea": "itemImage"
                                    }
                                },
                                "checkout.cart.item.price.sidebar": {
                                    "component": "uiComponent",
                                    "config": {
                                        "template": "Magento_Checkout\/minicart\/item\/price",
                                        "displayArea": "priceSidebar"
                                    }
                                }
                            }
                        },
                        "extra_info": {
                            "component": "uiComponent",
                            "config": {
                                "displayArea": "extraInfo"
                            }
                        },
                        "promotion": {
                            "component": "uiComponent",
                            "config": {
                                "displayArea": "promotion"
                            }
                        }
                    },
                    "config": {
                        "itemRenderer": {
                            "default": "defaultRenderer",
                            "simple": "defaultRenderer",
                            "virtual": "defaultRenderer"
                        },
                        "template": "Magento_Checkout\/minicart\/content"
                    },
                    "component": "Magento_Checkout\/js\/view\/minicart"
                }
            },
            "types": []
        }
    },
    "*": {
        "Magento_Ui/js/block-loader": "http://magento-2-1-1.dev/pub/static/frontend/Magento/luma/en_US/images/loader-1.gif"
    }
}
</script>

it does not use the XML based UI Component DSL to generate its JSON, nor does it use the XHTML based PHP template system to output its initial nodes. It also targets a specific CSS selector in its x-magento-init script

"[data-block='minicart']":  {
    "Magento_Ui/js/core/app": {/*...*/}
}

and invokes a completely separate program via the more-familiar-to-us * syntax

"*": {
        "Magento_Ui/js/block-loader": "http://magento-2-1-1.dev/pub/static/frontend/Magento/luma/en_US/images/loader-1.gif"
    }

On the backend the mini-cart code is rendered by a good old fashioned Magento block, created via a layout handle XML file.

#File: vendor/magento/module-checkout/view/frontend/layout/default.xml
<referenceContainer name="header-wrapper">
    <block class="Magento\Checkout\Block\Cart\Sidebar" name="minicart" as="minicart" after="logo" template="cart/minicart.phtml">
        <arguments>
            <argument name="jsLayout" xsi:type="array">
                <item name="types" xsi:type="array"/>
                <item name="components" xsi:type="array">
                    <item name="minicart_content" xsi:type="array">
                        <item name="component" xsi:type="string">Magento_Checkout/js/view/minicart</item>
                        <item name="config" xsi:type="array">
                            <item name="template" xsi:type="string">Magento_Checkout/minicart/content</item>
                        </item>
                        <item name="children" xsi:type="array">
                            <item name="subtotal.container" xsi:type="array">
                                <item name="component" xsi:type="string">uiComponent</item>
                                <item name="config" xsi:type="array">
                                    <item name="displayArea" xsi:type="string">subtotalContainer</item>
                                </item>
                                <item name="children" xsi:type="array">
                                    <item name="subtotal" xsi:type="array">
                                        <item name="component" xsi:type="string">uiComponent</item>
                                        <item name="config" xsi:type="array">
                                            <item name="template" xsi:type="string">Magento_Checkout/minicart/subtotal</item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                            <item name="extra_info" xsi:type="array">
                                <item name="component" xsi:type="string">uiComponent</item>
                                <item name="config" xsi:type="array">
                                    <item name="displayArea" xsi:type="string">extraInfo</item>
                                </item>
                            </item>
                            <item name="promotion" xsi:type="array">
                                <item name="component" xsi:type="string">uiComponent</item>
                                <item name="config" xsi:type="array">
                                    <item name="displayArea" xsi:type="string">promotion</item>
                                </item>
                            </item>
                        </item>
                    </item>
                </item>
            </argument>
        </arguments>
        <container name="minicart.addons" label="Mini-cart promotion block"/>
    </block>
</referenceContainer>

The entire x-magento-init JSON block is configured via <argument> nodes, and then rendered in the phtml template via the block’s getJsLayout method.

<script type="text/x-magento-init">
{
    "[data-block='minicart']": {
        "Magento_Ui/js/core/app": <?php /* @escapeNotVerified */ echo $block->getJsLayout();?>
    },
    "*": {
        "Magento_Ui/js/block-loader": "<?php /* @escapeNotVerified */ echo $block->getViewFileUrl('images/loader-1.gif'); ?>"
    }
}
</script>

Other frontend UI Components (checkout, messages, etc.) are similarly divergent. So — given all this: Are these true UI Components? They certainly use some of the UI Component system’s RequireJS/Knockout.js code, but with a completely separate method for rendering that code on the server side.

Did these front end XML-less UI Components come first and then Magento’s core developers created a DSL to help manage the complexity of those nodes?

Or did the DSL come first, and the above is an example of front end developers taking only what they needed to get their jobs done?

We say all this not in condemnation of Magento’s new front end systems — this sort of thing happens all the time in software and is part of the process of building out brand new systems. It does leave a working Magento developer with some puzzling choices on how to proceed with their own code, and some real head scratching as to why a system that’s clearly in a rough beta phase is being marketed as production ready.

Javascript Critique

Part of the issue here may be a simultaneously ambitious but impossibly vague goal around Magento 2 embracing our brave new javascript world. In this, Magento isn’t alone. Thanks to the ubiquity of javascript in all major web browsers, the tremendous efforts put forth by companies like Google, Apple, and Microsoft, etc. in optimizing the performance of the Javascript runtime, and the community of open source developers that have coalesced behind all this, it’s hard to be involved in a software project that doesn’t use javascript in some way, shape, or form. It makes sense that, in 2016, one of Magento 2’s goals should be embracing this new world.

The problem, as is all too often the case, is you can’t just glop technology on top of a project an expect it to work like magic fairy dust. Javascript is popular for a number of different, overlapping, but often incompatible reasons.

For example, javascript is popular with agency-style businesses because a javascript based user interface is often powered by a “server-less” HTTP based API. The nature and structure of agencies mean they’re often pretty bad at maintaining server side, business logic oriented software, and much better at building out user/customer driven interfaces built on top of APIs that have already solved a particular business problem.

Implicit in the above scenario, javascript/API based user interfaces are often popular with traditional software companies because they allow engineers to focus on the problem of scaling out their high demand systems and adjusting to changes in business logic without getting bogged down in the world of UI/UX. All an engineering team needs to do is expose and support a fully featured HTTP based API and let folks build whatever sort of UI/UX they’d like. Both jobs are important, but separating them out like this can reduce a lot of friction between teams with different skill sets.

Finally, javascript is also popular with smaller startups because of the efficiencies gained by having your server side code and your front end code written in the same language.

Magento 2, unfortunately, doesn’t address any of the above use cases in a compelling or complete way. The UI Component/x-magento-init system we’ve explored allows you to embed javascript application inside a HTML page, but much of Magento 2 still relies on PHP’s tried and true server rendered HTML, using javascript to enhance page functionality.

Modernizing a classic PHP based software system isn’t an easy task, but it is possible and it does have a precedent. Consider WordPress/Automattic, in some ways the grandfather of all these PHP based software systems. Automattic have never used their influence with the WordPress community to force the PHP based WordPress into adopting modern javascript patterns wholesale. While doing so may have turned WordPress into something interesting (like the Ghost blogging platform), leaving behind so much working code and so many community members wasn’t worth it.

Instead, Automattic created Calypso, a ground up re-imagining of WordPress.com as a javascript based application. The only way this directly impacted the existing WordPress user base was the WordPress PHP application (and many WordPress plugins) got a new, modern, RESTFul API system.

Magento’s approach, on the other hand, feels a bit like a plate of mush — there’s good ideas in there, but when combined the systems feel rickety, confusing, or both. Word out of Magento Inc. is the 2.2 release will bring myriad improvements to “developer experience” (i.e. stuff might work), but in the meantime working Magento developers will need to make do with the incomplete systems available to them.

Practical Solutions

So, what’s a Magento developer to do? My advice is to tread lightly. If you’re building a Magento extension, copying the UI Component DSL for grids and simple forms seems like a safe bet, but otherwise I’d stay away from UI Component XML. Beyond the burden of boilerplate and fighting Magento’s XSDs, the XML based DSL is likely to change in random and unpredictable ways. It seems better to stick to tried and true server rendered HTML enhanced with a single level x-magento-init/Magento_Ui/js/core/app program.

The same applies to system integrators, but if you’re the one building and maintaining a specific Magento instance I’d say it’s safe to also use RequireJS’s selective “monkey patching” abilities to modify the behavior of core javascript objects. Keep an eye on Magento Quickies for an upcoming article on how to do this with Magento’s special requirejs-config.js files. While an extension developer can do this, too many extension developers monkey patching the same module means someone’s functionality ends up losing. If you’re in a position to take responsibility for your changes, this RequireJS monkey patching is often more stable that the wholesale file replacement Magento system integrators fall back on.

In general, while you’ll need to understand Magento’s new javascript systems to work with and analyze the platform, be careful when embracing them for your own functionality. Hopefully, as Magento 1’s end of life looms closer and closer, Magento’s engineering teams will be able to get these new and incomplete systems under control. Until then, the old ways often remain the best ways. Remember that, at the end of the day, our job isn’t to solve technology problems, it’s to solve problems using technology. The less time we spend on the former, the more we’ll have for the later.

Series Navigation<< Magento 2: UI Component Data SourcesObservables, uiElement Objects, and Variable Tracking >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 24th October 2016