Component Communication in Knockout.JS
I'm designing a "medium" sized app in KnockoutJS and I'd like to know how to send events between components.
Imagine a nested component hierarchy in KnockoutJS:
Root Viewmodel -> A -> B -> C
-> D
How does D respond to the message from C? The obvious way to knock out JS is to have C write the observable passed as a parameter, and share that observable with D, which reacts to changes in the observable.
What I don't like about this approach is that A and B need to know about the message, while A and B actively forward the handler with their parameters. Using normal dependency injection methods, I can connect components C and D directly to each other, e.g. inject D into C without A and B knowing.
So my question is:
- Is there a way to manually wire components inside the root view model (eg by intercepting component creation)?
or rewrite:
- How can I configure nested components from the main viewmodel without looking at the parent component?
ko.components.register("aaa", {
viewModel: function (params) { this.handler = params.handler; },
template: "<span>A</span> <bbb params='handler: handler'></bbb>"
});
ko.components.register("bbb", {
viewModel: function (params) { this.handler = params.handler; },
template: "<span>B</span> <ccc params='handler: handler'></ccc>"
});
ko.components.register("ccc", {
viewModel: function (params) { this.handler = params.handler; },
template: "<span>C</span> <button data-bind='click: handler'>OK</button>"
});
ko.components.register("ddd", {
viewModel: function (params) {
var self = this;
this.text = ko.observable("No event received!");
if (params.onClick) params.onClick.subscribe(function () {
self.text("Event Received!");
});
},
template: "<span>D</span> <span data-bind='text:text'/>"
});
ko.applyBindings({
onClick: ko.observable()
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p><aaa params='handler: onClick'> </aaa></p>
<p><ddd params='onClick: onClick'> </ddd></p>
Is there a way to manually wire components inside the root view model (eg by intercepting component creation )?
(emphasis mine)
knockout mechanism
You can inject a custom component loader by adding ko.components.loaders
any combination of methods to an array of objects getConfig
, loadComponent
, , loadTemplate
, and loadViewModel
.
Since you're creating connections between view models, this is the only method we need to define. From the documentation:
loadViewModel(name, viewModelConfig, callback)
viewModelConfig values are just viewModel properties in any componentConfig object. For example, it could be a constructor, ...
this method
Your component is defined as a direct reference to the constructor. We'll wrap this constructor in a factory function that replaces what was passed to the component params
with a concatenated sharedParams
object that holds everything passed to any component on the chain. Whether this is "safe" enough is up to you. It should be easy to have another way to connect aaa
, ddd
once you have a custom loader you should be fine.
In short, our custom loader will:
- Retrieves the component's viewmodel's original constructor (
VM
) - Dynamically create a function
factory
that :- Add the passed content
params
to the binding context VM
Construct an instance with shared parameters- return new viewmodel
- Add the passed content
- call the newly created
factory
function
In code:
ko.components.loaders.unshift({
loadViewModel: function loadViewModel(name, VM, callback) {
const factory = function (params, componentInfo) {
const bc = ko.contextFor(componentInfo.element);
// Overwrite sharedParams
bc.sharedParams = Object.assign(
{ },
bc.sharedParams || {},
params
);
return new VM(bc.sharedParams);
};
return callback(factory);
}
});
Run the example
Check out the fiddle of this link and play with the custom loader yourself. I've included several nested component structures to show that params
manual passing is no longer necessary .