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
|
|
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 |
|
To send out a notification on this topic we would do:
1
|
|
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 |
|
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 |
|
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 |
|
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 cleansubscribe
andpublish
methods that take a topic as the first argument. - adds a
subscribeTo
function to all subscribables (observables, observableArrays, and computed observables). ThesubscribeTo
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 bysubscribeTo
. - adds a
publishOn
function to all subscribables that automatically publishes out new values. ThepublishOn
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 thepublishOn
messages. - adds a
syncWith
function that does both asubscribeTo
and apublishOn
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