Knockout: get reference to component A to call one of A's function from component B?

4.3k Views Asked by At

Used Yeoman's Knockout generator (c.a. early 2015) which includes in require.js and router.js. Just using KO's loader.

Am attempting to call a function (ko.observable or not) in component "a" from component "b". All the fluff below attempts to do merely:

// In componentB:

ComponentA.sayFoo();

Read KO docs on components and loaders, hacked for hours, etc. I don't want the overhead of say postal.js - and also could not get subscriptions (KO pub/sub) to work - I'm guessing for the same reason: the view models set up this way have no references to each other (?) - so the subscribers in one module don't see the publishers in another (right?) (… a bit over my head here %-)

1) Is this because the modules don't see each other… that this generated code does not place the KO stuff in a global namespace?

2) Trying to reach from one module to the other, seems to hinge on the getting the ref via the callback parms, using the function below, or is that incorrect? :

  ko.components.get (name, callback) ;

startup.js using require looks like this:

define(['jquery', 'knockout', './router', 'bootstrap', 'knockout-projections'], function($, ko, router) {

// Components can be packaged as AMD modules, such as the following:

   ko.components.register('component-a', { require: 'components/a/component-a' });  
   ko.components.register('component-b', { require: 'components/b/component-b' });  

// [Scaffolded component registrations will be inserted here. To retain this feature, don't remove this comment.]
// [Scaffold component's N/A (I think?)] 

// Start the application
   ko.applyBindings({ route: router.currentRoute });  
});

The (component) module A is straight forward, like this:

define(['knockout', 'text!./component-a'], function(ko, templateMarkup) {

   function ComponentA (params) { console.log ('CompA'); } ;

   ComponentA.prototype.sayFoo  = function () { console.log ('FOO!'); } ;
   ComponentA.prototype.dispose = function(){};

   return { viewModel: ComponentA, template: templateMarkup };
});

Similarly, module B is:

define(['knockout', 'text!./component-b'], function(ko, templateMarkup) {

   function ComponentB (params) { console.log ('Compb'); } ;

   ComponentB.prototype.doFoo  = function () { 
     //// B Needs to fire ComponentA.foo() … SEE CODE ATTEMPT BELOW 
   };

   ComponentB.prototype.dispose = function(){};

   return { viewModel: ComponentB, template: templateMarkup };
});

So this is where I'm stuck:

  ComponentB.prototype.doFoo  = function () { 
        ko.components.get ('component-a', ( function (parms) {
           console.log ('parms.viewModel : ' + parms.viewModel );  
           // parms.viewModel is (unexpectedly) undefined ! So how to get the ref?
           console.log ('parms.template : ' + parms.template );  
          // does have expected html objects, eg. [object HTMLwhatever], [object HTML...]
 })) ; 

This should be easy, or I'm dumbly leaving out something obvious!?

Maybe the modules need to be defined / set up differently?

Any suggestions would assist! Thx

2

There are 2 best solutions below

3
Bragolgirith On

This is just not how you'd normally communicate between knockout components.

Your options are:

1) Use https://github.com/rniemeyer/knockout-postbox. This is probably the best option as it integrates nicely with knockout. It is well documented and if you have troubles setting it up, you can always ask for help here.

2) Use any other global javascript EventBus (f.i. postal.js) and emit/subscribe to events in your components.

3) Have your root ViewModel pass common observables to each component as parameters - that way each component could modify/subscribe to the same observable.

4) (Probably what you want, although the worst scaling solution) If you give ids to the different components you could use ko.dataFor(document.getElementById("id")) to directly access the properties and methods of your components.

EDIT: In response to the comment:

I haven't been able to determine what / where the root view model is: ko.applyBindings({ route: router.currentRoute }) is the clue, but router.js is convoluted. Suggestions on how to determine that?

Exactly - in your case the { route: router.currentRoute } object IS your root ViewModel. It currently only has one property called route, but you could definitely extend that.

For instance:

var rootViewModel = {
    route: router.currentRoute,
    mySharedObservable: ko.observable('hi!')
}

ko.applyBindings(rootViewModel);

Then you can pass that observable to multiple components as a parameter like this:

<div id="component-a" data-bind="component: { name: 'component-a', params: {router: $root.router, mySharedObservable: $root.mySharedObservable} }"></div>
<div id="component-b" data-bind="component: { name: 'component-b', params: {router: $root.router, mySharedObservable: $root.mySharedObservable} }"></div>

And finally you can use the new observable from within the component like this:

function ComponentB (params) { 
    this.mySharedObservable = params && params.mySharedObservable;
    console.log(this.mySharedObservable());// This should log 'hi!'
};

You can now subscribe to the observable, change it and so on. It will be shared between components, so changing it one component will trigger the subscriptions in all components.

2
Retsam On

My standard approach would be to control the communication through the parent VM.

The parent VM can create a subscribable[1], and pass it to both componentA and componentB as a parameter; then ComponentA can subscribe to the subscribable, and ComponentB can trigger the subscribable.

//ComponentA
function ComponentA (params) { 
    var shouldSayFoo = params.shouldSayFoo;

    this.shouldSayFooSubscription = shouldSayFoo.subscribe(function () {
        this.sayFoo();
    });
} ;

ComponentA.prototype.sayFoo  = function () { console.log ('FOO!'); } ;
ComponentA.prototype.dispose = function () { this.shouldSayFooSubscription.dispose(); };

//In ComponentB
function ComponentB (params) {
    this._triggerFoo = params.triggerFoo; //Same subscribable as shouldSayFoo in ComponentA
}

ComponentB.prototype.doFoo = function () {
    this._triggerFoo.notifySubscribers(); //notifySubscribers triggers any .subscription callbacks
}

If ComponentA and ComponentB are siblings and you don't do this sort of stuff all the time, this works as a decently simple solution. If the components are "distant relatives" or if you find yourself doing this a lot, then I'd suggest some sort of pub-sub. And an advantage of this approach can be used by a lot of individual "A-B" pairs without interfering with each other, which is harder with a pub-sub system.

[1]: A ko.subscribable is an object with a subset of the observable functionality (ko.observable inherits from ko.subscribable): it doesn't let you read or write values, but lets you do .subscribe and .notifySubscribers. (They're created with new for some reason) You could use an observable, too, it's just a slight confusion of intent to create an observable if you don't intend it to hold a value.