29

Many Magento backend pages contain the following in their source code

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

I understand (or think I do?) that <!-- ko template is a KnockoutJS container-less template binding.

What's not clear to me is -- what context is the getTemplate() function called in? In the examples I've see online, there's usually a javascript object after the template:. I'm assuming that getTemplate is a javascript function that returns an object -- but there's no global javascript function named getTemplate.

Where is getTemplate bound? Or, possibly a better question, where does the KnockoutJS application binding happen on a Magento backend page?

I'm interested in this from a pure HTML/CSS/Javascript point of view. I know Magento 2 has many configuration abstractions so (in theory) developers don't need to worry about the implementation details. I'm interested in the implementation details.

3 Answers 3

48

The PHP code for a UI component renders a javascript initialization that looks like this

<script type="text/x-magento-init">
    {
        "*": {
            "Magento_Ui/js/core/app":{
                "types":{...},
                "components":{...},
            }
        }
    }
</script>       

This bit of code in the page means Magento will invoke the Magento_Ui/js/core/app RequireJS module to fetch a callback, and then call that that callback passing in the the {types:..., components:...} JSON object as an argument (data below)

#File: vendor/magento/module-ui/view/base/web/js/core/app.js
define([
    './renderer/types',
    './renderer/layout',
    'Magento_Ui/js/lib/ko/initialize'
], function (types, layout) {
    'use strict';

    return function (data) {
        types.set(data.types);
        layout(data.components);
    };
});

The data object contains all the data needed to render the UI component, as well as a configuration that links certain strings with certain Magento RequireJS modules. That mapping happens in the types and layout RequireJS modules. The application also loads the Magento_Ui/js/lib/ko/initialize RequireJS library. The initialize module kicks off Magento's KnockoutJS integration.

/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
/** Loads all available knockout bindings, sets custom template engine, initializes knockout on page */

#File: vendor/magento/module-ui/view/base/web/js/lib/ko/initialize.js
define([
    'ko',
    './template/engine',
    'knockoutjs/knockout-repeat',
    'knockoutjs/knockout-fast-foreach',
    'knockoutjs/knockout-es5',
    './bind/scope',
    './bind/staticChecked',
    './bind/datepicker',
    './bind/outer_click',
    './bind/keyboard',
    './bind/optgroup',
    './bind/fadeVisible',
    './bind/mage-init',
    './bind/after-render',
    './bind/i18n',
    './bind/collapsible',
    './bind/autoselect',
    './extender/observable_array',
    './extender/bound-nodes'
], function (ko, templateEngine) {
    'use strict';

    ko.setTemplateEngine(templateEngine);
    ko.applyBindings();
});

Each individual bind/... RequireJS module sets up a single custom binding for Knockout.

The extender/... RequireJS modules add some helper methods to native KnockoutJS objects.

Magento also extends the functionality of Knockout's javascript template engine in the ./template/engine RequireJS module.

Finally, Magento calls applyBindings() on the KnockoutJS object. This is normally where a Knockout program would bind a view model to the HTML page -- however, Magento calls applyBindings without a view model. This means Knockout will start processing the page as a view, but with no data bound.

In a stock Knockout setup, this would be a little silly. However, because of the previously mentioned custom Knockout bindings, there's plenty of opportunities for Knockout to do things.

We're interested in the scope binding. You can see that in this HTML, also rendered by the PHP UI Component system.

<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
    <div data-role="spinner" data-component="customer_listing.customer_listing.customer_columns" class="admin__data-grid-loading-mask">
        <div class="spinner">
            <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
        </div>
    </div>
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">
    </script>
</div>

Specifically, the data-bind="scope: 'customer_listing.customer_listing'"> attribute. When Magento kicks off applyBindings, Knockout will see this custom scope binding, and invoke the ./bind/scope RequireJS module. The ability to apply a custom binding is pure KnockoutJS. The implementation of the scope binding is something Magento Inc. has done.

The implementation of the scope binding is at

#File: vendor/magento/module-ui/view/base/web/js/lib/ko/bind/scope.js

The important bit in this file is here

var component = valueAccessor(),
    apply = applyComponents.bind(this, el, bindingContext);

if (typeof component === 'string') {
    registry.get(component, apply);
} else if (typeof component === 'function') {
    component(apply);
}

Without getting too into the details, the registry.get method will pull out an already generated object using the string in the component variable as an identifier, and pass it to the applyComponents method as the third parameter. The string identifier is the value of scope: (customer_listing.customer_listing above)

In applyComponents

function applyComponents(el, bindingContext, component) {
    component = bindingContext.createChildContext(component);

    ko.utils.extend(component, {
        $t: i18n
    });

    ko.utils.arrayForEach(el.childNodes, ko.cleanNode);

    ko.applyBindingsToDescendants(component, el);
}

the call to createChildContext will create what is, essentially, a new viewModel object based on the already instantiated component object, and then apply it to all the descendant element of the original div that used data-bind=scope:.

So, what is the already instantiated component object? Remember the call to layout back in app.js?

#File: vendor/magento/module-ui/view/base/web/js/core/app.js

layout(data.components);

The layout function/module will descend into the passed in data.components (again, this data comes from the object passed in via text/x-magento-init). For each object it finds, it will look for a config object, and in that config object it will look for a component key. If it finds a component key, it will

  1. Use RequireJS to return a module instance -- as though the module were called in a requirejs/define dependency.

  2. Call that module instance as a javascript constructor

  3. Store the resulting object in the registry object/module

