Magento 2 – How KnockoutJS Bindings Are Applied

javascriptknockoutjsmagento2requirejs

Per a very cursory reading of the KnockoutJS documentation, initializing a very basic Knockout view looks like the following

// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
    this.firstName = "Bert";
    this.lastName = "Bertington";
}

// Activates knockout.js
ko.applyBindings(new AppViewModel());

i.e. — you create a javascript function intended to be used as an object constructor, instantiate an object from it, and then pass that object into the ko.applyBindings method of the global knockout object (ko)

However, in Magento 2, if you load a backend page with a Grid UI, Magento will initialize the js/core/app.js RequireJS module

/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
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);
    };
});

This module, in turn, loads the Magento_Ui/js/lib/ko/initialize module, which appears to initialize Magento's use of KnockoutJS. However, if you look at the source of initialize module.

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();
});

You see Magento's called the ko.applyBindings(); object without a view object. This doesn't make any sense, and I'm not sure if it's my limited understanding of Knockout, or Magento doing something custom/strange here.

Is this where Magento actually applies Knockout bindings? Or does that happen somewhere else? Or is Magento doing something tricky to intercept Knockout code and process it elsewhere?

Best Answer

The Magento_Ui/js/lib/ko/initialize library is, indeed, where Magento initializes its Knockout instance. Magento does not assign a ViewModel when it applies bindings.

The missing key here is the custom KnockoutJS binding named scope.

When Magento's Knockout instance encounters a scope: binding like this

<li class="greet welcome" data-bind="scope: 'customer'">
    <span data-bind="text: customer().fullname ? $t('Welcome, %1!').replace('%1', customer().fullname) : 'Default welcome msg!'"></span>
</li>

It takes the value of that binding (named customer), and uses it to load and apply a ViewModel for the inner nodes from the uiRegistry. You can debug the data bound for a particular scope with some simple KnockoutJS pre debugging

<div data-bind="scope: 'someScope'">
    <pre data-bind="text: ko.toJSON($data, null, 2)"></pre>            
</div>

The uiRegistry is a simple dictionary like object, implemented in the Magento_Ui/js/lib/registry/registry RequireJS module.

vendor/magento/module-ui/view/base/requirejs-config.js
17:            uiRegistry:     'Magento_Ui/js/lib/registry/registry',

Objects are put into the registry via the bits of javascript that look like this

<script type="text/x-magento-init">
{
    "*": {
        "Magento_Ui/js/core/app": {
            "components": {
                "customer": {
                    "component": "Magento_Customer/js/view/customer",
                    "extra_data_1":"some_value",
                    "more_extra":"some_other_value",
                }
            }
        }
    }
}
</script>

The program in the Magento_Ui/js/core/app module will examine the components key of the passed in object, and for each sub-object will

  1. Fetch the object returned by the specified RequireJS module from the component key (Magento_Customer/js/view/customer)

  2. Use that object to instantiate a new javascript object (see below)

  3. Assign any extra data keys to that same object

  4. Add that same object to the uiRegistry with the key of the original object (customer above)

If you're not sure how the x-magento-init script works, I've written an article about it here.

There's a more in depth examination of the app.js process over in this answer.

The implementation of the scope binding is defined here

vendor/magento//module-ui/view/base/web/js/lib/ko/bind/scope.js
Related Topic