-
Notifications
You must be signed in to change notification settings - Fork 0
Asynchronous Dependent Observables
Dependent observables have to return a value synchronously. But what if you want to use a dependent observable to represent some data that you fetch using an asynchronous Ajax request?
For example, you might have a set of observables to represent query parameters, e.g.,
this.pageIndex = ko.observable(0);
this.sortColumn = ko.observable("lastName");
this.sortOrder = ko.observable("asc");
Conceptually, it makes sense to have a dependent observable to represent the result of performing a query using these parameters. After all, the result of the query is a function of the query parameters.
The traditional way to capture an asynchronous result is to set up a further observable - let's call it queryResults
- and then use a dependent observable to detect changes on any of the query parameters and populate queryResults
asynchronously as a side-effect. For example,
this.queryResults = ko.observable();
ko.dependentObservable(function() {
// Whenever "pageIndex", "sortColumn", or "sortDirection" change, this function will re-run and issue
// an Ajax request. When the Ajax request completes, assign the resulting value to "queryResults"
$.ajax("someUrl", {
data: { pageNum: this.pageIndex, sortBy: this.sortColumn, sortDirection: this.sortOrder },
success: this.queryResults
});
}, this);
The reason why the $.ajax
call is wrapped inside a ko.dependentObservable
is to ensure the request will be issued not just once when this code first runs, but also re-issued each time any query parameter changes. Automatic dependency tracking will detect the dependencies on pageIndex
, sortColumn
, and sortDirection
and force re-evaluation when they change.
This technique works fine, but if you're doing it a lot, wouldn't it be nicer to eliminate the separate queryResults
observable and just emit the output directly from your dependent observable somehow? Yes, and here's how...
A common mechanism for handling asynchronous operations is to have some object that represents the operation in progress:
- .NET developers may be familiar with the Task Parallel Library and Task<T>
- Ruby developers may know about EventMachine and its Deferrables
- If you use jQuery, you might know about its $.Deferred objects
What Task<T>
, Deferrable
, and $.Deferred
all have in common is that they represent the present-or-future availability of some result value. They all give you a way to add a callback so you'll be notified when the result becomes available (or you'll be called back immediately if the result is already available).
So, what if your dependent observable was to return a $.Deferred
object to represent an Ajax request that it issued? That technique would almost completely handle our requirements here.
The one thing that technique wouldn't handle, though, is letting you easily use regular bindings to display the result, because regular bindings don't understand $.Deferred
values. To solve this, you can create a standard wrapper around a dependent observable that captures the output for any $.Deferred
value and transfers it onto some other, normal observable, so you can apply bindings to that other observable in the normal way.
Here's a simple implementation:
function asyncDependentObservable(evaluator, owner) {
var result = ko.observable();
ko.dependentObservable(function() {
// Get the $.Deferred value, and then set up a callback so that when it's done,
// the output is transferred onto our "result" observable
evaluator.call(owner).done(result);
});
return result;
}
You can then use this, asyncDependentObservable
, in place of a regular dependent observable, and its result will appear asynchronously after any of its dependencies change. For example,
this.queryResults = asyncDependentObservable(function() {
// Whenever "pageIndex", "sortColumn", or "sortDirection" change, this function will re-run
return $.ajax("someUrl", {
data: { pageNum: this.pageIndex, sortBy: this.sortColumn, sortDirection: this.sortOrder }
});
}, this);
You can then bind queryResults
to your DOM elements in the usual way. Try it out - here's a live example on jsFiddle.net
What you've just seen may be perfectly sufficient in many cases, but you might want to add more functionality. For example,
- Gracefully handling result values that are either asynchronous (e.g.,
$.Deferred
), synchronous (regular JavaScript objects), or just null - Coping with out-of-order responses - ensuring that your dependent observable's value only ever represents the most-recently-requested data, even if the Ajax requests complete in a different order
- Exposing an
inProgress
sub-property so you can display a "loading" indicator in your UI
Here's a more sophisticated implementation that handles all of these things:
function asyncDependentObservable(evaluator, owner) {
var result = ko.observable(), currentDeferred;
result.inProgress = ko.observable(false); // Track whether we're waiting for a result
ko.dependentObservable(function() {
// Abort any in-flight evaluation to ensure we only notify with the latest value
if (currentDeferred) { currentDeferred.reject(); }
var evaluatorResult = evaluator.call(owner);
// Cope with both asynchronous and synchronous values
if (evaluatorResult && (typeof evaluatorResult.done == "function")) { // Async
result.inProgress(true);
currentDeferred = $.Deferred().done(function(data) {
result.inProgress(false);
result(data);
});
evaluatorResult.done(currentDeferred.resolve);
} else // Sync
result(evaluatorResult);
});
return result;
}
Try it out - here's a live example on jsFiddle.net
With Knockout 1.3, it will be possible to express this kind of facility as an "extender", so you could turn any dependent observable into a $.Deferred
-aware one like this:
this.someData = ko.dependentObservable(function() { /* ... */ }, this).extend({ async: true });
When KO 1.3 is released, this page will be updated to show that technique.
Alternatively, if the community would find it useful, we could consider enhancing all ko.dependentObservable
s so they are natively aware of $.Deferred
result types and automatically expose such results asynchronously.