Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add support for jQuery.Deferred on ajax calls #289

Closed
wants to merge 3 commits into from

5 participants

@PaulUithol

Deferreds (as of jQuery 1.5, see http://api.jquery.com/category/deferred-object/) can make it much easier to cope with (possibly) async behavior. See for examples http://www.erichynds.com/jquery/using-deferreds-in-jquery/ .

I've tried to incorporate this in Backbone without making any backwards incompatible change (so, didn't touch any return values) by adding a 'promise' attribute to Model and Collection that gets updated when using the Collection.fetch and Model.fetch/save/destroy functions. An alternative would be to make these methods return the XHR object right away.

This is (at least for me ;) ) very useful in cases where you may (or may not) need to make a number of requests to the server. Example:

var req = model.save();
// or, when using a version where the latest request is exposed as a property:
var req = model.save().request;

// all callbacks are executed whenever the request finishes (or right away if it's finished already)
req
    .done( function( response, textStatus, xhr ) {
        // success callback
    })
    .fail( function( response, textStatus, xhr ) {
        // error callback
     })
    .then( function( response, textStatus, xhr ) {
        // called on complete
    });

A more complicated example (where this feature is even nicer to have):

// Get a Collection of Tags; one or more may have to be created
var tags = TagList.getOrCreateByName( ['inbox', 'waiting for', 'work' ] );

// Get an array of all the promises
var promises = tags.map( function( tag ) {
    return tag.promise;
});

// $.when accept one or more promises, but not an array of promises, so use apply here.
// The .then() will execute when all Tags are either found, or created
$.when.apply( null, promises ).then( function() {
    // Do something that requires all Tags to be created
});

The change doesn't depend jQuery btw; if the xhr object doesn't have a "promise" function, it just assigns the xhr object itself.

@PaulUithol PaulUithol Make it possible to take advantage of jQuery.Deferred with Backbone, …
…without breaking compatibility by changing return values.

Implemented by adding a "promise" attribute to Backbone.Model and Backbone.Collection, set by Backbone.Model's "fetch", "save", "destroy" and Backbone.Collectin's "fetch" and "create".
ed5a88d
@dvv

Promises allow to normalize sync/async calls, and are very welcome

@lukebaker

I was just pondering adding support for deferreds as well, so well done! Am I correct in thinking that there's no guarantee that a given model will have only one pending promise? A user could conceivably save an existing model and then decide they'd rather delete it while the save is still pending. I guess you can hope that your code is done with the save promise before the user initiated the delete (which would overwrite the save promise). This sounds like it might be an acceptable trade-off for not changing the return values. Are there any other options available?

Is there any value in exposing the result of $.ajax() instead of just the .promise()? For instance, can I abort the Ajax request with .promise()?

@PaulUithol

Yes, you're correct in that. That's the tradeoff I made as well. But you currently run the same risk of course, if you render the model before waiting for the success callback. If do want to do that and expect that kind of behavior, you could probably choose to only execute the delete action after the original promise has finished, by doing something (in a view) like

remove: function() {
    $.when( this.model.promise ).then( this.model.destroy );
}

On the second point, from what I understand, returning the promise() actually returns sort of a 'protected' Deferred object (hides the resolve/reject methods).

Directly exposing the request is probably just as good here; this would also give you access to the normal $.ajax methods (like abort), as well all methods of a Deferred object. In that case, naming the property on Model/Collection 'request' would probably be better; actually, that's also more appropriate when not using jQuery. Doing that would make this change even simpler.

@keturn

As a new user who doesn't yet have backwards-compatibility to worry about, I'll say that having a Deferred return value sounds much better than having to check a property after making the call.

I don't mean to minimize the importance of a stable API, but on the other hand, it looks like those methods are only returning "this" for method chaining and their return value isn't documented, so as far as API breakages go, this is a pretty small one.

@PaulUithol

Well, I can see the point for the methods that operate directly on a Model (fetch/save/destroy), and Collection.fetch. Just returning the xhr object would be sufficient.

Collection.create is a different matter though. Having the new Model returned from that method is necessary to get a ref to it; and the 'request' property could still be used to give you access to the Deferred in that case.

I don't really have a preference either way; does anyone allowed to make actual changes to Backbone?

@jashkenas jashkenas referenced this pull request from a commit
@jashkenas Issue #289. Enable the use of jQuery.Deferred by returning Deferred o…
…bjects from save() and fetch() calls.
222d673
@jashkenas
Owner

The next release of Backbone is going to be a major version in any case, so it's a good time to make a slight change to the API. As of the previous commit, Backbone now returns the jQuery.Deferred object where possible. Collection#create still returns a reference to the model, so if you'd like to use deferreds with model creation, you'll have to use a pattern like this:

var model = new Model(attrs, {collection: collection});
var deferred = model.save();
@jashkenas jashkenas closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 22, 2011
  1. @PaulUithol

    Make it possible to take advantage of jQuery.Deferred with Backbone, …

    PaulUithol authored
    …without breaking compatibility by changing return values.
    
    Implemented by adding a "promise" attribute to Backbone.Model and Backbone.Collection, set by Backbone.Model's "fetch", "save", "destroy" and Backbone.Collectin's "fetch" and "create".
  2. @PaulUithol
  3. @PaulUithol
This page is out of date. Refresh to see the latest.
Showing with 13 additions and 5 deletions.
  1. +13 −5 backbone.js
View
18 backbone.js
@@ -145,6 +145,10 @@
// CouchDB users may want to set this to `"_id"`.
idAttribute : 'id',
+ // The most recent request object (set on 'fetch', 'save' and 'destroy').
+ // Enables the use of jQuery.Deferred methods.
+ request: null,
+
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize : function(){},
@@ -264,7 +268,7 @@
if (success) success(model, resp);
};
options.error = wrapError(options.error, model, options);
- (this.sync || Backbone.sync).call(this, 'read', this, options);
+ this.request = (this.sync || Backbone.sync).call(this, 'read', this, options);
return this;
},
@@ -282,7 +286,7 @@
};
options.error = wrapError(options.error, model, options);
var method = this.isNew() ? 'create' : 'update';
- (this.sync || Backbone.sync).call(this, method, this, options);
+ this.request = (this.sync || Backbone.sync).call(this, method, this, options);
return this;
},
@@ -297,7 +301,7 @@
if (success) success(model, resp);
};
options.error = wrapError(options.error, model, options);
- (this.sync || Backbone.sync).call(this, 'delete', this, options);
+ this.request = (this.sync || Backbone.sync).call(this, 'delete', this, options);
return this;
},
@@ -415,6 +419,10 @@
// This should be overridden in most cases.
model : Backbone.Model,
+ // The most recent request object (set on 'fetch'). Enables the use of
+ // jQuery.Deferred methods.
+ request: null,
+
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize : function(){},
@@ -507,7 +515,7 @@
if (success) success(collection, resp);
};
options.error = wrapError(options.error, collection, options);
- (this.sync || Backbone.sync).call(this, 'read', this, options);
+ this.request = (this.sync || Backbone.sync).call(this, 'read', this, options);
return this;
},
@@ -1006,7 +1014,7 @@
}
// Make the request.
- $.ajax(params);
+ return $.ajax(params);
};
// Helpers
Something went wrong with that request. Please try again.