Skip to content
Browse files

Merge branch 'master' of github.com:walmartlabs/thorax

  • Loading branch information...
2 parents cd29282 + 9624f73 commit c925bcb68b9d5f78c9f16ad2b6e72c5485d97f33 @kpdecker kpdecker committed Jan 7, 2013
View
3 build.json
@@ -24,7 +24,8 @@
"mixins": [
"thorax",
"thorax-helper-tags",
- "thorax-loading"
+ "thorax-loading",
+ "thorax-ie"
]
},
"thorax-mobile": {
View
6 lumbar.json
@@ -39,6 +39,12 @@
]
},
+ "thorax-ie": {
+ "scripts": [
+ {"src": "src/ie.js"}
+ ]
+ },
+
"thorax-helper-tags": {
"scripts": [
{"src": "src/helpers/button-link.js"},
View
68 src/collection.js
@@ -19,6 +19,9 @@ Thorax.Collection = Backbone.Collection.extend({
isPopulated: function() {
return this._fetched || this.length > 0 || (!this.length && !getValue(this, 'url'));
},
+ shouldFetch: function(options) {
+ return options.fetch && !!getValue(this, 'url') && !this.isPopulated();
+ },
fetch: function(options) {
options = options || {};
var success = options.success;
@@ -38,15 +41,15 @@ Thorax.Collections = {};
createRegistryWrapper(Thorax.Collection, Thorax.Collections);
dataObject('collection', {
- name: '_collectionEvents',
- array: '_collections',
- hash: '_collectionOptionsByCid',
set: 'setCollection',
setCallback: afterSetCollection,
- bind: 'bindCollection',
- unbind: 'unbindCollection',
- options: '_setCollectionOptions',
- change: '_onCollectionReset',
+ defaultOptions: {
+ render: true,
+ fetch: true,
+ success: false,
+ errors: true
+ },
+ change: onCollectionReset,
$el: 'getCollectionElement',
cidAttrName: collectionCidAttributeName
});
@@ -117,11 +120,13 @@ _.extend(Thorax.View.prototype, {
if (!viewEl.length) {
return false;
}
- var viewCid = viewEl.attr(viewCidAttributeName);
- if (this.children[viewCid]) {
- delete this.children[viewCid];
- }
viewEl.remove();
+ var viewCid = viewEl.attr(viewCidAttributeName),
+ child = this.children[viewCid];
+ if (child) {
+ this._removeChild(child);
+ child.destroy();
+ }
return true;
},
renderCollection: function() {
@@ -146,25 +151,26 @@ _.extend(Thorax.View.prototype, {
},
emptyClass: 'empty',
renderEmpty: function() {
- var context = this.emptyContext ? this.emptyContext.call(this) : this.context();
if (this.emptyView) {
- var view = Thorax.Util.getViewInstance(this.emptyView, {});
+ var viewOptions = {};
if (this.emptyTemplate) {
- view.render(this.renderTemplate(this.emptyTemplate, context));
- } else {
- view.render();
+ viewOptions.template = this.emptyTemplate;
}
+ var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
+ view.ensureRendered();
return view;
} else {
- return this.emptyTemplate && this.renderTemplate(this.emptyTemplate, context);
+ return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
}
},
renderItem: function(model, i) {
if (this.itemView) {
var viewOptions = {
model: model
};
- this.itemTemplate && (viewOptions.template = this.itemTemplate);
+ if (this.itemTemplate) {
+ viewOptions.template = this.itemTemplate;
+ }
var view = Thorax.Util.getViewInstance(this.itemView, viewOptions);
view.ensureRendered();
return view;
@@ -188,15 +194,10 @@ _.extend(Thorax.View.prototype, {
var element = this.$(this._collectionSelector);
return element.length === 0 ? this.$el : element;
},
- _onCollectionReset: function(collection) {
- if (collection === this.collection && this._collectionOptionsByCid[this.collection.cid].render) {
- this.renderCollection();
- }
- },
// Events that will only be bound to "this.collection"
_collectionRenderingEvents: {
- reset: '_onCollectionReset',
- sort: '_onCollectionReset',
+ reset: onCollectionReset,
+ sort: onCollectionReset,
filter: function() {
applyVisibilityFilter.call(this);
},
@@ -217,14 +218,7 @@ _.extend(Thorax.View.prototype, {
},
remove: function(model) {
var $el = this.getCollectionElement();
- $el.find('[' + modelCidAttributeName + '="' + model.cid + '"]').remove();
- for (var cid in this.children) {
- if (this.children[cid].model && this.children[cid].model.cid === model.cid) {
- this.children[cid].destroy();
- delete this.children[cid];
- break;
- }
- }
+ this.removeItem(model);
this.collection.length === 0 && $el.length && handleChangeFromNotEmptyToEmpty.call(this);
}
}
@@ -233,13 +227,19 @@ _.extend(Thorax.View.prototype, {
Thorax.View.on({
collection: {
error: function(collection, message) {
- if (this._collectionOptionsByCid[collection.cid].errors) {
+ if (this._objectOptionsByCid[collection.cid].errors) {
this.trigger('error', message, collection);
}
}
}
});
+function onCollectionReset(collection) {
+ if (collection === this.collection && this._objectOptionsByCid[this.collection.cid].render) {
+ this.renderCollection();
+ }
+}
+
function afterSetCollection(collection) {
if (!collectionHelperPresentForPrimaryCollection.call(this) && collection) {
_.each(this._collectionRenderingEvents, function(callback, eventName) {
View
185 src/data-object.js
@@ -1,6 +1,10 @@
/*global getValue, inheritVars, walkInheritTree */
+
function dataObject(type, spec) {
- spec = inheritVars[type] = _.defaults({event: true}, spec);
+ spec = inheritVars[type] = _.defaults({
+ name: '_' + type + 'Events',
+ event: true
+ }, spec);
// Add a callback in the view constructor
spec.ctor = function() {
@@ -13,71 +17,6 @@ function dataObject(type, spec) {
}
};
- function bindEvents(target, source) {
- var context = this;
- walkInheritTree(source, spec.name, true, function(event) {
- // getEventCallback will resolve if it is a string or a method
- // and return a method
- context.listenTo(target, event[0], _.bind(getEventCallback(event[1], context), event[2] || context));
- });
- }
-
- function loadObject(dataObject, options) {
- if (dataObject.load) {
- dataObject.load(function() {
- options && options.success && options.success(dataObject);
- }, options);
- } else {
- dataObject.fetch(options);
- }
- }
-
- function bindObject(dataObject, options) {
- if (this[spec.array].indexOf(dataObject) !== -1) {
- return false;
- }
- // Collections do not have a cid attribute by default
- ensureDataObjectCid(type, dataObject);
- this[spec.array].push(dataObject);
-
- var options = this[spec.options](dataObject, options);
-
- bindEvents.call(this, dataObject, this.constructor);
- bindEvents.call(this, dataObject, this);
-
- if (Thorax.Util.shouldFetch(dataObject, options)) {
- loadObject(dataObject, options);
- } else {
- // want to trigger built in rendering without triggering event on model
- this[spec.change](dataObject, options);
- }
- return true;
- }
-
- function unbindObject(dataObject) {
- if (this[spec.array].indexOf(dataObject) === -1) {
- return false;
- }
- this[spec.array] = _.without(this[spec.array], dataObject);
- dataObject.trigger('freeze');
- this.stopListening(dataObject);
- delete this[spec.hash][dataObject.cid];
- return true;
- }
-
- function objectOptions(dataObject, options) {
- if (!this[spec.hash][dataObject.cid]) {
- this[spec.hash][dataObject.cid] = {
- render: true,
- fetch: true,
- success: false,
- errors: true
- };
- }
- _.extend(this[spec.hash][dataObject.cid], options || {});
- return this[spec.hash][dataObject.cid];
- }
-
function setObject(dataObject, options) {
var old = this[type],
$el = getValue(this, spec.$el);
@@ -86,7 +25,7 @@ function dataObject(type, spec) {
return this;
}
if (old) {
- this[spec.unbind](old);
+ this.unbindDataObject(old);
}
if (dataObject) {
@@ -96,27 +35,102 @@ function dataObject(type, spec) {
spec.loading.call(this);
}
- this[spec.bind](dataObject, _.extend({}, this.options, options));
+ this.bindDataObject(dataObject, _.extend({}, this.options, options));
$el.attr(spec.cidAttrName, dataObject.cid);
dataObject.trigger('set', dataObject, old);
} else {
this[type] = false;
if (spec.change) {
- this[spec.change](false);
+ spec.change.call(this, false);
}
$el.removeAttr(spec.cidAttrName);
}
spec.setCallback && spec.setCallback.call(this, dataObject, options);
return this;
}
- var extend = {};
- extend[spec.bind] = bindObject;
- extend[spec.unbind] = unbindObject;
- extend[spec.set] = setObject;
- extend[spec.options] = objectOptions;
+ Thorax.View.prototype[spec.set] = setObject;
+}
- _.extend(Thorax.View.prototype, extend);
+_.extend(Thorax.View.prototype, {
+ bindDataObject: function(dataObject, options) {
+ var type = getDataObjectType(dataObject);
+ if (this._boundDataObjectsByCid[dataObject.cid]) {
+ return false;
+ }
+ // Collections do not have a cid attribute by default
+ ensureDataObjectCid(type, dataObject);
+ this._boundDataObjectsByCid[dataObject.cid] = dataObject;
+
+ var options = this._modifyDataObjectOptions(dataObject, _.extend({}, inheritVars[type].defaultOptions, options));
+ this._objectOptionsByCid[dataObject.cid] = options;
+
+ bindEvents.call(this, type, dataObject, this.constructor);
+ bindEvents.call(this, type, dataObject, this);
+
+ if (dataObject.shouldFetch) {
+ if (dataObject.shouldFetch(options)) {
+ loadObject(dataObject, options);
+ } else if (inheritVars[type].change) {
+ // want to trigger built in rendering without triggering event on model
+ inheritVars[type].change.call(this, dataObject, options);
+ }
+ }
+
+ return true;
+ },
+
+ unbindDataObject: function (dataObject) {
+ if (!this._boundDataObjectsByCid[dataObject.cid]) {
+ return false;
+ }
+ delete this._boundDataObjectsByCid[dataObject.cid];
+ dataObject.trigger('freeze');
+ this.stopListening(dataObject);
+ delete this._objectOptionsByCid[dataObject.cid];
+ return true;
+ },
+
+ _modifyDataObjectOptions: function(dataObject, options) {
+ return options;
+ }
+});
+
+function getDataObjectType(dataObject) {
+ if (isModel(dataObject)) {
+ return 'model';
+ } else if (isCollection(dataObject)) {
+ return 'collection';
+ } else {
+ throw new Error('Unknown data object bound: ' + (typeof dataObject));
+ }
+}
+
+function isModel(model) {
+ return model && model.attributes && model.set;
+}
+
+function isCollection(collection) {
+ return collection && collection.indexOf && collection.models;
+}
+
+function bindEvents(type, target, source) {
+ var context = this;
+ walkInheritTree(source, '_' + type + 'Events', true, function(event) {
+ // getEventCallback will resolve if it is a string or a method
+ // and return a method
+ context.listenTo(target, event[0], _.bind(getEventCallback(event[1], context), event[2] || context));
+ });
+}
+
+function loadObject(dataObject, options) {
+ if (dataObject.load) {
+ dataObject.load(function() {
+ options && options.success && options.success(dataObject);
+ }, options);
+ } else {
+ dataObject.fetch(options);
+ }
}
function getEventCallback(callback, context) {
@@ -130,24 +144,3 @@ function getEventCallback(callback, context) {
function ensureDataObjectCid(type, obj) {
obj.cid = obj.cid || _.uniqueId(type);
}
-
-Thorax.Util.shouldFetch = function(modelOrCollection, options) {
- if (!options.fetch) {
- return;
- }
-
- var isCollection = !modelOrCollection.collection && modelOrCollection._byCid && modelOrCollection._byId,
- url = (
- (!modelOrCollection.collection && getValue(modelOrCollection, 'urlRoot')) ||
- (modelOrCollection.collection && getValue(modelOrCollection.collection, 'url')) ||
- (isCollection && getValue(modelOrCollection, 'url'))
- );
-
- return url && !(
- (modelOrCollection.isPopulated && modelOrCollection.isPopulated()) ||
- (isCollection
- ? Thorax.Collection && Thorax.Collection.prototype.isPopulated.call(modelOrCollection)
- : Thorax.Model.prototype.isPopulated.call(modelOrCollection)
- )
- );
-};
View
8 src/event.js
@@ -46,11 +46,7 @@ _.extend(Thorax.View, {
_.extend(Thorax.View.prototype, {
freeze: function(options) {
- _.each(inheritVars, function(obj) {
- if (obj.unbind) {
- _.each(this[obj.array], this[obj.unbind], this);
- }
- }, this);
+ _.each(this._boundDataObjectsByCid, this.unbindDataObject, this);
options = _.defaults(options || {}, {
dom: true,
children: true
@@ -154,7 +150,7 @@ var domEvents = [],
domEventRegexp;
function pushDomEvents(events) {
domEvents.push.apply(domEvents, events);
- domEventRegexp = new RegExp('^(' + domEvents.join('|') + ')(?:\\s|$)');
+ domEventRegexp = new RegExp('^(nested\\s+)?(' + domEvents.join('|') + ')(?:\\s|$)');
}
pushDomEvents([
'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout',
View
19 src/form.js
@@ -1,18 +1,17 @@
-/*global extendOptions, extendViewMember */
+/*global inheritVars */
-extendOptions('_setModelOptions', function() {
- return {
- populate: true
- };
-});
+inheritVars.model.defaultOptions.populate = true;
-extendViewMember('_onModelChange', function(model) {
+var oldModelChange = inheritVars.model.change;
+inheritVars.model.change = function() {
+ oldModelChange.apply(this, arguments);
// TODO : What can we do to remove this duplication?
- var modelOptions = model && this._modelOptionsByCid[model.cid];
+ var modelOptions = this.model && this._objectOptionsByCid[this.model.cid];
if (modelOptions && modelOptions.populate) {
- this.populate(model.attributes, modelOptions.populate === true ? {} : modelOptions.populate);
+ this.populate(this.model.attributes, modelOptions.populate === true ? {} : modelOptions.populate);
}
-});
+};
+inheritVars.model.defaultOptions.populate = true;
_.extend(Thorax.View.prototype, {
//serializes a form present in the view, returning the serialized data
View
25 src/helpers/collection.js
@@ -7,7 +7,13 @@ Thorax.CollectionHelperView = Thorax.View.extend({
},
constructor: function(options) {
_.each(collectionOptionNames, function(viewAttributeName, helperOptionName) {
- options.options[helperOptionName] && (options[viewAttributeName] = options.options[helperOptionName]);
+ if (options.options[helperOptionName]) {
+ var value = options.options[helperOptionName];
+ if (viewAttributeName === 'itemTemplate' || viewAttributeName === 'emptyTemplate') {
+ value = Thorax.Util.getTemplate(value);
+ }
+ options[viewAttributeName] = value;
+ }
});
// Handlebars.VM.noop is passed in the handlebars options object as
// a default for fn and inverse, if a block was present. Need to
@@ -23,21 +29,24 @@ Thorax.CollectionHelperView = Thorax.View.extend({
!options.template && (options.template = Handlebars.VM.noop);
var response = Thorax.CollectionHelperView.__super__.constructor.call(this, options);
if (this.parent.name) {
- this.emptyTemplate = this.emptyTemplate || Thorax.Util.getTemplate(this.parent.name + '-empty', true);
- this.itemTemplate = this.itemTemplate || Thorax.Util.getTemplate(this.parent.name + '-item', true);
+ if (!this.emptyTemplate) {
+ this.emptyTemplate = Thorax.Util.getTemplate(this.parent.name + '-empty', true);
+ }
+ if (!this.itemTemplate) {
+ this.itemTemplate = Thorax.Util.getTemplate(this.parent.name + '-item', true);
+ }
}
return response;
},
+ // will be used by emptyView and emptyTemplate
+ _getContext: function(attributes) {
+ return _.extend({}, getValue(this.parent, 'context'), attributes || {});
+ },
setAsPrimaryCollectionHelper: function(collection) {
this.$el.attr(primaryCollectionAttributeName, collection.cid);
_.each(forwardableProperties, function(propertyName) {
forwardMissingProperty.call(this, propertyName);
}, this);
- // emptyContext needs to be forced because it has a default
- forwardMissingProperty.call(this, 'emptyContext', true);
- },
- emptyContext: function() {
- return getValue(this.parent, 'context');
}
});
View
7 src/helpers/element.js
@@ -12,13 +12,14 @@ Handlebars.registerHelper('element', function(element, options) {
Thorax.View.on('append', function(scope, callback) {
(scope || this.$el).find('[' + elementPlaceholderAttributeName + ']').forEach(function(el) {
- var cid = el.getAttribute(elementPlaceholderAttributeName),
+ var $el = $(el),
+ cid = $el.attr(elementPlaceholderAttributeName),
element = this._elementsByCid[cid];
- // A callback function may be specified as the vaue
+ // A callback function may be specified as the value
if (_.isFunction(element)) {
element = element.call(this);
}
- $(el).replaceWith(element);
+ $el.replaceWith(element);
callback && callback(element);
}, this);
});
View
2 src/helpers/template.js
@@ -1,6 +1,6 @@
Handlebars.registerHelper('template', function(name, options) {
var context = _.extend({fn: options && options.fn}, this, options ? options.hash : {});
- var output = Thorax.View.prototype.renderTemplate.call(getOptionsData(options).view, name, context);
+ var output = getOptionsData(options).view.renderTemplate(name, context);
return new Handlebars.SafeString(output);
});
View
18 src/ie.js
@@ -0,0 +1,18 @@
+var isIE = (/msie [\w.]+/).exec(navigator.userAgent.toLowerCase());
+
+// IE will lose a reference to the elements if view.el.innerHTML = '';
+// If they are removed one by one the references are not lost.
+// For instance a view's childrens' `el`s will be lost if the view
+// sets it's `el.innerHTML`.
+if (isIE) {
+ Thorax.View.on('before:append', function() {
+ if (this._renderCount > 0) {
+ _.each(this._elementsByCid, function(element, cid) {
+ $(element).remove();
+ });
+ _.each(this.children, function(child, cid) {
+ child.$el.remove();
+ });
+ }
+ });
+}
View
25 src/layout.js
@@ -75,20 +75,27 @@ Thorax.LayoutView = Thorax.View.extend({
if (options.destroy && view) {
view._shouldDestroyOnNextSetView = true;
}
+
this.trigger('change:view:start', view, oldView, options);
- oldView && oldView.trigger('deactivated', options);
- view && view.trigger('activated', options);
- if (oldView && oldView.el && oldView.el.parentNode) {
+
+ if (oldView) {
+ this._removeChild(oldView);
oldView.$el.remove();
+ oldView.trigger('deactivated', options);
+ if (oldView._shouldDestroyOnNextSetView) {
+ oldView.destroy();
+ }
}
- //make sure the view has been rendered at least once
- view && this._addChild(view);
- this._view = view || undefined;
- oldView && (delete this.children[oldView.cid]);
- oldView && oldView._shouldDestroyOnNextSetView && oldView.destroy();
- if (this._view) {
+
+ if (view) {
+ view.trigger('activated', options);
+ this._addChild(view);
+ this._view = view;
this._view.appendTo(getLayoutViewsTargetElement.call(this));
+ } else {
+ this._view = undefined;
}
+
this.trigger('change:view:end', view, oldView, options);
return view;
},
View
142 src/loading.js
@@ -1,4 +1,5 @@
-/*global collectionOptionNames, extendOptions, inheritVars */
+/*global collectionOptionNames, inheritVars */
+
var loadStart = 'load:start',
loadEnd = 'load:end',
rootObject;
@@ -8,47 +9,47 @@ 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);
}
},
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,
background: !!background
}, 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;
-
- 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;
- }
+ routeChanged = false;
- 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,27 +316,15 @@ _.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
// if we are loading in standalone mode
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);
}
});
});
@@ -333,14 +336,11 @@ if (Thorax.Router) {
}
// Propagates loading view parameters to the AJAX layer
-function loadingDataOptions() {
- return {
- ignoreErrors: this.ignoreFetchError,
- background: this.nonBlockingLoad
- };
-}
-extendOptions('_setModelOptions', loadingDataOptions);
-extendOptions('_setCollectionOptions', loadingDataOptions);
+Thorax.View.prototype._modifyDataObjectOptions = function(dataObject, options) {
+ options.ignoreErrors = this.ignoreFetchError;
+ options.background = this.nonBlockingLoad;
+ return options;
+};
inheritVars.collection.loading = function() {
var loadingView = this.loadingView,
@@ -365,9 +365,7 @@ inheritVars.collection.loading = function() {
}
item = instance;
} else {
- item = this.renderTemplate(loadingTemplate, {
- collection: this.collection
- });
+ item = this.renderTemplate(loadingTemplate);
}
var index = loadingPlacement
? loadingPlacement.call(this)
View
43 src/model.js
@@ -17,44 +17,53 @@ Thorax.Model = Backbone.Model.extend({
}
var keys = _.keys(attributes);
return keys.length > 1 || (keys.length === 1 && keys[0] !== this.idAttribute);
+ },
+ shouldFetch: function(options) {
+ // url() will throw if model has no `urlRoot` and no `collection`
+ // or has `collection` and `collection` has no `url`
+ var url;
+ try {
+ url = this.url();
+ } catch(e) {
+ url = false;
+ }
+ return options.fetch && !!url && !this.isPopulated();
}
});
Thorax.Models = {};
createRegistryWrapper(Thorax.Model, Thorax.Models);
dataObject('model', {
- name: '_modelEvents',
- array: '_models',
- hash: '_modelOptionsByCid',
set: 'setModel',
- bind: 'bindModel',
- unbind: 'unbindModel',
- options: '_setModelOptions',
- change: '_onModelChange',
+ defaultOptions: {
+ render: true,
+ fetch: true,
+ success: false,
+ errors: true
+ },
+ change: onModelChange,
$el: '$el',
cidAttrName: modelCidAttributeName
});
-_.extend(Thorax.View.prototype, {
- _onModelChange: function(model) {
- var modelOptions = model && this._modelOptionsByCid[model.cid];
- // !modelOptions will be true when setModel(false) is called
- if (!modelOptions || (modelOptions && modelOptions.render)) {
- this.render();
- }
+function onModelChange(model) {
+ var modelOptions = model && this._objectOptionsByCid[model.cid];
+ // !modelOptions will be true when setModel(false) is called
+ if (!modelOptions || (modelOptions && modelOptions.render)) {
+ this.render();
}
-});
+}
Thorax.View.on({
model: {
error: function(model, errors) {
- if (this._modelOptionsByCid[model.cid].errors) {
+ if (this._objectOptionsByCid[model.cid].errors) {
this.trigger('error', errors, model);
}
},
change: function(model) {
- this._onModelChange(model);
+ onModelChange.call(this, model);
}
}
});
View
58 src/thorax.js
@@ -45,11 +45,12 @@ Thorax.View = Backbone.View.extend({
_configure: function(options) {
var self = this;
+ this._objectOptionsByCid = {};
+ this._boundDataObjectsByCid = {};
+
// Setup object event tracking
_.each(inheritVars, function(obj) {
self[obj.name] = [];
- if (obj.array) { self[obj.array] = []; }
- if (obj.hash) { self[obj.hash] = {}; }
});
viewsIndexedByCid[this.cid] = this;
@@ -60,6 +61,9 @@ Thorax.View = Backbone.View.extend({
//properties directly with the view and template context
_.extend(this, options || {});
+ // Setup helpers
+ bindHelpers.call(this);
+
//compile a string if it is set as this.template
if (typeof this.template === 'string') {
this.template = Handlebars.compile(this.template, {data: true});
@@ -91,20 +95,24 @@ Thorax.View = Backbone.View.extend({
return view;
},
+ _removeChild: function(view) {
+ delete this.children[view.cid];
+ view.parent = null;
+ return view;
+ },
+
destroy: function(options) {
options = _.defaults(options || {}, {
children: true
});
this.trigger('destroyed');
delete viewsIndexedByCid[this.cid];
- if (options.children) {
- _.each(this.children, function(child) {
- child.parent = null;
+ _.each(this.children, function(child) {
+ this._removeChild(child);
+ if (options.children) {
child.destroy();
- });
- this.children = {};
- }
-
+ }
+ }, this);
this.freeze && this.freeze();
},
@@ -154,6 +162,15 @@ Thorax.View = Backbone.View.extend({
};
},
+ _getHelpers: function() {
+ if (this.helpers) {
+ return _.extend({}, Handlebars.helpers, this.helpers);
+ } else {
+ return Handlebars.helpers;
+ }
+
+ },
+
renderTemplate: function(file, data, ignoreErrors) {
var template;
data = this._getContext(data);
@@ -169,7 +186,10 @@ Thorax.View = Backbone.View.extend({
throw new Error('Unable to find template ' + file);
}
} else {
- return template(data, {data: this._getData(data)});
+ return template(data, {
+ helpers: this._getHelpers(),
+ data: this._getData(data)
+ });
}
},
@@ -187,9 +207,11 @@ Thorax.View = Backbone.View.extend({
if (typeof html === 'undefined') {
return this.el.innerHTML;
} else {
+ // Event for IE element fixes
+ this.trigger('before:append');
this.el.innerHTML = "";
var element;
- if (this.collection && this._collectionOptionsByCid[this.collection.cid] && this._renderCount) {
+ if (this.collection && this._objectOptionsByCid[this.collection.cid] && this._renderCount) {
// preserveCollectionElement calls the callback after it has a reference
// to the collection element, calls the callback, then re-appends the element
preserveCollectionElement.call(this, function() {
@@ -230,6 +252,20 @@ Thorax.View.extend = function() {
createRegistryWrapper(Thorax.View, Thorax.Views);
+function bindHelpers() {
+ if (this.helpers) {
+ _.each(this.helpers, function(helper, name) {
+ var view = this;
+ this.helpers[name] = function() {
+ var args = _.toArray(arguments),
+ options = _.last(args);
+ options.context = this;
+ return helper.apply(view, args);
+ };
+ }, this);
+ }
+}
+
//$(selector).view() helper
$.fn.view = function(options) {
options = _.defaults(options || {}, {
View
23 src/util.js
@@ -27,12 +27,15 @@ function registryGet(object, type, name, ignoreErrors) {
}
}
+// getValue is used instead of _.result because we
+// need an extra scope parameter, and will minify
+// better than _.result
function getValue(object, prop, scope) {
if (!(object && object[prop])) {
return null;
}
return _.isFunction(object[prop])
- ? object[prop].apply(scope || object, Array.prototype.slice.call(arguments, 2))
+ ? object[prop].call(scope || object)
: object[prop];
}
@@ -83,7 +86,7 @@ function objectEvents(target, eventName, callback, context) {
if (_.isObject(callback)) {
var spec = inheritVars[eventName];
if (spec && spec.event) {
- addEvents(target[spec.name], callback, context);
+ addEvents(target['_' + eventName + 'Events'], callback, context);
return true;
}
}
@@ -100,21 +103,6 @@ function addEvents(target, source, context) {
});
}
-function extendViewMember(name, callback) {
- var $super = Thorax.View.prototype[name];
- Thorax.View.prototype[name] = function() {
- var ret = $super.apply(this, arguments);
- callback.apply(this, arguments);
- return ret;
- };
-}
-function extendOptions(name, callback) {
- var $super = Thorax.View.prototype[name];
- Thorax.View.prototype[name] = function(dataObject, options) {
- return $super.call(this, dataObject, _.extend(callback.call(this, dataObject, options), options));
- };
-}
-
function getOptionsData(options) {
if (!options || !options.data) {
throw new Error('Handlebars template compiled without data, use: Handlebars.compile(template, {data: true})');
@@ -124,6 +112,7 @@ function getOptionsData(options) {
Thorax.Util = {
getViewInstance: function(name, attributes) {
+ attributes = attributes || {};
attributes['class'] && (attributes.className = attributes['class']);
attributes.tag && (attributes.tagName = attributes.tag);
if (typeof name === 'string') {
View
32 test/src/collection.js
@@ -15,8 +15,8 @@ describe('collection', function() {
Thorax.View.extend({name: 'letter-empty'});
it("should implement isPopulated", function() {
- expect(letterCollection.isPopulated()).to.be.true;
- expect(letterCollection.at(0).isPopulated()).to.be.true;
+ expect(letterCollection.isPopulated()).to.be['true'];
+ expect(letterCollection.at(0).isPopulated()).to.be['true'];
});
it("collection view binding", function() {
@@ -76,6 +76,7 @@ describe('collection', function() {
clonedLetterCollection.remove(clonedLetterCollection.models);
expect(view.$('li')[0].innerHTML).to.equal('empty', msg + 'empty collection renders empty');
clonedLetterCollection.add(new LetterModel({letter: 'a'}));
+
expect(view.$('li').length).to.equal(1 * indexMultiplier, msg + 'transition from empty to one item');
expect(view.$('li')[0 * indexMultiplier].innerHTML).to.equal('a', msg + 'transition from empty to one item');
expect(renderedCount).to.equal(1, msg + 'rendered event count');
@@ -93,7 +94,7 @@ describe('collection', function() {
clonedLetterCollection.remove(clonedLetterCollection.models);
expect(renderedEmptyCount).to.equal(1, msg + 'rendered:empty event count');
- expect(view.$('li')[0 * indexMultiplier].innerHTML).to.equal('a', msg + 'transition from empty to one item');
+ expect(view.$('li')[0 * indexMultiplier].innerHTML).to.equal('a', msg + 'transition from empty to one item after freeze');
}
runCollectionTests(new LetterCollectionView(), 1, 'base');
@@ -149,6 +150,11 @@ describe('collection', function() {
});
runCollectionTests(viewWithCollectionHelperWithEmptyView, 1, 'block helper with item-template');
+ var viewWithCollectionHelperWithItemViewAndItemTemplate = new Thorax.View({
+ template: '{{collection tag="ul" empty-view="letter-empty" item-view="letter-item" item-template="letter-item"}}'
+ });
+ runCollectionTests(viewWithCollectionHelperWithItemViewAndItemTemplate, 1, 'block helper with item-template');
+
var viewWithCollectionHelperWithEmptyViewAndBlock = new Thorax.View({
template: '{{collection tag="ul" empty-template="letter-empty" empty-view="letter-empty" item-template="letter-item"}}'
});
@@ -256,7 +262,7 @@ describe('collection', function() {
expect(view.$('li').eq(0).html()).to.equal('d');
});
- it("_bindCollection or model.set can be called in context()", function() {
+ it("bindDataObject or model.set can be called in context()", function() {
//this causes recursion
var view = new Thorax.View({
model: new Thorax.Model(),
@@ -581,11 +587,7 @@ describe('collection', function() {
}
}))(),
template: "{{#collection this.collection}}{{test}}{{else}}<b>{{test}}</b>{{/collection}}",
- emptyContext: function() {
- return {
- test: 'testing'
- };
- }
+ test: 'testing'
});
view.render();
expect(view.$('b')[0].innerHTML).to.equal('testing');
@@ -597,25 +599,25 @@ describe('collection', function() {
collection: new (Thorax.Collection.extend({url: false}))()
});
view.render();
- expect(view.$('ul').hasClass('a')).to.be.true;
+ expect(view.$('ul').hasClass('a')).to.be['true'];
var model = new Thorax.Model({key: 'value'});
view.collection.add(model);
- expect(view.$('ul').hasClass('a')).to.be.false;
+ expect(view.$('ul').hasClass('a')).to.be['false'];
view.collection.remove(model);
- expect(view.$('ul').hasClass('a')).to.be.true;
+ expect(view.$('ul').hasClass('a')).to.be['true'];
//with default arg
view = new Thorax.View({
template: "{{#collection tag=\"ul\"}}{{/collection}}",
collection: new (Thorax.Collection.extend({url: false}))()
});
view.render();
- expect(view.$('ul').hasClass('empty')).to.be.true;
+ expect(view.$('ul').hasClass('empty')).to.be['true'];
var model = new Thorax.Model({key: 'value'});
view.collection.add(model);
- expect(view.$('ul').hasClass('empty')).to.be.false;
+ expect(view.$('ul').hasClass('empty')).to.be['false'];
view.collection.remove(model);
- expect(view.$('ul').hasClass('empty')).to.be.true;
+ expect(view.$('ul').hasClass('empty')).to.be['true'];
});
it("helper and local scope collision", function() {
View
6 test/src/event.js
@@ -157,7 +157,7 @@ describe('event', function() {
expect(e).to.equal(2);
});
- it("unbindModel / unbindCollection stops events from being triggered", function() {
+ it("unbindDataObject stops events from being triggered", function() {
var spy = this.spy();
var view = new Thorax.View({
events: {
@@ -167,11 +167,11 @@ describe('event', function() {
}
});
view.myModel = new Thorax.Model({key: 'value'});
- view.bindModel(view.myModel, {render: false});
+ view.bindDataObject(view.myModel, {render: false});
expect(spy.callCount).to.equal(0);
view.myModel.trigger('test');
expect(spy.callCount).to.equal(1);
- view.unbindModel(view.myModel);
+ view.unbindDataObject(view.myModel);
view.myModel.trigger('test');
expect(spy.callCount).to.equal(1);
});
View
2 test/src/form.js
@@ -64,7 +64,7 @@ describe('form', function() {
template: '<input name="parentKey">{{view child}}' + mockViewHelperFragment
});
view.render();
- var model = new Backbone.Model({
+ var model = new Thorax.Model({
parentKey: 'parentValue',
childKey: 'childValue'
});
View
2 test/src/helpers/view.js
@@ -57,7 +57,7 @@ describe('view helper', function() {
this.on('rendered', function() {
++parentRenderedCount;
});
- this.childModel = new Backbone.Model({
+ this.childModel = new Thorax.Model({
value: 'a'
});
this.child = new Thorax.Views.child({
View
2 test/src/layout.js
@@ -120,7 +120,7 @@ describe('layout', function() {
layoutWithTemplateWithoutLayoutTag.setView(new Thorax.View({
template: '<div class="inner"></div>'
}));
- }).to.throw();
+ }).to['throw']();
});
});
View
100 test/src/loading.js
@@ -16,12 +16,12 @@ describe('loading', function() {
view = new Thorax.View({name: 'food', render: function() {}, model: model});
view.on('load:start', spy);
- expect($(view.el).hasClass('loading')).to.be.false;
+ expect($(view.el).hasClass('loading')).to.be['false'];
model.loadStart();
this.clock.tick(1000);
expect(spy).to.have.been.calledOnce;
- expect($(view.el).hasClass('loading')).to.be.true;
+ expect($(view.el).hasClass('loading')).to.be['true'];
});
it('views should see load start from collection', function() {
var spy = this.spy(),
@@ -31,31 +31,31 @@ describe('loading', function() {
myCollection: collection,
template: function() {}
});
- view.bindCollection(view.myCollection);
+ view.bindDataObject(view.myCollection);
view.on('load:start', spy);
view.render();
- expect($(view.el).hasClass('loading')).to.be.false;
+ expect($(view.el).hasClass('loading')).to.be['false'];
collection.loadStart();
this.clock.tick(1000);
expect(spy).to.have.been.calledOnce;
- expect($(view.el).hasClass('loading')).to.be.true;
+ expect($(view.el).hasClass('loading')).to.be['true'];
});
it('views should not see load start after destroy', function() {
var spy = this.spy(),
model = new Thorax.Model({url: 'foo'}),
view = new Thorax.View({name: 'food', render: function() {}, model: model});
view.on('load:start', spy);
- expect($(view.el).hasClass('loading')).to.be.false;
+ expect($(view.el).hasClass('loading')).to.be['false'];
view.destroy();
model.loadStart();
this.clock.tick(1000);
expect(spy).to.not.have.been.called;
- expect($(view.el).hasClass('loading')).to.be.false;
+ expect($(view.el).hasClass('loading')).to.be['false'];
});
it('views should see load end from model', function() {
@@ -70,7 +70,7 @@ describe('loading', function() {
this.clock.tick(1000);
expect(spy).to.have.been.calledOnce;
- expect($(view.el).hasClass('loading')).to.be.false;
+ expect($(view.el).hasClass('loading')).to.be['false'];
});
it('views should see load end from collection', function() {
var collection = new Thorax.Collection({url: 'foo'});
@@ -81,15 +81,15 @@ describe('loading', function() {
itemTemplate: function() {return ''; }
});
var spy = this.spy(view, 'onLoadEnd');
- view.bindCollection(view.collection);
+ view.bindDataObject(view.collection);
collection.loadStart();
this.clock.tick(1000);
collection.loadEnd();
this.clock.tick(1000);
expect(spy).to.have.been.calledOnce;
- expect($(view.el).hasClass('loading')).to.be.false;
+ expect($(view.el).hasClass('loading')).to.be['false'];
});
it('views should see load end after destroy', function() {
var spy = this.spy(),
@@ -98,11 +98,11 @@ describe('loading', function() {
endSpy = this.spy(view, 'onLoadEnd');
view.on('load:start', spy);
- expect($(view.el).hasClass('loading')).to.be.false;
+ expect($(view.el).hasClass('loading')).to.be['false'];
model.loadStart();
this.clock.tick(1000);
- expect($(view.el).hasClass('loading')).to.be.true;
+ expect($(view.el).hasClass('loading')).to.be['true'];
view.destroy();
model.loadEnd();
@@ -111,7 +111,7 @@ describe('loading', function() {
this.clock.tick(1000);
expect(spy).to.have.been.calledOnce;
expect(endSpy).to.have.been.calledOnce;
- expect($(view.el).hasClass('loading')).to.be.false;
+ expect($(view.el).hasClass('loading')).to.be['false'];
});
});
@@ -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(),
@@ -639,7 +697,7 @@ describe('loading', function() {
expect(collectionLoadingTemplateView.$('li').length).to.equal(3);
expect(collectionLoadingTemplateView.$('li.empty-item').length).to.equal(0);
expect(collectionLoadingTemplateView.$('li.loading-item').length).to.equal(1);
- expect($(collectionLoadingTemplateView.$('li')[2]).hasClass('loading-item')).to.be.true;
+ expect($(collectionLoadingTemplateView.$('li')[2]).hasClass('loading-item')).to.be['true'];
collectionLoadingTemplateView.myCollection.add([{"number": "three"}, {"number": "four"}]);
collectionLoadingTemplateView.myCollection.loadEnd();
this.clock.tick(loadEndTimeout);
View
33 test/src/model.js
@@ -1,23 +1,24 @@
describe('model', function() {
it("shouldFetch", function() {
- [Thorax, Backbone].forEach(function(type) {
- var options = {fetch: true};
- var a = new (type.Model.extend())();
- expect(Thorax.Util.shouldFetch(a, options)).to.not.be.ok;
+ var options = {fetch: true};
+ var a = new (Thorax.Model.extend())();
+ expect(a.shouldFetch(options)).to.not.be.ok;
- var b = new (type.Model.extend({urlRoot: '/'}))();
- expect(Thorax.Util.shouldFetch(b, options)).to.be.true;
+ var b = new (Thorax.Model.extend({urlRoot: '/'}))();
+ expect(b.shouldFetch(options)).to.be['true'];
- var c = new (type.Model.extend({urlRoot: '/'}))();
- c.set({key: 'value'});
- expect(Thorax.Util.shouldFetch(c, options)).to.not.be.ok;
+ var c = new (Thorax.Model.extend({urlRoot: '/'}))();
+ c.set({key: 'value'});
+ expect(c.shouldFetch(options)).to.not.be.ok;
- var d = new (type.Collection.extend())();
- expect(Thorax.Util.shouldFetch(d, options)).to.not.be.ok;
+ var d = new (Thorax.Collection.extend())();
+ expect(d.shouldFetch(options)).to.not.be.ok;
- var e = new (type.Collection.extend({url: '/'}))();
- expect(Thorax.Util.shouldFetch(e, options)).to.be.true;
- });
+ var e = new (Thorax.Collection.extend({url: '/'}))();
+ expect(e.shouldFetch(options)).to.be['true'];
+
+ var f = new (Thorax.Collection.extend({url: '/'}))();
+ expect(e.shouldFetch({fetch: false})).to.be['false'];
});
it("model view binding", function() {
@@ -53,8 +54,8 @@ describe('model', function() {
});
it("isPopulated", function() {
- expect((new Thorax.Model()).isPopulated()).to.be.false;
- expect((new Thorax.Model({key: 'value'})).isPopulated()).to.be.true;
+ expect((new Thorax.Model()).isPopulated()).to.be['false'];
+ expect((new Thorax.Model({key: 'value'})).isPopulated()).to.be['true'];
});
it("$.fn.model", function() {
View
76 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>';
@@ -89,22 +89,80 @@ describe('core', function() {
expect(view.$('p > span').html()).to.equal('content');
});
- it("local view functions are called in template scope", function() {
- var child = new Thorax.View({
- template: '{{key}}',
- key: function() {
- return 'value';
+ it("element helper", function() {
+ var a = document.createElement('li');
+ a.innerHTML = 'one';
+ var view = new Thorax.View({
+ template: '<ul>{{element a tag="li"}}{{element b tag="li"}}{{element c}}{{element d}}</ul>',
+ a: a,
+ b: function() {
+ var li = document.createElement('li');
+ li.innerHTML = 'two';
+ return li;
+ },
+ c: function() {
+ return $('<li>three</li><li>four</li>');
+ },
+ d: $('<li>five</li>')
+ });
+ view.render();
+ expect(view.$('li')[0].innerHTML).to.equal('one');
+ expect(view.$('li')[1].innerHTML).to.equal('two');
+ expect(view.$('li')[2].innerHTML).to.equal('three');
+ expect(view.$('li')[3].innerHTML).to.equal('four');
+ expect(view.$('li')[4].innerHTML).to.equal('five');
+ view.html('');
+ expect(view.$('li').length).to.equal(0);
+ view.render();
+ expect(view.$('li')[0].innerHTML).to.equal('one');
+ expect(view.$('li')[1].innerHTML).to.equal('two');
+ expect(view.$('li')[2].innerHTML).to.equal('three');
+ expect(view.$('li')[3].innerHTML).to.equal('four');
+ expect(view.$('li')[4].innerHTML).to.equal('five');
+ });
+
+ it("should allow local helpers to be declared", function() {
+ // register a global helper to ensure that it isn't overwritten
+ Handlebars.registerHelper('globalHelper', function() {
+ return '-';
+ });
+
+ var view = new Thorax.View({
+ helpers: {
+ test: function() {
+ return this.key;
+ },
+ testWithArg: function(arg) {
+ return this.key + arg;
+ },
+ testWithBlock: function(options) {
+ return options.fn(options.context);
+ }
+ },
+ key: 'value',
+ template: '{{globalHelper}} {{test}} {{testWithArg "!"}} {{#testWithBlock}}{{key}}{{/testWithBlock}}'
+ });
+ view.render();
+ expect(view.html()).to.equal('- value value! value');
+
+ view = new Thorax.View({
+ collection: new Thorax.Collection([{letter: 'a'}]),
+ template: '{{#collection tag="ul"}}<li>{{globalHelper}} {{test letter}}</li>{{/collection}}',
+ helpers: {
+ test: function(letter) {
+ return letter + "!";
+ }
}
});
- child.render();
- expect(child.html()).to.equal('value');
+ view.render();
+ expect(view.$('li').html()).to.equal('- a!');
});
it("template not found handling", function() {
var view = new Thorax.View();
expect(function() {
view.render();
- }).to.throw();
+ }).to['throw']();
});
it("render() subclassing", function() {

0 comments on commit c925bcb

Please sign in to comment.
Something went wrong with that request. Please try again.