Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' of github-beastridge:walmartlabs/thorax

  • Loading branch information...
commit c5b972d4cbfa345cad55195d6778e36bfef06b10 2 parents d336c09 + 024ff4f
Ryan Eastridge authored
Showing with 129 additions and 68 deletions.
  1. +63 −60 src/loading.js
  2. +65 −7 test/src/loading.js
  3. +1 −1  test/src/thorax.js
View
123 src/loading.js
@@ -1,4 +1,5 @@
/*global collectionOptionNames, inheritVars */
+
var loadStart = 'load:start',
loadEnd = 'load:end',
rootObject;
@@ -8,15 +9,18 @@ Thorax.setRootObject = function(obj) {
};
Thorax.loadHandler = function(start, end, context) {
+ var loadCounter = _.uniqueId();
return function(message, background, object) {
var self = context || this;
+ self._loadInfo = self._loadInfo || [];
+ var loadInfo = self._loadInfo[loadCounter];
function startLoadTimeout() {
- clearTimeout(self._loadStart.timeout);
- self._loadStart.timeout = setTimeout(function() {
+ clearTimeout(loadInfo.timeout);
+ loadInfo.timeout = setTimeout(function() {
try {
- self._loadStart.run = true;
- start.call(self, self._loadStart.message, self._loadStart.background, self._loadStart);
+ loadInfo.run = true;
+ start.call(self, loadInfo.message, loadInfo.background, loadInfo);
} catch (e) {
Thorax.onException('loadStart', e);
}
@@ -24,14 +28,11 @@ Thorax.loadHandler = function(start, end, context) {
loadingTimeout * 1000);
}
- if (!self._loadStart) {
- var loadingTimeout = self._loadingTimeoutDuration;
- if (loadingTimeout === void 0) {
- // If we are running on a non-view object pull the default timeout
- loadingTimeout = Thorax.View.prototype._loadingTimeoutDuration;
- }
+ if (!loadInfo) {
+ var loadingTimeout = self._loadingTimeoutDuration !== undefined ?
+ self._loadingTimeoutDuration : Thorax.View.prototype._loadingTimeoutDuration;
- self._loadStart = _.extend({
+ loadInfo = self._loadInfo[loadCounter] = _.extend({
events: [],
timeout: 0,
message: message,
@@ -39,16 +40,16 @@ Thorax.loadHandler = function(start, end, context) {
}, Backbone.Events);
startLoadTimeout();
} else {
- clearTimeout(self._loadStart.endTimeout);
+ clearTimeout(loadInfo.endTimeout);
- self._loadStart.message = message;
- if (!background && self._loadStart.background) {
- self._loadStart.background = false;
+ loadInfo.message = message;
+ if (!background && loadInfo.background) {
+ loadInfo.background = false;
startLoadTimeout();
}
}
- self._loadStart.events.push(object);
+ loadInfo.events.push(object);
object.on(loadEnd, function endCallback() {
object.off(loadEnd, endCallback);
@@ -58,26 +59,26 @@ Thorax.loadHandler = function(start, end, context) {
loadingEndTimeout = Thorax.View.prototype._loadingTimeoutEndDuration;
}
- var events = self._loadStart.events,
+ var events = loadInfo.events,
index = events.indexOf(object);
if (index >= 0) {
events.splice(index, 1);
}
if (!events.length) {
- self._loadStart.endTimeout = setTimeout(function() {
+ loadInfo.endTimeout = setTimeout(function() {
try {
if (!events.length) {
- var run = self._loadStart.run;
+ var run = loadInfo.run;
if (run) {
// Emit the end behavior, but only if there is a paired start
- end.call(self, self._loadStart.background, self._loadStart);
- self._loadStart.trigger(loadEnd, self._loadStart);
+ end.call(self, loadInfo.background, loadInfo);
+ loadInfo.trigger(loadEnd, loadInfo);
}
// If stopping make sure we don't run a start
- clearTimeout(self._loadStart.timeout);
- self._loadStart = undefined;
+ clearTimeout(loadInfo.timeout);
+ loadInfo = self._loadInfo[loadCounter] = undefined;
}
} catch (e) {
Thorax.onException('loadEnd', e);
@@ -125,7 +126,7 @@ Thorax.mixinLoadable = function(target, useParent) {
// Propagates loading view parameters to the AJAX layer
onLoadStart: function(message, background, object) {
var that = useParent ? this.parent : this;
- if (!that.nonBlockingLoad && !background && rootObject) {
+ if (!that.nonBlockingLoad && !background && rootObject && rootObject !== this) {
rootObject.trigger(loadStart, message, background, object);
}
$(that.el).addClass(that._loadingClassName);
@@ -182,37 +183,33 @@ Thorax.sync = function(method, dataObj, options) {
function bindToRoute(callback, failback) {
var fragment = Backbone.history.getFragment(),
- completed;
+ routeChanged = false;
- function finalizer(isCanceled) {
- var same = fragment === Backbone.history.getFragment();
-
- if (completed) {
- // Prevent multiple execution, i.e. we were canceled but the success callback still runs
- return;
- }
-
- if (isCanceled && same) {
- // Ignore the first route event if we are running in newer versions of backbone
- // where the route operation is a postfix operation.
+ function routeHandler() {
+ if (fragment === Backbone.history.getFragment()) {
return;
}
+ routeChanged = true;
+ res.cancel();
+ failback && failback();
+ }
- completed = true;
- Backbone.history.off('route', resetLoader);
+ Backbone.history.on('route', routeHandler);
+ function finalizer() {
+ Backbone.history.off('route', routeHandler);
var args = Array.prototype.slice.call(arguments, 1);
- if (!isCanceled && same) {
+ if (!routeChanged) {
callback.apply(this, args);
- } else {
- failback && failback.apply(this, args);
}
}
- var resetLoader = _.bind(finalizer, this, true);
- Backbone.history.on('route', resetLoader);
+ var res = _.bind(finalizer, this);
+ res.cancel = function() {
+ Backbone.history.off('route', routeHandler);
+ };
- return _.bind(finalizer, this, false);
+ return res;
}
function loadData(callback, failback, options) {
@@ -225,9 +222,27 @@ function loadData(callback, failback, options) {
failback = false;
}
+ var self = this,
+ routeChanged = false,
+ successCallback = bindToRoute(_.bind(callback, self), function() {
+ routeChanged = true;
+ if (self._request) {
+ self._aborted = true;
+ self._request.abort();
+ }
+ failback && failback.call(self, false);
+ });
+
this.fetch(_.defaults({
- success: bindToRoute(callback, failback && _.bind(failback, this, false)),
- error: failback && _.bind(failback, this, true)
+ success: successCallback,
+ error: failback && function() {
+ if (!routeChanged) {
+ failback.apply(self, [true].concat(_.toArray(arguments)));
+ }
+ },
+ complete: function() {
+ successCallback.cancel();
+ }
}, options));
}
@@ -301,6 +316,7 @@ _.each(klasses, function(DataClass) {
options = failback;
failback = false;
}
+
options = options || {};
if (!options.background && !this.isPopulated() && rootObject) {
// Make sure that the global scope sees the proper load events here
@@ -308,20 +324,7 @@ _.each(klasses, function(DataClass) {
Thorax.forwardLoadEvents(this, rootObject, true);
}
- var self = this;
- loadData.call(this, callback,
- function(isError) {
- // Route changed, kill it
- if (!isError) {
- if (self._request) {
- self._aborted = true;
- self._request.abort();
- }
- }
-
- failback && failback.apply && failback.apply(this, arguments);
- },
- options);
+ loadData.call(this, callback, failback, options);
}
});
});
View
72 test/src/loading.js
@@ -245,7 +245,7 @@ describe('loading', function() {
it('pair with timeout registers', function() {
this.object.loadStart('foo', false);
this.clock.tick(1000);
- var loaderWrapper = this.object._loadStart;
+ var loaderWrapper = this.object._loadInfo[this.object._loadInfo.length - 1];
this.object.loadEnd();
this.clock.tick(1000);
@@ -257,7 +257,7 @@ describe('loading', function() {
it('consequtive pairs emit one event', function() {
this.object.loadStart('foo', false);
this.clock.tick(1000);
- var loaderWrapper = this.object._loadStart;
+ var loaderWrapper = this.object._loadInfo[this.object._loadInfo.length - 1];
this.object.loadEnd();
this.clock.tick(10);
@@ -277,7 +277,7 @@ describe('loading', function() {
it('consequtive pairs emit two events after timeout', function() {
this.object.loadStart('foo', false);
this.clock.tick(1000);
- var loaderWrapper = this.object._loadStart;
+ var loaderWrapper = this.object._loadInfo[this.object._loadInfo.length - 1];
this.object.loadEnd();
this.clock.tick(1000);
@@ -287,7 +287,7 @@ describe('loading', function() {
this.object.loadStart('bar', true);
this.clock.tick(1000);
- var loaderWrapper2 = this.object._loadStart;
+ var loaderWrapper2 = this.object._loadInfo[this.object._loadInfo.length - 1];
this.object.loadEnd();
this.clock.tick(1000);
@@ -299,7 +299,7 @@ describe('loading', function() {
it('overlapping pairs emit one event', function() {
this.object.loadStart('foo', false);
this.clock.tick(1000);
- var loaderWrapper = this.object._loadStart;
+ var loaderWrapper = this.object._loadInfo[this.object._loadInfo.length - 1];
this.object.loadStart('bar', true);
this.clock.tick(1000);
@@ -315,6 +315,47 @@ describe('loading', function() {
expect(this.loads).to.eql([{msg: 'foo', background: false, model: loaderWrapper}]);
expect(this.ends).to.eql([{background: false, model: loaderWrapper}]);
});
+
+ it('loadHandlers are isolated', function() {
+ var startSpy = this.spy(),
+ endSpy = this.spy();
+ this.object.on('load:start', Thorax.loadHandler(startSpy, endSpy));
+ this.object.loadStart('foo', false);
+
+ expect(this.loads.length).to.equal(0);
+ expect(this.ends.length).to.equal(0);
+ expect(startSpy).to.not.have.been.called;
+ expect(endSpy).to.not.have.been.called;
+
+ this.clock.tick(200);
+
+ expect(this.loads.length).to.equal(0);
+ expect(this.ends.length).to.equal(0);
+ expect(startSpy).to.not.have.been.called;
+ expect(endSpy).to.not.have.been.called;
+
+ this.clock.tick(1000);
+
+ expect(this.loads.length).to.equal(1);
+ expect(this.ends.length).to.equal(0);
+ expect(startSpy).to.have.been.calledOnce;
+ expect(endSpy).to.not.have.been.called;
+
+ this.object.loadEnd();
+
+ expect(this.loads.length).to.equal(1);
+ expect(this.ends.length).to.equal(0);
+ expect(startSpy).to.have.been.calledOnce;
+ expect(endSpy).to.not.have.been.called;
+
+ this.clock.tick(1000);
+
+ expect(this.loads.length).to.equal(1);
+ expect(this.ends.length).to.equal(1);
+ expect(startSpy).to.have.been.calledOnce;
+ expect(endSpy).to.have.been.calledOnce;
+
+ });
});
@@ -405,6 +446,20 @@ describe('loading', function() {
expect(this.startSpy).to.have.been.calledOnce;
expect(this.endSpy).to.have.been.calledOnce;
});
+ it('data load on error calls failback once', function() {
+ var success = this.spy(),
+ failback = this.spy();
+
+ this.model.load(success, failback);
+ this.requests[0].respond(0, {}, '');
+
+ Backbone.history.trigger('route');
+ expect(success).to.not.have.been.called;
+ expect(failback).to.have.been.calledOnce;
+ expect(failback).to.have.been.calledWith(true);
+ expect(this.startSpy).to.have.been.calledOnce;
+ expect(this.endSpy).to.have.been.calledOnce;
+ });
it('data load on route change sends load events', function() {
var success = this.spy(),
failback = this.spy();
@@ -415,11 +470,14 @@ describe('loading', function() {
fragment = 'data-foo';
Backbone.history.trigger('route');
+ expect(this.endSpy).to.have.been.calledOnce;
+
+ this.requests[0].respond(200, {}, '{}');
expect(success).to.not.have.been.called;
- expect(failback).to.have.been.calledTwice;
+ expect(failback).to.have.been.calledOnce;
+ expect(failback).to.have.been.calledWith(false);
expect(this.startSpy).to.have.been.calledOnce;
- expect(this.endSpy).to.have.been.calledOnce;
});
it('data load sent for background and foreground requests', function() {
var success = this.spy(),
View
2  test/src/thorax.js
@@ -1,5 +1,5 @@
describe('core', function() {
- Backbone.history = new Backbone.History();
+ Backbone.history || (Backbone.history = new Backbone.History());
Backbone.history.start();
Thorax.templates.parent = '<div>{{view child}}</div>';
Please sign in to comment.
Something went wrong with that request. Please try again.