Search code examples
knockout.jscomputed-observable

How can I force a throttled observable to update immediately?


I show or hide a "Loading" indicator on my UI by binding its visibility to an observable named waiting, which is defined like this:

// Viewmodel
var outstandingRequests = ko.observable(0);

// true if any requests are outstanding
var waiting = ko.computed(function() {
    return outstandingRequests() > 0;
}.extend({ throttle: 500 });

// These are called when AJAX requests begin or end
function ajaxBegin() {
    outstandingRequests(++outstandingRequests());
}
function ajaxEnd() {
    outstandingRequests(--outstandingRequests());
}

<!-- View -->
<div data-bind="visible: waiting">Please wait, loading...</div>

I'm throttling the waiting observable because I don't want the loading message to appear unless the request is taking a long time (>500ms in this case), to increase the perceived speed of the application. The problem is that once a long-running request finishes, the loading indicator doesn't disappear until an additional 500ms has passed. Instead, when the last outstanding request finishes, I want waiting to flip to false immediately.

My first attempt at a fix involved using valueHasMutated(), but the update is still delayed.

function ajaxEnd() {
    outstandingRequests(--outstandingRequests());
    // If that was the last request, we want the loading widget to disappear NOW.
    outstandingRequests.valueHasMutated(); // Nope, 'waiting' still 500ms to update :(
}

How can I bypass the throttle extension and force waiting to update immediately?


Solution

  • What you really want is to delay the notifications from the waiting observable when it becomes true. This can be done by intercepting the notifySubscribers function of the observable:

    var originalNotifySubscribers = this.isWaiting.notifySubscribers,
        timeoutInstance;
    this.isWaiting.notifySubscribers = function(value, event) {
        clearTimeout(timeoutInstance);
        if ((event === 'change' || event === undefined) && value) {
            timeoutInstance = setTimeout(function() {
                originalNotifySubscribers.call(this, value, event);
            }.bind(this), 500);
        } else {
            originalNotifySubscribers.call(this, value, event);
        }
    };
    

    jsFiddle: http://jsfiddle.net/mbest/Pk6mH/

    EDIT: I just thought of another, possibly better, solution for your particular case. Since the waiting observable only depends on one other observable, you can create a manual subscription that updates the waiting observable:

    var timeoutInstance;
    this.isLoading.subscribe(function(value) {
        clearTimeout(timeoutInstance);
        if (value) {
            timeoutInstance = setTimeout(function() {
                this.isWaiting(true);
            }.bind(this), 500);
        } else {
            this.isWaiting(false);
        }
    }, this);
    

    jsFiddle: http://jsfiddle.net/mbest/wCJHT/