So, that's a lot to take in. Here's a quick review, using

<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
    <div data-role="spinner" data-component="customer_listing.customer_listing.customer_columns" class="admin__data-grid-loading-mask">
        <div class="spinner">
            <span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
        </div>
    </div>
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">
    </script>
</div>

as a starting point. The scope value is customer_listing.customer_listing.

If we look at the JSON object from the text/x-magento-init initialization

{
    "*": {
        "Magento_Ui/js/core/app": {
            /* snip */
            "components": {
                "customer_listing": {
                    "children": {
                        "customer_listing": {
                            "type": "customer_listing",
                            "name": "customer_listing",
                            "children": /* snip */
                            "config": {
                                "component": "uiComponent"
                            }
                        },
                        /* snip */
                    }
                }
            }
        }
    }
}

We see the components.customer_listing.customer_listing object has a config object, and that config object has a component object that's set to uiComponent. The uiComponent string is a RequireJS module. In fact, its a RequireJS alias that corresponds to the Magento_Ui/js/lib/core/collection module.

vendor/magento/module-ui/view/base/requirejs-config.js
14:            uiComponent:    'Magento_Ui/js/lib/core/collection',

In layout.js, Magento has run code that's equivalent to the following.

//The actual code is a bit more complicated because it
//involves jQuery's promises. This is already a complicated 
//enough explanation without heading down that path

require(['Magento_Ui/js/lib/core/collection'], function (collection) {    
    object = new collection({/*data from x-magento-init*/})
}

For the really curious, if you take a look in the collection model and follow its execution path, you'll discover that collection is a javascript object that's been enhanced both by the lib/core/element/element module and the lib/core/class module. Researching these customizations is beyond the scope of this answer.

Once instantiated, layout.js stores this object in the registry. This means when Knockout starts processing the bindings and encounters the custom scope binding

<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
    <!-- snip -->
    <!-- ko template: getTemplate() --><!-- /ko -->
    <!-- snip -->
</div>

Magento will fetch this object back out of the registry, and bind it as the view model for things inside the div. In other words, the getTemplate method that's called when Knockout invokes the tagless binding (<!-- ko template: getTemplate() --><!-- /ko -->) is the getTemplate method on the new collection object.

7
  • 3
    I hate to just ask the question 'why' to your answer, so more focused question would be, what does M2 gain by using this (seemingly convoluted) system to call KO templates?
    – circlesix
    Commented Jun 12, 2016 at 22:46
  • 26
    I don't know whether to laugh or cry! What a mess.
    – koosa
    Commented Sep 23, 2016 at 9:38
  • 14
    I think they are digging their own grave. If they keep complicating things like this, companies will stop using it because of development costs
    – ononononon
    Commented Nov 26, 2016 at 23:17
  • 6
    I just spend around 5 hours trying to figure out how to bind a custom behaviour to a form thats rendered by all this "magic". One part of the problem is that this highly generic framework needs you to go through a ton of layers until you have a chance to understand how to do things. Also tracking where a certain configuration is coming from becomes incredibly tedious.
    – greenone83
    Commented Jul 18, 2017 at 17:42
  • 6
    Why Magento, why? Commented Jan 4, 2021 at 20:21
18

The binding for any of the knockout JS templates happens in the .xml files of the module. Using the Checkout module as an example you can find the config for the content template in vendor/magento/module-checkout/view/frontend/layout/default.xml

<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>

In this file you can see that the block class has nodes that define the "jsLayout" and call out the <item name="minicart_content" xsi:type="array">. It's a little bit of a round robin of logic, but if you are in vendor/magento/module-checkout/view/frontend/templates/cart/minicart.phtml you will see this line:

<div id="minicart-content-wrapper" data-bind="scope: 'minicart_content'">
    <!-- ko template: getTemplate() --><!-- /ko -->
</div>

So the data-bind directs where to look for any nested template, in this case it's the Magento_Checkout/js/view/minicart of vendor/magento/module-checkout/view/frontend/web/js/view/minicart.js for the logic (or MV in knockouts Model-View-View Model system) and you have Magento_Checkout/minicart/content (or V in knockouts Model-View-View Model system) for the template call. So the template that is being pulled in this spot is vendor/magento/module-checkout/view/frontend/web/template/minicart/content.html.

Really it's not to hard to figure out once you get use to looking in the .xml's. Most of this I learned here if you can make your way past the broken English. But so far i feel like the Knockout integration is the least documented part of M2.

2
  • 2
    Useful information, so +1, but per the question, I know Magento has abstractions to deal with this -- but I'm curious about the implementation details themselves. i.e -- when you configuration something in that XML file, magento does something else to make sure that your configured values does a third thing. I'm interested in the something else and the third thing. Commented Jun 9, 2016 at 21:46
  • so when you look at ` <!-- ko template: getTemplate() --><!-- /ko -->` in any .phtml file. it mean .xml file has this file in <item name=" "/> node.
    – Ali Raza
    Commented Dec 16, 2020 at 7:46
4

I'm pretty sure the global getTemplate JS method you're looking for is defined under app/code/Magento/Ui/view/base/web/js/lib/core/element/element.js you can find it here: https://github.com/magento/magento2/blob/4d71bb4780625dce23274c90e45788a72f345dd9/app/code/Magento/Ui/view/base/web/js/lib/core/element/element.js#L262

As I'm on my phone I'm having a hard time finding out exactly how the binding is getting done.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.