Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Using KO's Native PubSub for Decoupled Synchronization

| Comments

In the previous post, I suggested some ideas for binding against multiple view models in Knockout.js. When working with more than one view model, a common task is deciding how to communicate between the separate components. Creating direct references between them often doesn’t feel right and can lead you down a path where each view model has lost its independence and cannot be used effectively without the other objects.

To handle this situation, we can keep our components loosely coupled by using a messaging system where each view model or component does not need to have direct references to its counterparts. There are several benefits to using this technique:

  • Components remain independent. They can be developed, tested, and used in isolation.
  • Knockout view models and non-KO components can communicate using a common interface without direct knowledge of each other.
  • Components can be safely refactored. Properties can be renamed, moved, and adjusted without worrying about breaking compatibility.

Knockout’s native pub/sub

There are already many libraries and options for providing this type of message bus. One option might be triggering custom events or using great pub/sub libraries like amplify.js or postal.js. While these libraries provide robust capabilities, Knockout itself already has all of the tools to do basic pub/sub communication built-in. This is part of the ko.subscribable type, which has its capabilities added to observables, computed observables, and observableArrays.

Normally, you would not construct a ko.subscribable directly, but it is easy enough to do so:

1
var postbox = new ko.subscribable();

In Knockout 2.0, support was added for topic-based subscriptions to aid in sending out beforeChange notifications. To create a topic-based subscription against our ko.subscribable, we would simply do:

1
2
3
4
5
6
postbox.subscribe(callback, target, topic);

//with some actual code
postbox.subscribe(function(newValue) {
    this.latestTopic(newValue);
}, vm, "mytopic");

To send out a notification on this topic we would do:

1
postbox.notifySubscribers(value, "mytopic");

Now, we can do basic pub/sub messaging using subscribe and notifySubscribers against our mediator, the postbox object. However, whenever I explore integrating a new technique with Knockout, I try to consider how to make it feel as easy and natural as possible.

Extending Observables

A typical scenario for this type of functionality would be that we want to synchronize an observable between view models. This might be a one-way or even a two-way conversation. To make this easy, we can look at extending the ko.subscribable type, which would affect observables, observableArrays, and computed observables.

Suppose that we want to setup an observable to automatically publish on a topic whenever it changes. We would want to set up a manual subscription against that observable and then use notifySubscribers on our postbox.

1
2
3
4
5
6
7
8
9
10
ko.subscribable.fn.publishOn = function(topic) {
    this.subscribe(function(newValue) {
        postbox.notifySubscribers(newValue, topic);
    });

    return this; //support chaining
};

//usage of publishOn
this.myObservable = ko.observable("myValue").publishOn("myTopic");

Now, whenever the observable’s value changes, it will publish a message on the topic.

On the other side, we might want to make it easy for an observable to update itself from messages on a topic. We can use this same concept:

1
2
3
4
5
6
7
8
ko.subscribable.fn.subscribeTo = function(topic) {
    postbox.subscribe(this, null, topic);

    return this;  //support chaining
};

//usage
this.observableFromAnotherVM = ko.observable().subscribeTo("myTopic");

Notice that we can just pass this (the observable) in as the callback to the subscribe function. We know that an observable is a function and that it will have its value set when you pass an argument into the function. So, there is no need to write something like:

1
2
3
4
//no need to add an extra anonymous function like:
postbox.subscribe(function(newValue) {
    this(newValue);
}, this, topic);

Now, our observables can exchange information without direct references to each other. They do need to agree on a topic, but do not have any knowledge about the internals of the other view model or component. We can even mock or simulate the other components in testing scenarios by firing messages.

Potential gotcha: publishing objects

If the new value being published is an object, then we need to be careful, as both sides will have a reference to the same object. If code from multiple view models makes changes to that object, then we are likely no longer as decoupled as we would like. If the object is simply configuration/options passed as an object literal that are not modified, then this seems okay. Otherwise, it is preferable to stick to primitives in the values being published. Another alternative is to use something like ko.toJS to create a deep-copy of the object that is being published, so neither side has the same reference.

A new library: knockout-postbox

I created a small library called knockout-postbox to handle this type of communication. It uses the techniques described above and adds a bit of additional functionality:

  • creates a ko.postbox object with clean subscribe and publish methods that take a topic as the first argument.
  • adds a subscribeTo function to all subscribables (observables, observableArrays, and computed observables). The subscribeTo function also allows you to initialize your observable from the latest published value and allows you to pass a transform function that runs on the incoming values.
  • adds an unsubcribeFrom function to all subscribables that removes subscriptions created by subscribeTo.
  • adds a publishOn function to all subscribables that automatically publishes out new values. The publishOn function also allows you to control whether you want to immediately publish the initial value, and lets you supply an override function (equalityComparer) that allows you to compare the old and new values to determine if the new value should really be published.
  • adds a stopPublishingOn function to all subscribables that removes the subscription that handles the publishOn messages.
  • adds a syncWith function that does both a subscribeTo and a publishOn on the same topic for two-way synchronization.

Project link: https://github.com/rniemeyer/knockout-postbox

Basic sample:

Link to sample on jsFiddle.net

Next post: using this concept to integrate with client-side routing in a way that is natural to Knockout

Comments