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
Use RequireJS
to return a module instance -- as though the module were called in a requirejs
/define
dependency.
Call that module instance as a javascript constructor
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.