Reputation: 343
I am desiging a "medium" sized application in KnockoutJS and I am wondering how to send events between components.
Imagined a nested component hierarchy in KnockoutJS:
Root Viewmodel -> A -> B -> C
-> D
How can D react to a message from C? The obvious knockoutJS approach would be to have C write to an observable passed as a parameter, and share this observable with D which reacts to changes to this observable.
What I do not like about this approach is, that A and B need to know about the message, and A and B actively forward the handler via its parameters. With a normal dependency Inject approach I could wire up the components C and D directly with each other, e.g. by injecting D into C without A and B knowing.
So my question:
or rephrased:
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>
Upvotes: 3
Views: 2101
Reputation: 411
What I've done is when D is created by C, C passes a parameter as a reference to D, and D assigns itself to this parameter. The param can be an observable, and the onDispose of D can clear the observable so that C knows if D was disposed.
Upvotes: 0
Reputation: 23382
Is there a way to manually wire up the components inside the root viewmodel (e.g. by intercepting component creation)?
(emphasis mine)
You can inject custom component loaders by adding an object to the ko.components.loaders
array with any combination of the methods getConfig
, loadComponent
, loadTemplate
, and loadViewModel
.
Since you're looking to create a connection between view models, this is the only method we need to define. From the docs:
loadViewModel(name, viewModelConfig, callback)
The viewModelConfig value is simply the viewModel property from any componentConfig object. For example, it may be a constructor function, ...
Your components are defined referencing a constructor function directly. We're going to wrap this constructor in a factory function that replaces the params
passed to the component, by a joined sharedParams
object that holds everything that's passed to any component up the chain. Whether this is "safe" enough is up to you. It shouldn't be too hard to come up with another way of connecting aaa
and ddd
once you got the custom loader in place.
In short, our custom loader will:
VM
)factory
function that:
params
to the binding contextVM
instance with the shared paramsfactory
functionIn 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);
}
});
Check out this linked fiddle to play with the custom loader yourself. I've included several nested component structures to show there's no longer the need to pass the params
manually.
https://jsfiddle.net/sqLathu9/
Upvotes: 1
Reputation: 5241
In that case of scenarios I think it's better to use EventEmitter https://github.com/Olical/EventEmitter
This way you can create a global ee
instance of EventEmitter
for example in a script on your main html
window.ee = new EventEmitter()
then you can add listeners or trigger actions between components:
as for your example from component C you can trigger an event with: window.ee.emitEvent('some-name-for-the-event', someArgumentsArray)
and in your D component just add a listener for that event to react accordingly like window.ee.addListener('some-name-for-the-event', function (params) {
// your own code to handle the event
});
Upvotes: 0
Reputation: 23382
A solution that I personally find quite ugly, but can be considered a valid answer to this question:
You can use $root
, $parents[i]
or $parent
to refer to any layer in the binding context of a component. I removed some of the params
from your example to show you how this would work. It shows two flavors:
bindingContext
inside the params
attribute (preferred)bindingContext
inside the component template (not recommended)ko.components.register("aaa", {
viewModel: function (params) { },
template: "<span>A</span> <bbb></bbb>"
});
ko.components.register("bbb", {
viewModel: function (params) { },
template: "<span>B</span> <ccc></ccc>"
});
ko.components.register("ccc", {
viewModel: function (params) { },
// This hides the dependency in the template. It's better to pass it in the DOM via `params`
template: "<span>C</span> <button data-bind='click: $root.onClick'>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> </aaa></p>
<!-- This approach isn't *that* ugly, since the dependency is made explicit in the DOM -->
<p><ddd params='onClick: $root.onClick'> </ddd></p>
The weakness here is that you create a fixed dependency on the DOM structure that is hard to document/check. I usually start running in to bugs as soon as I use $parent
...
Upvotes: 0