3

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?

Brant Bobby
  • 14,956
  • 14
  • 78
  • 115

2 Answers2

4

When you extend an observable, the extender typically wraps the observable with another with the desired behavior. You could keep a reference to the original observable which will allow you to make direct writes to it all while exposing your throttled version of the observable normally.

e.g.,

var myObservable = ko.observable('foo');
var myThrottledObservable = myObservable.extend({ throttle: 500 });
myThrottledObservable('bar'); // delayed
myObservable('baz'); // immediate

In your particular use case, rather than throttling the waiting observable, throttle the outstandingRequests observable and use the throttled value in waiting.

var outstandingRequests = ko.observable(0);

// throttled requests for the waiting observable
var throttledOutstandingRequests = outstandingRequests.extend({ throttle: 500 });

// true if any requests are outstanding
var waiting = ko.computed(function() {
    return throttledOutstandingRequests() > 0;
};

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

Writes to your outstandingRequests observable happen immediately but your waiting observable will effectively be throttled.


Alternatively, a cleaner solution in my opinion would be to reimplement the throttled extender to add the ability to update immediately.

ko.extenders['throttleEx'] = function(target, timeout) {
    // Throttling means two things:

    // (1) For dependent observables, we throttle *evaluations* so that, no matter how fast its dependencies
    //     notify updates, the target doesn't re-evaluate (and hence doesn't notify) faster than a certain rate
    target['throttleEvaluation'] = timeout;

    // (2) For writable targets (observables, or writable dependent observables), we throttle *writes*
    //     so the target cannot change value synchronously or faster than a certain rate
    var writeTimeoutInstance = null;
    var throttled = ko.dependentObservable({
        'read': target,
        'write': function(value) {
            clearTimeout(writeTimeoutInstance);
            writeTimeoutInstance = setTimeout(function() {
                target(value);
            }, timeout);
        }
    });

    // add function to set the value directly
    throttled['immediate'] = function(value) {
        target(value);
    };

    return throttled;
};

Then to use it:

var waiting = ko.computed(function() {
    return outstandingRequests() > 0;
}.extend({ throttleEx: 500 });

// These are called when AJAX requests begin or end
function ajaxBegin() {
    outstandingRequests.immediate(++outstandingRequests());
}
function ajaxEnd() {
    outstandingRequests.immediate(--outstandingRequests());
}
Jeff Mercado
  • 129,526
  • 32
  • 251
  • 272
  • Ah, so `throttle` is just a wrapper, I see. But how can I bind my UI to multiple observables to get the desired behavior? – Brant Bobby Oct 31 '13 at 16:53
  • For the cleanest implementation, I would probably write my own implementation of the `throttle` extender to add that functionality and use that on your computed observable. Alternatively in your particular case, you could throttle the `outstandingRequests` observable instead of `waiting` and use the approach in my example. – Jeff Mercado Oct 31 '13 at 16:58
  • After having thought about your case, I don't think this is the ideal solution. Although delayed, _any_ change to `outstandingRequests` will trigger an update to `waiting` _eventually_ so quick changes will not lump them all into a single change. I'll have to rethink that one. – Jeff Mercado Oct 31 '13 at 19:05
  • It's not so much that I want them all lumped together into one change, but rather that I only want the observable to the throttled *sometimes*. In other words, I need a `throttleUnlessNewValueIsZero` extender... – Brant Bobby Oct 31 '13 at 19:08
  • On second thought, I was wrong about that. After looking back at the implementation of `throttled`, quick changes to the value will still send out only one update. It will still behave pretty much the same. – Jeff Mercado Oct 31 '13 at 19:39
2

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/

Michael Best
  • 16,623
  • 1
  • 37
  • 70