Hi Joel!
Regarding the following statement:
<!-- ko template: getTemplate() --><!-- /ko -->
This is utilizing Knockout's template binding functionality to render the template specified by the JavaScript component's getTemplate()
directive.
The getTemplate()
directive is defined in Magento's uiElement
JavaScript module (mapped with RequireJS to Magento_Ui/js/lib/core/element/element
).
'uiElement': 'Magento_Ui/js/lib/core/element/element'
Other Resources
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
Fetch the object returned by the specified RequireJS
module from the component
key (Magento_Customer/js/view/customer
)
Use that object to instantiate a new javascript object (see below)
Assign any extra data keys to that same object
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
Best Answer
The PHP code for a UI component renders a javascript initialization that looks like this
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)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
andlayout
RequireJS modules. The application also loads theMagento_Ui/js/lib/ko/initialize
RequireJS library. Theinitialize
module kicks off Magento's KnockoutJS integration.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 callsapplyBindings
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.
Specifically, the
data-bind="scope: 'customer_listing.customer_listing'">
attribute. When Magento kicks offapplyBindings
, Knockout will see this customscope
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
The important bit in this file is here
Without getting too into the details, the
registry.get
method will pull out an already generated object using the string in thecomponent
variable as an identifier, and pass it to theapplyComponents
method as the third parameter. The string identifier is the value ofscope:
(customer_listing.customer_listing
above)In
applyComponents
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 originaldiv
that useddata-bind=scope:
.So, what is the already instantiated component object? Remember the call to
layout
back inapp.js
?The
layout
function/module will descend into the passed indata.components
(again, this data comes from the object passed in viatext/x-magento-init
). For each object it finds, it will look for aconfig
object, and in that config object it will look for acomponent
key. If it finds a component key, it willUse
RequireJS
to return a module instance -- as though the module were called in arequirejs
/define
dependency.Call that module instance as a javascript constructor
Store the resulting object in the
registry
object/moduleSo, that's a lot to take in. Here's a quick review, using
as a starting point. The
scope
value iscustomer_listing.customer_listing
.If we look at the JSON object from the
text/x-magento-init
initializationWe see the
components.customer_listing.customer_listing
object has aconfig
object, and that config object has acomponent
object that's set touiComponent
. TheuiComponent
string is a RequireJS module. In fact, its a RequireJS alias that corresponds to theMagento_Ui/js/lib/core/collection
module.In
layout.js
, Magento has run code that's equivalent to the following.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 thelib/core/element/element
module and thelib/core/class
module. Researching these customizations is beyond the scope of this answer.Once instantiated,
layout.js
stores thisobject
in the registry. This means when Knockout starts processing the bindings and encounters the customscope
bindingMagento will fetch this object back out of the registry, and bind it as the view model for things inside the
div
. In other words, thegetTemplate
method that's called when Knockout invokes the tagless binding (<!-- ko template: getTemplate() --><!-- /ko -->
) is thegetTemplate
method on thenew collection
object.