Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Pausing Notifications in KnockoutJS

| Comments

On the forums for Knockout, there have been several questions related to temporarily preventing the changes to an observable from being sent out to subscribers. Typically, someone wants to apply many changes to their view model, perhaps as the result of an update call to the server, without having subscriptions triggered until the very end.

This behavior is not currently built into Knockout, but it is still possible to achieve this in a fairly straightforward way. Suppose we have a simple view model that looks like:

1
2
3
4
5
6
7
8
9
var viewModel = {
    users: ko.observableArray(usersFromServer)
}
//just the active users
viewModel.activeUsers = ko.computed(function() {
    return ko.utils.arrayFilter(viewModel.users(), function(user) {
        return user.isActive();
    });
}, viewModel);

If we receive updates from the server that add/remove users and/or modify the status of each user, then the activeUsers computed observable will be re-evaluated on each and every one of those changes.

To add the ability to pause activeUsers from being re-evaluated on every change, we can create an augmented computed observable with this added functionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//wrapper for a computed observable that can pause its subscriptions
ko.pauseableComputed = function(evaluatorFunction, evaluatorFunctionTarget) {
    var _cachedValue = "";
    var _isPaused = ko.observable(false);

    //the computed observable that we will return
    var result = ko.computed(function() {
        if (!_isPaused()) {
            //call the actual function that was passed in
            return evaluatorFunction.call(evaluatorFunctionTarget);
        }
        return _cachedValue;
    }, evaluatorFunctionTarget);

    //keep track of our current value and set the pause flag to release our actual subscriptions
    result.pause = function() {
        _cachedValue = this();
        _isPaused(true);
    }.bind(result);

    //clear the cached value and allow our computed observable to be re-evaluated
    result.resume = function() {
        _cachedValue = "";
        _isPaused(false);
    }

    return result;
};

We can now replace our computed observable with a pauseableComputed:

1
2
3
4
5
viewModel.activeUsers = ko.pauseableComputed(function() {
    return ko.utils.arrayFilter(viewModel.users(), function(user) {
        return user.active();
    });
}, viewModel);

When we want to apply many updates at once, we can wrap our actual updates with the pause() and resume() calls.

1
2
3
4
5
6
7
//callback from AJAX call to retrieve updates
viewModel.onUpdateUsersSuccess = function() {
    this.activeUsers.pause();
    //add or remove users
    //update status flags for users
    this.activeUsers.resume();
}.bind(viewModel);

The key fact that makes this work is that dependency detection is performed each time that a computed observable is re-evaluated. This means that the actual dependencies for a computed observable can change over time. In our case, the dependencies will look like:

When we call pause(), the _isPaused flag is set, which forces our computed observable to be re-evaluated. Now, we release our subscriptions to the users array and to the isActive flag of each user, but we do keep a cached copy of our latest value to return while we are paused.

When we call resume(), the cached value is thrown away and our computed observable is re-evaluated, because our only current dependency, the _isPaused flag, has now changed.

From what I have seen, the most common scenario is that a computed observable depends on many observables that you want to update en masse before re-evaluating the dependentObservable. However, at the bottom of the code for this sample, there is also an implementation that supports pausing writeable computed observables and one for pausing an observableArray as well (would be the same pattern for an observable).

Here is a live sample:

Link to full sample on jsFiddle.net

Comments