backbone
backbone copied to clipboard
Return a rejected promise instead of false on invalid model save
From the discussion in #2345, returning false from a failed save prevents a clean chaining API (model.save().done(...).fail(...)
). Using deferreds, we can reject a promise instead to avoid the discrepancy.
This pull doesn't introduce any new options vars (i.e. isValid
), instead suggesting a separate event listener for the model's invalid
event or querying model.isValid()
directly within the failed handler. The invalid
event will always be fired before the deferred is rejected and returned. The fail handler is fired for both failed validation as well as any ajax errors which makes it difficult to mutate the options object or keep the two callbacks' arguments consistent.
If we're going to allow swapping the promise lib, it would be great if that applied to all ajax instances...
sync: function() {
return Backbone.Deferred().when(Backbone.sync.apply(this, arguments));
}
Since jQuery's promises don't follow the promises spec... I would really, really love to see this change.
@tgriesser I would want to stay away from relying on when
and stick to the generic reject
, resolve
, promise
API... when
isn't in every promise implementation and in any case in jQuery it lives directly on the $
, not on $.Deferred()
@wookiehangover - ah right, it does live on $... I guess I was thinking that it'd be nice if we were going to use promises (outside of the ones included in jqXHR), and you want to swap it for an implementation that works properly (not jQuery's), it'd be nice to have that switch be automatically applied across the board... but I guess it only takes a simple shim of Backbone.ajax
to do that...
Ran into this again today when I forgot you can't assume a promise on the save
. I think it'd be a nice enhancement.
@jashkenas can we get a go/no go on merging this?
I have to say, this one makes me a bit nervous. Despite returning promises, Backbone doesn't currently use them. While it might be slightly nicer to get a promise instead of false
(and this is certainly debatable), it's a giant can of worms to open for a fairly small issue.
@braddunbar which is why I closed the original issue in the first place.... @jashkenas opened it back up in the interest of having a cleaner api.
But there are several things that fall out of this, including breaking out-of-the-box compatibility with Zepto once and for all (which has already happened IMO, but I digress)
Yep, Zepto and any other library with a similar API. Good point.
Yes -- I'm fairly far towards leaning towards a "no go" on this ... but I haven't sat down and thought about it as much as I'd like to just yet. Maybe tag as a change wontfix
, but leave it open a little longer?
As @ErichBSchulz pointed out in #2597, jqXHR's success
, error
, and complete
~~callbacks~~ methods were deprecated in jQuery 1.8 in favor of the promises API, and may be removed in a future version. I agree Backbone should generally keep its API surface small, but I think this solution is much more elegant when dealing with these async actions. Checking for a false return value separate from a server / network failure just feels like a kludge.
To @braddunbar's point, adding extra stuff is always a slippery slope, but the advantage is this opens Backbone up to do a lot better with success callbacks along the Backbone.sync
path (no more var success = options.success
cascades).
Zepto support is easily shimmed with Simply Deferred.
edit: clarified callbacks wording. The methods on jqxhr are deprecated, not the passed-in options.
Closing this particular PR. I'd still be happy to look at and discuss a more holistic approach to pervasive promises, in concert with a version bump ... but it looks like this patch is incomplete.
It seems like the best solution in the interim is to wrap all save
/destroy
calls with Q/when/etc. then, right? That is, if you cared about promise correctness you probably wouldn't be using jQuery's promises in the first place...
I still think that this PR is independent of #3582
The only thing that we should agree that
save
and destroy
should always return thenables.
Thus, if we combine this PR and #3582 it will look like:
Backbone.newDestroyed = function(model) {
return Backbone.Deferred().resolve(model);
}
Backbone.notValidated = function(model) {
return Backbone.Deferred().reject(model.validationError);
}
There also a question: should the obtained xhr be also wrapped into Backbone.Defered().resolve(xhr)? To unify all thenables?
I see this as solving the root problem brought up by #3582. Additionally, it only adds one method onto the Backbone
namespace, and it's a well-defined class (deferred
s are easy to create from Promise libraries).
There also a question: should the obtained xhr be also wrapped into Backbone.Defered().resolve(xhr)? To unify all thenables?
So that all thenable return values are the same class, yes we should. Imagine overriding Backbone.Deferred
to use native promises:
Backbone.Deferred = function() {
var deferred = {};
deferred.promise = new Promise(function(resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
};
I'd hate for some return values to be jQuery promises and some to be native promises.
As a side note, I don't think we should return deferred.resolve()
. #resolve
and #reject
return a non-standard value (the deferred, vs undefined
), and that the deferred object has #then
is non-standard.
So why Deferred? Why not Backbone.Promise?
And use pretty sandard Backbone.Promise.resolve(value)
and Backbone.Promise.reject(reason)
?
There is widespread belief that you should avoid using deferreds at all.
So why Deferred? Why not Backbone.Promise?
Because jQuery provides a Deferred class, so we don't have to write a ton of extra code.
There is widespread belief that you should avoid using deferreds at all.
Hm?
Opening for more discussion ... but we'd still need a real patch+proposal.
I would strongly advocate for the Backbone.Promise
that should provide only two static methods resolve
and reject
declare module Backbone {
...
export interface Thenable<T> {
then<U>(
fulfilled: (result: T) => U | Thenable<U>,
rejected: (reason: any) => U | Thenable<U>
): Thenable<U>
}
...
export class Model<TSync> {
save(...): Thenable<TSync>
fetch(...): Thenable<TSync>
destroy(...): Thenable<TSync>
sync(...): Thenable<TSync>
}
export class Collection<TModel extends Model, TSync> {
fetch(...): Thenable<TSync>
sync(...): Thenable<TSync>
}
export function sync<TModel, TOpts, TSync>(
method: string,
model: TModel,
options: TOpts
): Thenable<TSync>
export class Promise {
// lets rely only on those two static methods:
static resolve<T>(value: T | Thenable<T>): Thenable<T>;
static reject<T>(reason: any): Thenable<T>;
}
}
Inside you can use promises created by jQuery.Deferred by default or the same jqXHR
but Backbone itself should rely only on two standard static methods that are placed under Backbone.Promise: resolve
and reject
.
My motivation:
- Those who never want to override methods will get the same jqXHR and JQueryPromises by the default implementation. Note that we do not need to implement promises in Backbone, we will reuse jQuery Deferred.promise() or similar things
- Those who wnat to override this, probably want to use Promises/A+ implementation and would be able to do it with single:
Backbone.Promise = MyFavoritePromiseClass
I agree fully with @Artazor here (and thanks for the detailed interface). Do you propose we wrap sync
(or ajax
) in Backbone.Promise
or should the implementing user's ajax
method be required to return a thenable?
It'd be awesome to see a pull with this implementation if you have time.