If you’ve been reading through my Advanced Javascript, UI Components, and uiElement Internals series, you’re aware that Magento have extended the Knockout.js template system such that
- Templates can be Loaded via ajax/HTTP
- Knockout’s “tag-less” bindings have tag equivalents
The remote HTTP templates are a life saver as far as code organization goes.
The tag equivalents, however, have some fundamental design problems that make working with them difficult. Exacerbating the situation is a complete lack of documentation on these tags, leaving us uncertain if they’re an optional thing, or if they must be used due to some weird handling of Knockout.js’s standard comment based tags with Magento’s remote ajax-loaded templates.
Today we’re going to run through some of the problem with this XHTML based template system. While this post isn’t a full tutorial, we will end up explaining how these tags work. We’ll also end with a quick cheat sheet that should help you find the particular node or attribute you’re looking for. Finally, we’d be remiss if we didn’t mention Commerce Bug 3.2, which introduced a feature that automatically translates these templates for you.
XSLT Redux
The usage problems are subtle, but maddening. First, lets consider the new tags/nodes introduced. If you see a tag like this in a Magento remote Knockout.js template
<if args="foo">
//...
</if>
Magento will translate this into the following Knockout.js code
<!-- ko if: foo --><!-- /ko -->
That is, the tag itself will be turned into Knockout’s tag-less template bindings
<!-- ko if: [...] --><!-- /ko -->
and the args
attribute will be dropped in as the binding’s arguments
<!-- ko if: foo --><!-- /ko -->
This will work with complex arguments as well. This
<each args="data: elems, as: 'element'">
</each>
translates into
<!-- ko foreach: {data: elems, as: 'element'} --><!-- /ko -->
The <each/>
becomes <!-- ko foreach...
, and the args="data: elems, as: 'element'"
becomes {data: elems, as: 'element'}
.
Already, two problems present themselves. The first – there’s not a 1-to-1 relationship between the tag name, and the binding name. The <if/>
tag corresponds to an <!-- ko if
. However, there’s no <!-- ko each
binding in Knockout.js. Something transforms <each/>
into foreach
. Because there’s no documentation on what tags and bindings are supported, even a developer already familiar with Knockout.js may struggle to translate Magento’s tag-equivalent syntax.
The second problem is more fundamental. While the above example are somewhat obvious, consider this system in context.
<h1>Hello World</h1>
<p>We have some text that we love <b><if args="isQuestion">?</if></b><b><if args="isStatment">?</if></b></p>
Which of the above tags are HTML tags, and which are Knockout.js tags? Even putting aside the issue of undocumented tags, buy making the programming language syntax identical to the markup syntax (i.e. tags), the cognitive load on a developer working with this system is greatly increased. Scanning templates for logic vs. style becomes much more difficult.
Developers familiar with XSLT will recognize this pattern
<xsl:for-each select="catalog/cd">
<xsl:if test="price>10">
<tr>
<td><xsl:value-of select="title"/></td>
<td><xsl:value-of select="artist"/></td>
<td><xsl:value-of select="price"/></td>
</tr>
</xsl:if>
</xsl:for-each>
While I remember XSLT fondly, this context confusion was a huge hurdle for developers and prevented widespread adoption of the language. Magento’s Knockout.js templates face a similar hurdle.
Attribute Swapping
The next bit of confusion w/r/t Magento’s Knockout templates is attribute swapping. If you write a bit of template code like this
<div css="foo" html="bar"></div>
Magento will swap out the css
and html
attribute for a Knockout.js data binding.
<div data-bind="css: foo, html: bar"></div>
i.e. css="foo" html="bar"
transforms into data-bind="css: foo, html: bar"
. This is an interesting feature, but again we have context overloading. When a developer is looking at a template, they need to stop and think
Is this a standard HTML attribute? Or is this something Magento’s template engine will transform into a
data-bind
attribute?
For all its blunt-ness, when you see data-bind
as an attribute, you know the code will invoke Knockout.js. When you see a non-standard HTML attribute like css
, you’re left wondering if that’s one Knockout.js will swap out, or if its related to some other, as yet undiscovered, javascript system.
This attribute system also has the 1-to-1 problem. This bit of template code
<div ko-value="foo"></div>
translates into
<div data-bind="value: foo"></div>
That is – ko-value=
becomes data-bind="value:
. This was likely done to avoid overloading the HTML standard value="..."
. Again, lack of documentation is crippling here.
Conditional Attributes
Magento piles on with the context switching in attributes. Some of the supported attributes don’t work as we described above. For example, based on the rules above, the following
<div ifnot="foo"></div>
Should translate into
<div data-bind="ifnot: foo"></div>
It does not. Instead, it translates into
<!-- ko ifnot: foo --><div></div><!-- /ko -->
There’s a special sub-set of conditional attributes that Magento will treat differently. Instead of transforming these attributes into data-bind
-ings, Magento will transform these attributes into “tag-less” wrappers. So that’s yet another context to be aware of when looking at these remote templates.
Cheat Sheet
While not 100% comprehensive, here’s what I’ve been able to piece together w/r/t what custom XHTML nodes invoke a tag-less template binding, which attributes transform into a data-bind
, and which attributes transform into a tag-less wrapper.
Nodes and Tag-less Bindings
Magento will be transform the following nodes into the following Knockout tag-less bindings
<if args="..."></if> : <!-- ko if: ... --><!-- /ko -->
<text args="..."></text> : <!-- ko text: ... --><!-- /ko -->
<with args="..."></with> : <!-- ko with: ... --><!-- /ko -->
<scope args="..."></scope> : <!-- ko scope: ... --><!-- /ko -->
<ifnot args="..."></ifnot> : <!-- ko ifnot: ... --><!-- /ko -->
<each args="..."></each> : <!-- ko foreach: ... --><!-- /ko -->
<component args="..."></component> : <!-- ko component: ... --><!-- /ko -->
Also, other than the args
attribute, there will be no other argument replacement for these nodes.
Argument to Data-Bind
The following attributes will be transformed, 1-to-1, and combined into a single data-bind
attribute.
<strong css="foo"></strong> : <strong data-bind="css: foo"></strong>
<strong attr="foo"></strong> : <strong data-bind="attr: foo"></strong>
<strong html="foo"></strong> : <strong data-bind="html: foo"></strong>
<strong with="foo"></strong> : <strong data-bind="with: foo"></strong>
<strong text="foo"></strong> : <strong data-bind="text: foo"></strong>
<strong click="foo"></strong> : <strong data-bind="click: foo"></strong>
<strong event="foo"></strong> : <strong data-bind="event: foo"></strong>
<strong submit="foo"></strong> : <strong data-bind="submit: foo"></strong>
<strong enable="foo"></strong> : <strong data-bind="enable: foo"></strong>
<strong disable="foo"></strong> : <strong data-bind="disable: foo"></strong>
<strong options="foo"></strong> : <strong data-bind="options: foo"></strong>
<strong visible="foo"></strong> : <strong data-bind="visible: foo"></strong>
<strong template="foo"></strong> : <strong data-bind="template: foo"></strong>
<strong hasFocus="foo"></strong> : <strong data-bind="hasFocus: foo"></strong>
<strong textInput="foo"></strong> : <strong data-bind="textInput: foo"></strong>
<strong component="foo"></strong> : <strong data-bind="component: foo"></strong>
<strong uniqueName="foo"></strong> : <strong data-bind="uniqueName: foo"></strong>
<strong optionsText="foo"></strong> : <strong data-bind="optionsText: foo"></strong>
<strong optionsValue="foo"></strong> : <strong data-bind="optionsValue: foo"></strong>
<strong checkedValue="foo"></strong> : <strong data-bind="checkedValue: foo"></strong>
<strong selectedOptions="foo"></strong> : <strong data-bind="selectedOptions: foo"></strong>
There are also the attributes that will be transformed, but in a “not 1-to-1” basis. These are ko-
versions of standard HTML attributes, and an each=
attribute that creates a foreach
data binding.
<strong each="foo"></strong> : <strong data-bind="foreach: foo"></strong>
<strong ko-value="foo"></strong> : <strong data-bind="value: foo"></strong>
<strong ko-style="foo"></strong> : <strong data-bind="style: foo"></strong>
<strong ko-checked="foo"></strong> : <strong data-bind="checked: foo"></strong>
<strong ko-disabled="foo"></strong> : <strong data-bind="disable: foo"></strong>
<strong ko-focused="foo"></strong> : <strong data-bind="hasFocus: foo"></strong>
The following three attributes will end up wrapping the tags in a tag-less control structures
<strong if="foo"></strong> : <strong></strong>
<strong ifnot="foo"></strong> : <strong></strong>
<strong outereach="foo"></strong> : <strong></strong>
Finally, there’s innerif
and innerifnot
.
<strong innerif="foo"></strong> : <strong data-bind="if: foo"></strong>
<strong innerifnot="foo"></strong> : <strong data-bind="ifnot: foo"></strong>
These look like, and behave like, the other “not 1-to-1” aliases. However, they’re worth calling out as they exist to allow data-bind
if/ifnot statements, which is something the wrapping attributes above prevent.
Also, one last curiosity. Despite ko-focused
and ko-disabled
existing, it appears the disable
and hasFocus
attributes will make it through to a data-bind
. This is likely an implementation bug/side-effect, and not something I’d rely on working in future versions of Magento 2.
<strong disable="foo"></strong> : <strong data-bind="disable: foo"></strong>
<strong hasFocus="foo"></strong> : <strong data-bind="hasFocus: foo"></strong>