From 69b74cfcb81cbbebafd8d07bb780fe55d681f335 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Sun, 31 May 2015 17:58:59 -0400 Subject: [PATCH 1/3] Backbone.Promise Competes with #2489. Specifically, is implements `Backbone.Promise` instead of `Backbone.Deferred`, so it can be easily swapped with any ES6 compatible Promise library. --- backbone.js | 34 +++++++++++++++++++++++++++++----- test/model.js | 23 ++++++++++++++++++----- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/backbone.js b/backbone.js index afd37537c..d8502ff19 100644 --- a/backbone.js +++ b/backbone.js @@ -601,9 +601,9 @@ // `set(attr).save(null, opts)` with validation. Otherwise, check if // the model will be valid when the attributes, if any, are set. if (attrs && !wait) { - if (!this.set(attrs, options)) return false; + if (!this.set(attrs, options)) return Backbone.Promise.reject(this.validationError); } else { - if (!this._validate(attrs, options)) return false; + if (!this._validate(attrs, options)) return Backbone.Promise.reject(this.validationError); } // After a successful server-side save, the client is (optionally) @@ -639,7 +639,7 @@ // Optimistically removes the model from its collection, if it has one. // If `wait: true` is passed, waits for the server to respond before removal. destroy: function(options) { - options = options ? _.clone(options) : {}; + options = _.extend({}, options); var model = this; var success = options.success; var wait = options.wait; @@ -655,9 +655,9 @@ if (!model.isNew()) model.trigger('sync', model, resp, options); }; - var xhr = false; + var xhr; if (this.isNew()) { - _.defer(options.success); + xhr = Backbone.Promise.resolve().then(options.success); } else { wrapError(this, options); xhr = this.sync('delete', this, options); @@ -1408,6 +1408,30 @@ return Backbone.$.ajax.apply(Backbone.$, arguments); }; + // A psuedo Promise implementation used to ensure asynchronous methods + // return thenables. + // Override this if you'd like to use a different ES6 library. + Backbone.Promise = function() { + throw new Error('Backbone does not provide a spec compliant Promise by default.'); + }; + + // A helper method that forces jQuery's first `then` callback to be + // executed asynchronously. + // This is used so we can guarantee our async return values execute + // callbacks async, not async sometimes and sync other times. + var asyncDeferred = function(method) { + return function(value) { + var deferred = Backbone.$.Deferred(); + _.defer(deferred[method], value); + return deferred.promise(); + }; + }; + + _.extend(Backbone.Promise, { + resolve: asyncDeferred('resolve'), + reject: asyncDeferred('reject') + }); + // Backbone.Router // --------------- diff --git a/test/model.js b/test/model.js index faaf61dda..de7197835 100644 --- a/test/model.js +++ b/test/model.js @@ -662,13 +662,19 @@ this.ajaxSettings.success(); }); - test("destroy", 3, function() { + asyncTest("destroy", 3, function() { doc.destroy(); equal(this.syncArgs.method, 'delete'); ok(_.isEqual(this.syncArgs.model, doc)); var newModel = new Backbone.Model; - equal(newModel.destroy(), false); + var promise = newModel.destroy(); + var async = false; + promise.then(function() { + ok(async, 'then chains asynchronously'); + start(); + }); + async = true; }); test("destroy will pass extra options to success callback", 1, function () { @@ -1156,11 +1162,18 @@ }}); }); - test("#1433 - Save: An invalid model cannot be persisted.", 1, function() { + asyncTest("#1433 - Save: An invalid model cannot be persisted.", 2, function() { var model = new Backbone.Model; - model.validate = function(){ return 'invalid'; }; + model.validate = function(){ return { error: 'invalid' }; }; model.sync = function(){ ok(false); }; - strictEqual(model.save(), false); + var promise = model.save(); + var async = false; + promise.then(null, function(reason) { + strictEqual(reason, model.validationError, 'passes error to onRejected'); + ok(async, 'then chains asynchronously'); + start(); + }) + async = true; }); test("#1377 - Save without attrs triggers 'error'.", 1, function() { From 6de1cbbf0c13bc26a07a389ac2c955e0bf7b58bc Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Sun, 31 May 2015 18:11:15 -0400 Subject: [PATCH 2/3] Ensure Backbone.Promise is ES6 "compliant" --- backbone.js | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/backbone.js b/backbone.js index d8502ff19..0d4c1b74e 100644 --- a/backbone.js +++ b/backbone.js @@ -1415,21 +1415,22 @@ throw new Error('Backbone does not provide a spec compliant Promise by default.'); }; - // A helper method that forces jQuery's first `then` callback to be - // executed asynchronously. - // This is used so we can guarantee our async return values execute - // callbacks async, not async sometimes and sync other times. - var asyncDeferred = function(method) { - return function(value) { + _.extend(Backbone.Promise, { + // A wrapper around jQuery's normal resolve to force it to adopt a + // thenable's state, and execute then callbacks asynchronously. + resolve: function(value) { var deferred = Backbone.$.Deferred(); - _.defer(deferred[method], value); - return deferred.promise(); - }; - }; + _.defer(deferred.resolve); + return deferred.promise().then(_.constant(value)); + }, - _.extend(Backbone.Promise, { - resolve: asyncDeferred('resolve'), - reject: asyncDeferred('reject') + // A wrapper around jQuery's normal reject to force it to execute + // then callbacks asynchronously. + reject: function(reason) { + var deferred = Backbone.$.Deferred(); + _.defer(deferred.reject, reason); + return deferred.promise(); + } }); // Backbone.Router From 247de5b957610a2ddf5abf175e48a3632cd04aa4 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Sun, 31 May 2015 18:32:55 -0400 Subject: [PATCH 3/3] Test Backbone.Promise --- test/index.html | 1 + test/promise.js | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 test/promise.js diff --git a/test/index.html b/test/index.html index 3dad8c679..1deab47a3 100644 --- a/test/index.html +++ b/test/index.html @@ -20,5 +20,6 @@ + diff --git a/test/promise.js b/test/promise.js new file mode 100644 index 000000000..e28e0bf29 --- /dev/null +++ b/test/promise.js @@ -0,0 +1,56 @@ +(function() { + + module("Backbone.Promise"); + + test("throws an error if invoked", 1, function() { + try { + Backbone.Promise(); + } catch (e) { + ok(e); + } + }); + + asyncTest(".resolve to passed in value", 1, function() { + var value = {}; + Backbone.Promise.resolve({}).then(function(val) { + strictEqual(val, value); + start(); + }); + }); + + asyncTest(".resolve adopts promise state", 1, function() { + var value = {}; + var promise = Backbone.Promise.resolve(val); + Backbone.Promise.resolve(promise).then(function(val) { + strictEqual(val, value); + start(); + }); + }); + + asyncTest(".resolve executes then callback asynchronously", 1, function() { + var async = false; + Backbone.Promise.resolve().then(function() { + ok(async); + start(); + }); + async = true; + }); + + asyncTest(".reject to passed in value", 1, function() { + var value = {}; + Backbone.Promise.reject({}).then(null, function(val) { + strictEqual(val, value); + start(); + }); + }); + + asyncTest(".reject executes then callback asynchronously", 1, function() { + var async = false; + Backbone.Promise.reject().then(null, function() { + ok(async); + start(); + }); + async = true; + }); + +});