Question

As near as I can tell, when you view a Grid in Magento's backend, the following "loaded over XHR" KnockoutJS template is what starts rendering things

File: vendor/magento//module-ui/view/base/web/templates/collection.html
URL:  http://magento.example.xom/pub/static/adminhtml/Magento/backend/en_US/Magento_Ui/templates/collection.html
<each args="data: elems, as: 'element'">
    <render if="hasTemplate()"/>
</each>

However -- I'm at a bit of a loss as to what the <each/> tag and the <render/> tag are. They aren't (or don't appear to be?) a part of stock KnockoutJS.

I know its possible to add custom tags to KnockoutJS via components, but I don't see any obvious places where a component named each or render is added to KnockoutJS.

So, I'm not sure if these are components registered somewhere I'm not aware of, or some other customization that Magento has made to KnockoutJS that enables custom tags, or something else entirely.

Note: I'm not completely in the dark here -- I get that <each/> is probably iterating over every child ui component rendered in the JSON, and rendering its template (if that template exists).

What I'm not clear on at all is how these tags are implemented. I want to see where they're implemented so I can debug how data is bound, and also understand the mechanism that Magento's using to create these tags in case there are others.

Was it helpful?

Solution

As Raphael hinted at, it turns out that when Magento downloads its KnockoutJS templates via an XHR (i.e. ajax) request, it also passes them through some custom parsing routines that look for a number of custom tags and attributes

This custom parsing is done by the Magento_Ui/js/lib/knockout/template/renderer RequireJS module. This module's source code sets up a number of default tags and attributes to search for. There are also other modules which may add additional tags and attributes to this renderer. For example, the following

#File: vendor/magento/module-ui/view/base/web/js/lib/knockout/bindings/scope.js
renderer
    .addNode('scope')
    .addAttribute('scope', {
        name: 'ko-scope'
    });

will will add the <scope/> tag and scope attribute (<div scope="...">) to the list of parseable attributes.

Is seems like the basic idea is to translate these tags and attributes into native Knockout "tagless" template blocks. For example, the following Magento KnockoutJS template

<each args="data: elems, as: 'element'">
    <render if="hasTemplate()"/>
</each>

Translates into the following native KnockoutJS code

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

The exact rules of this translation are still unclear to me -- the code in Magento_Ui/js/lib/knockout/template/renderer is a little indirect, and it seems like they can change from tag to tag, attribute to attribute.

I've ginned up the following code snippet that can download a Magento KnockoutJS template, and translate it into native KnockoutJS code.

jQuery.get('http://magento-2-1-0.dev/static/adminhtml/Magento/backend/en_US/Magento_Ui/templates/collection.html', function(result){
    var renderer = requirejs('Magento_Ui/js/lib/knockout/template/renderer')
    var fragment = document.createDocumentFragment();
    $(fragment).append(result);

    //fragment is passed by reference, modified
    renderer.normalize(fragment);
    var string = new XMLSerializer().serializeToString(fragment);
    console.log(string);    
})

As to why Magento might do this -- my guess is wanting some sort of syntax highlighting and readability for KnockoutJS's commenting template, but never rule out more Mallory-ish reasons.

OTHER TIPS

Both tags are implemented under app/code/Magento/Ui/view/base/web/js/lib/knockout/template/renderer.js, I'm not too sure to understand exactly how they are implemented though:

_.extend(preset.nodes, {
    foreach: {
        name: 'each'
    },

    /**
     * Custom 'render' node handler function.
     * Replaces node with knockout's 'ko template:' comment tag.
     *
     * @param {HTMLElement} node - Element to be processed.
     * @param {String} data - Data specified in 'args' attribute of a node.
     */
    render: function (node, data) {
        data = data || 'getTemplate()';
        data = renderer.wrapArgs(data);

        renderer.wrapNode(node, 'template', data);
        $(node).replaceWith(node.childNodes);
    }
});
Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top