diff --git a/src/collection.js b/src/collection.js
index 6bc1bb89..f2698a94 100644
--- a/src/collection.js
+++ b/src/collection.js
@@ -1,4 +1,8 @@
-/*global assignView, assignTemplate, createRegistryWrapper, dataObject, getValue, modelCidAttributeName, viewCidAttributeName */
+/*global
+ $serverSide,
+ assignView, assignTemplate, createRegistryWrapper, dataObject, getValue,
+ modelCidAttributeName, modelIdAttributeName, viewCidAttributeName
+*/
var _fetch = Backbone.Collection.prototype.fetch,
_set = Backbone.Collection.prototype.set,
_replaceHTML = Thorax.View.prototype._replaceHTML,
@@ -107,6 +111,51 @@ Thorax.CollectionView = Thorax.View.extend({
}
},
+ restore: function(el) {
+ var self = this,
+ children = this.$el.children(),
+ toRemove = [];
+
+ if (!Thorax.View.prototype.restore.call(this, el)) {
+ // If we had to rerender for other reasons then we don't need to do anything as it
+ // was already all blown away
+ return;
+ }
+
+ this._lookupCollectionElement();
+
+ // Find any items annotated with server info and restore. Else rerender
+ $('[' + modelIdAttributeName + ']', el).each(function() {
+ var id = this.getAttribute(modelIdAttributeName),
+ model = self.collection.get(id);
+ if (!model) {
+ toRemove.push(this);
+ } else {
+ self.restoreItem(model, children.index(this), this);
+ }
+ });
+ $('[data-view-empty]', el).each(function() {
+ self.restoreEmpty(this);
+ });
+
+ if (toRemove.length) {
+ // TODO : Is it better to just rerender the whole thing since we don't necessarily
+ // know what state the list is in at this point?
+ // Kill off any now invalid nodes
+ _.each(toRemove, function(el) {
+ el.parentNode.removeChild(el);
+ });
+
+ // Render anything that we might have locally but was missed
+ var $el = this._collectionElement;
+ this.collection.each(function(model) {
+ if (!$el.find('[' + modelCidAttributeName + '="' + model.cid + '"]').length) {
+ self.appendItem(model);
+ }
+ });
+ }
+ },
+
//appendItem(model [,index])
//appendItem(html_string, index)
//appendItem(view, index)
@@ -154,7 +203,10 @@ Thorax.CollectionView = Thorax.View.extend({
});
if (model) {
- itemElement.attr(modelCidAttributeName, model.cid);
+ itemElement.attr({
+ 'data-model-id': model.id,
+ 'data-model-cid': model.cid
+ });
}
var previousModel = index > 0 ? collection.at(index - 1) : false;
if (!previousModel) {
@@ -166,7 +218,10 @@ Thorax.CollectionView = Thorax.View.extend({
}
this.trigger('append', null, function($el) {
- $el.attr(modelCidAttributeName, model.cid);
+ $el.attr({
+ 'data-model-cid': model.cid,
+ 'data-model-id': model.id,
+ });
});
if (!options || !options.silent) {
@@ -258,12 +313,20 @@ Thorax.CollectionView = Thorax.View.extend({
viewOptions.template = this.emptyTemplate;
}
var view = Thorax.Util.getViewInstance(this.emptyView, viewOptions);
- view.ensureRendered();
+ view.$el.attr('data-view-empty', 'true');
return view;
} else {
return this.emptyTemplate && this.renderTemplate(this.emptyTemplate);
}
},
+ restoreEmpty: function(el) {
+ var child = this.renderEmpty();
+
+ child.restore(el);
+ this._addChild(child);
+ return child;
+ },
+
renderItem: function(model, i) {
if (!this.itemView) {
assignView.call(this, 'itemView', {
@@ -294,6 +357,18 @@ Thorax.CollectionView = Thorax.View.extend({
return this.renderTemplate(this.itemTemplate, this.itemContext.call(this, model, i));
}
},
+ restoreItem: function(model, i, el) {
+ // Associate the element with the proper model.
+ el.setAttribute(modelCidAttributeName, model.cid);
+
+ // If we are dealing with something other than a template then reinstantiate the view.
+ if (this.itemView || this.renderItem !== Thorax.CollectionView.prototype.renderItem) {
+ var child = this.renderItem(model, i);
+
+ child.restore(el);
+ this._addChild(child);
+ }
+ },
itemContext: function(model /*, i */) {
return model.attributes;
},
diff --git a/src/data-object.js b/src/data-object.js
index 92aae3fb..0b13b14d 100644
--- a/src/data-object.js
+++ b/src/data-object.js
@@ -1,4 +1,4 @@
-/*global getValue, inheritVars, listenTo, walkInheritTree */
+/*global $serverSide, getValue, inheritVars, listenTo, walkInheritTree */
function dataObject(type, spec) {
spec = inheritVars[type] = _.defaults({
@@ -36,7 +36,14 @@ function dataObject(type, spec) {
}
this.bindDataObject(type, dataObject, _.extend({}, this.options, options));
- $el && $el.attr(spec.cidAttrName, dataObject.cid);
+ if ($el) {
+ var attr = {};
+ if ($serverSide && spec.idAttrName) {
+ attr[spec.idAttrName] = dataObject.id;
+ }
+ attr[spec.cidAttrName] = dataObject.cid;
+ $el.attr(attr);
+ }
dataObject.trigger('set', dataObject, old);
} else {
this[type] = false;
diff --git a/src/event.js b/src/event.js
index 0f822b3f..72c8d6eb 100644
--- a/src/event.js
+++ b/src/event.js
@@ -121,7 +121,7 @@ _.extend(Thorax.View.prototype, {
} else if (!$serverSide) {
// DOM Events
if (!params.nested) {
- boundHandler = containHandlerToCurentView(boundHandler, this.cid);
+ boundHandler = containHandlerToCurentView(boundHandler, this);
}
var name = params.name + '.delegateEvents' + this.cid;
@@ -168,10 +168,12 @@ pushDomEvents([
'focus', 'blur'
]);
-function containHandlerToCurentView(handler, cid) {
+function containHandlerToCurentView(handler, current) {
+ // Passing the current view rather than just a cid to allow for updates to the view's cid
+ // caused by the restore process.
return function(event) {
var view = $(event.target).view({helper: false});
- if (view && view.cid === cid) {
+ if (view && view.cid === current.cid) {
event.originalContext = this;
return handler(event);
}
diff --git a/src/helper-view.js b/src/helper-view.js
index 0e17754d..46f47251 100644
--- a/src/helper-view.js
+++ b/src/helper-view.js
@@ -1,4 +1,8 @@
-/*global ServerMarshal, $serverSide, getOptionsData, normalizeHTMLAttributeOptions, viewHelperAttributeName */
+/*global
+ ServerMarshal,
+ $serverSide, createError, getOptionsData,
+ normalizeHTMLAttributeOptions, viewHelperAttributeName
+*/
var viewPlaceholderAttributeName = 'data-view-tmp',
viewTemplateOverrides = {};
@@ -56,17 +60,8 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) {
// Evaluate any nested parameters that we may have to content with
var expandTokens = expandHash(this, options.hash);
- var viewOptions = {
- inverse: options.inverse,
- options: options.hash,
- declaringView: declaringView,
- parent: getParent(declaringView),
- _helperName: name,
- _helperOptions: {
- options: cloneHelperOptions(options),
- args: _.clone(args)
- }
- };
+ var viewOptions = createViewOptions(name, args, options, declaringView);
+ setHelperTemplate(viewOptions, options, ViewClass);
normalizeHTMLAttributeOptions(options.hash);
var htmlAttributes = _.clone(options.hash);
@@ -96,15 +91,6 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) {
return attrs;
};
- if (options.fn) {
- // Only assign if present, allow helper view class to
- // declare template
- viewOptions.template = options.fn;
- } else if (ViewClass && ViewClass.prototype && !ViewClass.prototype.template) {
- // ViewClass may also be an instance or object with factory method
- // so need to do this check
- viewOptions.template = Handlebars.VM.noop;
- }
// Check to see if we have an existing instance that we can reuse
var instance = _.find(declaringView._previousHelpers, function(child) {
@@ -113,25 +99,14 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) {
// Create the instance if we don't already have one
if (!instance) {
- if (ViewClass.factory) {
- instance = ViewClass.factory(args, viewOptions);
- if (!instance) {
- return '';
- }
-
- instance._helperName = viewOptions._helperName;
- instance._helperOptions = viewOptions._helperOptions;
- } else {
- instance = new ViewClass(viewOptions);
- }
- if (!instance.el) {
- // ViewClass.factory may return existing objects which may have been destroyed
- throw new Error('insert-destroyed-factory');
+ instance = getHelperInstance(args, viewOptions, ViewClass);
+ if (!instance) {
+ return '';
}
instance.$el.attr('data-view-helper-restore', name);
- if ($serverSide) {
+ if ($serverSide && instance.$el.attr('data-view-restore') !== 'false') {
try {
ServerMarshal.store(instance.$el, 'args', args, options.ids, options);
ServerMarshal.store(instance.$el, 'attrs', options.hash, options.hashIds, options);
@@ -142,16 +117,11 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) {
ServerMarshal.store(instance.$el, 'inverse', options.inverse.program);
}
} catch (err) {
- instance.$el.attr('data-view-server', 'false');
+ instance.$el.attr('data-view-restore', 'false');
}
}
- args.push(instance);
- declaringView._addChild(instance);
- declaringView.trigger.apply(declaringView, ['helper', name].concat(args));
- declaringView.trigger.apply(declaringView, ['helper:' + name].concat(args));
-
- callback && callback.apply(this, args);
+ helperInit(args, instance, callback, viewOptions);
} else {
if (!instance.el) {
throw new Error('insert-destroyed');
@@ -178,9 +148,127 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) {
return new Handlebars.SafeString(Thorax.Util.tag(htmlAttributes, '', expandTokens ? this : null));
});
var helper = Handlebars.helpers[name];
+
+ helper.restore = function(declaringView, el) {
+ var context = declaringView.context(),
+ args = ServerMarshal.load(el, 'args', declaringView, context) || [],
+ attrs = ServerMarshal.load(el, 'attrs', declaringView, context) || {};
+
+ var options = {
+ hash: attrs,
+ fn: ServerMarshal.load(el, 'fn'),
+ inverse: ServerMarshal.load(el, 'inverse')
+ };
+ if (options.fn) {
+ options.fn = declaringView.template.child(options.fn);
+ }
+ if (options.inverse) {
+ options.inverse = declaringView.template.child(options.inverse);
+ }
+
+ var viewOptions = createViewOptions(name, args, options, declaringView);
+ setHelperTemplate(viewOptions, options, ViewClass);
+
+ if (viewOptionWhiteList) {
+ _.each(viewOptionWhiteList, function(dest, source) {
+ if (!_.isUndefined(attrs[source])) {
+ viewOptions[dest] = attrs[source];
+ }
+ });
+ }
+
+ var instance = getHelperInstance(args, viewOptions, ViewClass);
+ instance._assignCid(el.getAttribute('data-view-cid'));
+ helperInit(args, instance, callback, viewOptions);
+
+ instance.restore(el);
+
+ return instance;
+ };
+
return helper;
};
+Thorax.View.on('restore', function() {
+ var parent = this,
+ context;
+
+ function filterAncestors(parent, callback) {
+ return function() {
+ if ($(this).parent().view({el: true, helper: true})[0] === parent.el) {
+ return callback.call(this);
+ }
+ };
+ }
+
+ parent.$('[data-view-helper-restore]').each(filterAncestors(parent, function() {
+ if (this.getAttribute('data-view-restore') === 'true') {
+ var helper = Handlebars.helpers[this.getAttribute('data-view-helper-restore')],
+ child = helper.restore(parent, this);
+ parent._addChild(child);
+ }
+ }));
+});
+
+function createViewOptions(name, args, options, declaringView) {
+ return {
+ inverse: options.inverse,
+ options: options.hash,
+ declaringView: declaringView,
+ parent: getParent(declaringView),
+ _helperName: name,
+ _helperOptions: {
+ options: cloneHelperOptions(options),
+ args: _.clone(args)
+ }
+ };
+}
+
+function setHelperTemplate(viewOptions, options, ViewClass) {
+ if (options.fn) {
+ // Only assign if present, allow helper view class to
+ // declare template
+ viewOptions.template = options.fn;
+ } else if (ViewClass && ViewClass.prototype && !ViewClass.prototype.template) {
+ // ViewClass may also be an instance or object with factory method
+ // so need to do this check
+ viewOptions.template = Handlebars.VM.noop;
+ }
+}
+
+function getHelperInstance(args, viewOptions, ViewClass) {
+ var instance;
+
+ if (ViewClass.factory) {
+ instance = ViewClass.factory(args, viewOptions);
+ if (!instance) {
+ return;
+ }
+
+ instance._helperName = viewOptions._helperName;
+ instance._helperOptions = viewOptions._helperOptions;
+ } else {
+ instance = new ViewClass(viewOptions);
+ }
+
+ if (!instance.el) {
+ // ViewClass.factory may return existing objects which may have been destroyed
+ throw createError('insert-destroyed-factory');
+ }
+ return instance;
+}
+function helperInit(args, instance, callback, viewOptions) {
+ var declaringView = viewOptions.declaringView,
+ name = viewOptions._helperName;
+
+ args.push(instance);
+ declaringView._addChild(instance);
+ declaringView.trigger.apply(declaringView, ['helper', name].concat(args));
+ declaringView.trigger.apply(declaringView, ['helper:' + name].concat(args));
+
+ callback && callback.apply(this, args);
+}
+
function helperAppend(scope, callback) {
this._pendingAppend = undefined;
@@ -211,6 +299,12 @@ function helperAppend(scope, callback) {
function cloneHelperOptions(options) {
var ret = _.pick(options, 'fn', 'inverse', 'hash', 'data');
ret.data = _.omit(options.data, 'cid', 'view', 'yield', 'root', '_parent');
+
+ // This is necessary to prevent failures when mixing restored and rendered data
+ // as it forces the keys object to be complete.
+ ret.fn = ret.fn || undefined;
+ ret.inverse = ret.inverse || undefined;
+
return ret;
}
@@ -246,7 +340,7 @@ function compareHelperOptions(a, b) {
// Implements a first level depth comparison
return a.args.length === b.args.length
&& compareValues(a.args, b.args)
- && _.isEqual(_.keys(a.options), _.keys(b.options))
+ && _.isEqual(_.keys(a.options).sort(), _.keys(b.options).sort())
&& _.every(a.options, function(value, key) {
if (key === 'data' || key === 'hash') {
return compareValues(a.options[key], b.options[key]);
diff --git a/src/helpers/collection.js b/src/helpers/collection.js
index 2dfadf68..0643f315 100644
--- a/src/helpers/collection.js
+++ b/src/helpers/collection.js
@@ -1,6 +1,8 @@
/* global
+ $serverSide,
collectionElementAttributeName, createErrorMessage, getOptionsData, getParent,
- helperViewPrototype, normalizeHTMLAttributeOptions
+ helperViewPrototype, normalizeHTMLAttributeOptions,
+ viewRestoreAttribute
*/
Thorax.CollectionHelperView = Thorax.CollectionView.extend({
@@ -19,6 +21,8 @@ Thorax.CollectionHelperView = Thorax.CollectionView.extend({
},
constructor: function(options) {
+ var restorable = true;
+
// need to fetch templates if template name was passed
if (options.options['item-template']) {
options.itemTemplate = Thorax.Util.getTemplate(options.options['item-template']);
@@ -33,10 +37,20 @@ Thorax.CollectionHelperView = Thorax.CollectionView.extend({
if (!options.itemTemplate && options.template && options.template !== Handlebars.VM.noop) {
options.itemTemplate = options.template;
options.template = Handlebars.VM.noop;
+
+ // We can not restore if the item has a depthed reference, ../foo, so we need to
+ // force a rerender on the client-side
+ if (options.itemTemplate.depth) {
+ restorable = false;
+ }
}
if (!options.emptyTemplate && options.inverse && options.inverse !== Handlebars.VM.noop) {
options.emptyTemplate = options.inverse;
options.inverse = Handlebars.VM.noop;
+
+ if (options.emptyTemplate.depth) {
+ restorable = false;
+ }
}
var shouldBindItemContext = _.isFunction(options.itemContext),
@@ -72,6 +86,10 @@ Thorax.CollectionHelperView = Thorax.CollectionView.extend({
}
}
+ if ($serverSide && !restorable) {
+ this.$el.attr(viewRestoreAttribute, 'false');
+ }
+
return response;
},
setAsPrimaryCollectionHelper: function() {
diff --git a/src/layout.js b/src/layout.js
index e42d5d30..354ed0ec 100644
--- a/src/layout.js
+++ b/src/layout.js
@@ -1,4 +1,8 @@
-/*global $serverSide, emit, getOptionsData, normalizeHTMLAttributeOptions, createErrorMessage */
+/*global
+ $serverSide,
+ createErrorMessage, emit, getLayoutViewsTargetElement, getOptionsData,
+ normalizeHTMLAttributeOptions, viewNameAttributeName
+*/
var layoutCidAttributeName = 'data-layout-cid';
Thorax.LayoutView = Thorax.View.extend({
@@ -22,7 +26,17 @@ Thorax.LayoutView = Thorax.View.extend({
if (_.isString(view)) {
view = new (Thorax.Util.registryGet(Thorax, 'Views', view, false))();
}
+
+ if (!$serverSide && !this.hasBeenSet) {
+ var existing = this.$('[' + viewNameAttributeName + '="' + view.name + '"]')[0];
+ if (existing) {
+ view.restore(existing);
+ } else {
+ $(this._layoutViewEl).empty();
+ }
+ }
this.ensureRendered();
+
var oldView = this._view, append, remove, complete;
if (view === oldView) {
return false;
@@ -61,6 +75,7 @@ Thorax.LayoutView = Thorax.View.extend({
}, this);
complete = _.bind(function() {
+ this.hasBeenSet = true;
this.trigger('change:view:end', view, oldView, options);
}, this);
@@ -80,6 +95,8 @@ Thorax.LayoutView = Thorax.View.extend({
}
});
+Thorax.LayoutView.on('restore', ensureLayoutViewsTargetElement);
+
Handlebars.registerHelper('layout-element', function(options) {
var view = getOptionsData(options).view;
// duck type check for LayoutView
diff --git a/src/model.js b/src/model.js
index d56b1086..2e3ffce1 100644
--- a/src/model.js
+++ b/src/model.js
@@ -1,5 +1,6 @@
/*global createRegistryWrapper, dataObject, getValue, inheritVars */
-var modelCidAttributeName = 'data-model-cid';
+var modelCidAttributeName = 'data-model-cid',
+ modelIdAttributeName = 'data-model-id';
Thorax.Model = Backbone.Model.extend({
isEmpty: function() {
@@ -46,6 +47,7 @@ dataObject('model', {
},
change: onModelChange,
$el: '$el',
+ idAttrName: modelIdAttributeName,
cidAttrName: modelCidAttributeName
});
diff --git a/src/thorax.js b/src/thorax.js
index 99907c65..9ea77e99 100644
--- a/src/thorax.js
+++ b/src/thorax.js
@@ -25,7 +25,14 @@ if (!$.fn.forEach) {
var viewNameAttributeName = 'data-view-name',
viewCidAttributeName = 'data-view-cid',
- viewHelperAttributeName = 'data-view-helper';
+ viewHelperAttributeName = 'data-view-helper',
+
+ // Used to identify views that can be restored vs. rerendered on the client side.
+ // Values are:
+ // - true - Can be restored
+ // - false - Must be rerendered
+ // - Omitted - Normal HTML element without associated view
+ viewRestoreAttribute = 'data-view-restore';
//view instances
var viewsIndexedByCid = {};
@@ -91,16 +98,43 @@ Thorax.View = Backbone.View.extend({
_configure: function() {},
_ensureElement: function () {
configureView.call(this);
+
+ if (!$serverSide && this.el) {
+ var $el = $(_.result(this, 'el'));
+ if ($el.length) {
+ return this.restore($el);
+ }
+ }
+
return Backbone.View.prototype._ensureElement.call(this);
},
- setElement : function() {
+ setElement : function(element) {
+ var $element = $(element),
+ existingCid = $element.attr('data-view-cid');
+ if (existingCid) {
+ this._assignCid(existingCid);
+ }
+
var response = Backbone.View.prototype.setElement.apply(this, arguments);
- this.name && this.$el.attr(viewNameAttributeName, this.name);
- this.$el.attr(viewCidAttributeName, this.cid);
+
+ // Use a hash here to avoid multiple DOM operations
+ var attr = {'data-view-cid': this.cid};
+ if (this.name) {
+ attr[viewNameAttributeName] = this.name;
+ }
+ this.$el.attr(attr);
+
return response;
},
+ _assignCid: function(cid) {
+ if (this.cid) {
+ delete viewsIndexedByCid[this.cid];
+ }
+ this.cid = cid;
+ viewsIndexedByCid[cid] = this;
+ },
_addChild: function(view) {
if (this.children[view.cid]) {
@@ -157,6 +191,39 @@ Thorax.View = Backbone.View.extend({
this._helperOptions = undefined;
},
+ restore: function(element) {
+ if (this._renderCount) {
+ // Ensure that we are registered to the right cid (this could have been reset previously)
+ var oldCid = this.$el.attr('data-view-cid');
+ if (this.cid !== oldCid) {
+ this._assignCid(oldCid);
+ }
+
+ $(element).replaceWith(this.$el);
+ return;
+ }
+
+ this.setElement(element);
+
+ var $element = $(element);
+ if (!$serverSide && $element.attr(viewRestoreAttribute) === 'true') {
+ this._renderCount = 1;
+ this.trigger('restore');
+
+ var remainingViews = this.$('[data-view-restore=false]'),
+ rerender = _.any(remainingViews, function(el) {
+ return $(el).parent().view({el: true, helper: true})[0] === $element[0];
+ });
+ if (rerender) {
+ this.render();
+ }
+
+ return true;
+ } else {
+ this.render();
+ }
+ },
+
render: function(output) {
var self = this;
// NOP for destroyed views
@@ -210,6 +277,15 @@ Thorax.View = Backbone.View.extend({
}, self);
self._previousHelpers = undefined;
+
+ if ($serverSide) {
+ if (self.$el.attr(viewRestoreAttribute) !== 'false') {
+ self.$el.attr(viewRestoreAttribute, $serverSide);
+ }
+ } else {
+ self.$el.removeAttr(viewRestoreAttribute);
+ }
+
//accept a view, string, Handlebars.SafeString or DOM element
self.html((output && output.el) || (output && output.string) || output);
diff --git a/test/src/server-side.js b/test/src/server-side.js
index c325a233..f5093301 100644
--- a/test/src/server-side.js
+++ b/test/src/server-side.js
@@ -1,3 +1,5 @@
+/*global $serverSide */
+
describe('serverSide', function() {
var _serverSide = window.$serverSide,
emit;
@@ -76,4 +78,966 @@ describe('serverSide', function() {
expect(end.called).to.be(false);
});
});
+
+ describe('rendering', function() {
+ it('should track server-side rendering vs. not', function() {
+ var view = new Thorax.View({template: function() { return 'foo'; }});
+ view.render();
+ expect(view.$el.attr('data-view-restore')).to.equal('true');
+
+ window.$serverSide = false;
+ view = new Thorax.View({template: function() { return 'foo'; }});
+ view.render();
+ expect(view.$el.attr('data-view-restore')).to.not.exist;
+ });
+ });
+
+ describe('restore', function() {
+ if ($serverSide) {
+ return;
+ }
+
+ var count,
+ Counter = Thorax.View.extend({
+ template: function() {
+ return 'foo_' + count++;
+ }
+ }),
+ SomethingElse = Thorax.View.extend({
+ _name: 'somethingelse',
+ template: function() { return 'somethingelse'; }
+ }),
+ render,
+ fixture,
+ server,
+ view;
+
+ function cleanIds(view) {
+ view.$('[data-model-cid]').each(function() {
+ $(this).removeAttr('data-model-cid');
+ });
+ view.$('[data-view-restore]').each(function() {
+ $(this).removeAttr('data-view-restore');
+ });
+ view.$('[data-server-data]').each(function() {
+ $(this).removeAttr('data-server-data');
+ });
+ }
+ function restoreView(shouldRender) {
+ server.render();
+
+ var el = server.$el.clone();
+ fixture.append(el);
+
+ window.$serverSide = false;
+
+ render.reset();
+ view.restore(el);
+ expect(render.called).to.equal(!!shouldRender);
+ }
+ function compareViews() {
+ cleanIds(view);
+ cleanIds(server);
+ expect(view.$el.html()).to.equal(server.$el.html());
+ }
+
+ beforeEach(function() {
+ window.$serverSide = false;
+ count = 0;
+
+ fixture = $('
');
+ $('body').append(fixture);
+
+ render = this.spy(Thorax.View.prototype, 'render');
+
+ Thorax.Views.registry = Thorax.View.extend({
+ name: 'registry',
+ template: function() {
+ return 'foo ' + count++;
+ }
+ });
+ });
+ afterEach(function() {
+ fixture.remove();
+ });
+
+ it('should restore views explicitly', function() {
+ var el = $('
bar
');
+ fixture.append(el);
+
+ var view = new Thorax.View({
+ el: '.foo-view'
+ });
+ expect(view.el).to.equal(el[0]);
+ expect(view._renderCount).to.equal(1);
+ expect(el.html()).to.equal('bar');
+ el.remove();
+
+ el = $('
bar
');
+ fixture.append(el);
+ view = new Thorax.View({
+ el: function() {
+ return '.foo-view';
+ }
+ });
+ expect(view.el).to.equal(el[0]);
+ expect(view._renderCount).to.equal(1);
+ expect(el.html()).to.equal('bar');
+ });
+ it('should re-render non-server elements on restore', function() {
+ var el = $('
bar
');
+ fixture.append(el);
+
+ var view = new Thorax.View({
+ el: '.foo-view',
+ template: function() {
+ return 'bat';
+ }
+ });
+ expect(view.el).to.equal(el[0]);
+ expect(view._renderCount).to.equal(1);
+ expect(el.html()).to.equal('bat');
+ });
+
+ it('should restore views with a passed el', function() {
+ var el = $('
bar
');
+ fixture.append(el);
+
+ var view = new Thorax.View({});
+ view.restore(el);
+
+ expect(view.el).to.equal(el[0]);
+ expect(view._renderCount).to.equal(1);
+ expect(el.html()).to.equal('bar');
+ });
+
+ it('should update view attributes on restore', function() {
+ var el = $('
bar
');
+ fixture.append(el);
+
+ var spy = this.spy();
+ var view = new Thorax.View({
+ events: {
+ click: spy
+ }
+ });
+ view.restore(el);
+
+ expect(view.$el.attr('data-view-cid')).to.equal(view.cid);
+ });
+
+ if (!$serverSide) {
+ it('should restore DOM events', function() {
+ var el = $('
bar
');
+ fixture.append(el);
+
+ var spy = this.spy();
+ var view = new Thorax.View({
+ events: {
+ click: spy
+ }
+ });
+ view.restore(el);
+
+ el.trigger('click');
+ expect(spy.calledOnce).to.be(true);
+ });
+ }
+
+ it('should replace restore of rendered view', function() {
+ var el = $('
bar
');
+ fixture.append(el);
+
+ var view = new Thorax.View({});
+ view.render('foo');
+ view.restore(el);
+
+ var el = fixture.find('div');
+ expect(view.el).to.equal(el[0]);
+ expect(view._renderCount).to.equal(1);
+ expect(el.html()).to.equal('foo');
+ expect(view.$el.attr('data-view-restore')).to.not.exist;
+ });
+
+ describe('setView', function() {
+ var layout,
+ view;
+ beforeEach(function() {
+ layout = new Thorax.LayoutView();
+ layout.render();
+
+ view = new Thorax.View({
+ name: 'winning',
+ template: function() {
+ return 'not winning';
+ }
+ });
+ });
+ it('should restore matching views', function() {
+ var $el = $('
winning
');
+ layout.$el.append($el);
+
+ layout.setView(view);
+ expect(view.$el.html()).to.equal('winning');
+ expect(layout._view).to.equal(view);
+ expect(layout.$el.children().length).to.equal(1);
+ expect(layout.$el.text()).to.equal('winning');
+ });
+ it('should rerender non-matching views', function() {
+ var $el = $('
winning
');
+ layout.$el.append($el);
+
+ layout.setView(view);
+ expect(view.$el.html()).to.equal('not winning');
+ expect(layout._view).to.equal(view);
+ expect(layout.$el.children().length).to.equal(1);
+ expect(layout.$el.text()).to.equal('not winning');
+ });
+ it('should rerender non-server views', function() {
+ var $el = $('
winning
');
+ layout.$el.append($el);
+
+ layout.setView(view);
+ expect(view.$el.html()).to.equal('not winning');
+ expect(layout._view).to.equal(view);
+ expect(layout.$el.children().length).to.equal(1);
+ expect(layout.$el.text()).to.equal('not winning');
+ });
+ });
+
+ describe('view helper', function() {
+ beforeEach(function() {
+ window.$serverSide = true;
+ });
+ describe('registry', function() {
+ it('should rerender anonymous block', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{#view}}something{{/view}}', {trackIds: true})
+ });
+
+ server = new View();
+ view = new View();
+ restoreView(false);
+
+ compareViews();
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0].$el.html()).to.equal('something');
+ expect(view._renderCount).to.equal(1);
+ });
+
+ it('should restore views instantiated through the registry', function() {
+ server = new Thorax.View({
+ template: Handlebars.compile('{{view "registry"}}')
+ });
+ view = new SomethingElse();
+ restoreView();
+
+ compareViews();
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.be.a(Thorax.Views.registry);
+ });
+ it('should include view args when instantiating view', function() {
+ server = new Thorax.View({
+ template: Handlebars.compile('{{view "registry" key=4}}', {trackIds: true})
+ });
+ view = new SomethingElse();
+ restoreView();
+
+ expect(_.values(view.children)[0].key).to.equal(4);
+ });
+ it('should restore named complex args', function() {
+ server = new Thorax.View({
+ foo: {
+ yes: false
+ },
+ template: Handlebars.compile('{{view "registry" key=4 bar=foo}}', {trackIds: true})
+ });
+ view = new SomethingElse({
+ foo: {
+ yes: true
+ }
+ });
+ restoreView();
+
+ expect(_.values(view.children)[0].key).to.equal(4);
+ expect(_.values(view.children)[0].bar).to.equal(view.foo);
+ });
+ it('should restore passed classes', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view ChildClass}}', {trackIds: true})
+ });
+
+ server = new View({
+ context: function() {
+ return {
+ ChildClass: Counter
+ };
+ }
+ });
+ view = new View({
+ context: function() {
+ return {
+ ChildClass: SomethingElse
+ };
+ }
+ });
+ restoreView();
+
+ compareViews();
+ expect(_.keys(view.children).length).to.equal(1);
+ });
+ });
+ it('should restore named references', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view child}}', {trackIds: true})
+ });
+
+ server = new View({
+ child: new Counter()
+ });
+ view = new View({
+ child: new SomethingElse()
+ });
+ restoreView();
+
+ compareViews();
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(view.child);
+ });
+ it('should restore pathed named references', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view parent.child}}', {trackIds: true})
+ });
+
+ server = new View({
+ parent: {
+ child: new Counter()
+ }
+ });
+ view = new View({
+ parent: {
+ child: new SomethingElse()
+ }
+ });
+ restoreView();
+
+ compareViews();
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(view.parent.child);
+ });
+ it('should restore context responses', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view child}}', {trackIds: true})
+ });
+ var child = new SomethingElse();
+
+ server = new View({
+ context: function() {
+ return {
+ child: new Counter()
+ };
+ }
+ });
+ view = new View({
+ context: function() {
+ return {
+ child: child
+ };
+ }
+ });
+ restoreView();
+
+ compareViews();
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(child);
+ });
+ it('should restore named references within iterators', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{#each parent}}{{view .}}{{/each}}', {trackIds: true})
+ });
+
+ server = new View({
+ parent: {
+ child: new Counter()
+ }
+ });
+ view = new View({
+ parent: {
+ child: new SomethingElse()
+ }
+ });
+ restoreView();
+
+ compareViews();
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(view.parent.child);
+ });
+ it('should rerender ../ depthed references', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{#each parent}}{{view ../parent.child}}{{/each}}', {trackIds: true})
+ });
+
+ server = new View({
+ parent: {
+ child: new Counter()
+ }
+ });
+ view = new View({
+ parent: {
+ child: new SomethingElse()
+ }
+ });
+ restoreView(true);
+
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(view.parent.child);
+ expect(view.parent.child.$el.html()).to.equal('somethingelse');
+ expect(view._renderCount).to.equal(2);
+ });
+ it('should restore block view helpers', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{#view child}}something{{/view}}', {trackIds: true})
+ });
+
+ server = new View({
+ child: new Counter()
+ });
+ view = new View({
+ child: new SomethingElse()
+ });
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(view.child);
+ expect(server.child.$el.html()).to.equal('something');
+ expect(view.child.$el.html()).to.equal('something');
+ expect(view._renderCount).to.equal(1);
+ });
+ it('should restore block view helper with depth', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{#view child}}{{../root}}{{/view}}', {trackIds: true}),
+ root: 1
+ });
+
+ server = new View({
+ child: new Counter()
+ });
+ view = new View({
+ child: new SomethingElse()
+ });
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(view.child);
+ expect(server.child.$el.html()).to.equal('1');
+ expect(view.child.$el.html()).to.equal('1');
+ expect(view._renderCount).to.equal(1);
+ });
+ it('should restore block view helper with data', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{#view child}}{{@cid}}{{/view}}', {trackIds: true})
+ });
+
+ server = new View({
+ child: new Counter()
+ });
+ view = new View({
+ child: new SomethingElse()
+ });
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(view.child);
+ expect(view._renderCount).to.equal(1);
+ });
+
+ it('should handle partial restore', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view theGoodOne}}{{view (ambiguousResponse child)}}', {trackIds: true}),
+ ambiguousResponse: function(child) {
+ return child;
+ }
+ });
+
+ server = new View({
+ theGoodOne: new Counter(),
+ child: new Counter()
+ });
+ view = new View({
+ theGoodOne: new SomethingElse(),
+ child: new SomethingElse()
+ });
+ restoreView(true);
+
+ expect(_.keys(view.children).length).to.equal(2);
+ expect(_.values(view.children)[0]).to.equal(view.theGoodOne);
+ expect(_.values(view.children)[1]).to.equal(view.child);
+ expect(view.child.$el.html()).to.equal('somethingelse');
+ expect(view.theGoodOne.$el.html()).to.equal('foo_0');
+ expect(view._renderCount).to.equal(2);
+ });
+
+ it('should restore nested views properly', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view "nested"}}', {trackIds: true})
+ });
+
+ Thorax.Views.nested = Thorax.View.extend({
+ name: 'nested',
+ template: Handlebars.compile('{{view child}}', {trackIds: true}),
+ child: new Counter()
+ });
+
+ server = new View();
+ view = new View();
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var nested = _.values(view.children)[0];
+ expect(nested.name).to.equal('nested');
+ expect(nested.child.$el.html()).to.equal('foo_0');
+ });
+ it('should rerender views using subexpressions', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view (ambiguousResponse child)}}', {trackIds: true}),
+ ambiguousResponse: function(view) {
+ return view;
+ }
+ });
+
+ server = new View({
+ child: new Counter()
+ });
+ view = new View({
+ child: new SomethingElse()
+ });
+ restoreView(true);
+
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(view.child);
+ expect(server.child.$el.html()).to.equal('foo_0');
+ expect(view.child.$el.html()).to.equal('somethingelse');
+ expect(view._renderCount).to.equal(2);
+ });
+
+ it('should rerender lookup templates if lacking trackId', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view child}}')
+ });
+ var child = new SomethingElse();
+
+ server = new View({
+ context: function() {
+ return {
+ child: new Counter()
+ };
+ }
+ });
+ view = new View({
+ context: function() {
+ return {
+ child: child
+ };
+ }
+ });
+ restoreView(true);
+
+ expect(_.keys(view.children).length).to.equal(1);
+ expect(_.values(view.children)[0]).to.equal(child);
+ });
+ });
+ describe('collection views', function() {
+ var collection1, collection2;
+ beforeEach(function() {
+ window.$serverSide = true;
+ count = 0;
+
+ collection1 = new Thorax.Collection([{id: 1}, {id: 2}, {id: 3}, {id: 4}]);
+ collection2 = new Thorax.Collection([{id: 1}, {id: 2}, {id: 3}, {id: 4}]);
+ });
+
+ it('should restore inline views', function() {
+ server = new Thorax.View({
+ template: Handlebars.compile('{{#collection}}something{{/collection}}', {trackIds: true}),
+ collection: collection1
+ });
+ view = new Thorax.View({
+ template: Handlebars.compile('{{#collection}}somethingelse{{/collection}}', {trackIds: true}),
+ collection: collection2
+ });
+ restoreView();
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(collection2);
+ expect(_.keys(collectionView.children).length).to.equal(0);
+ expect(collectionView.itemTemplate()).to.equal('somethingelse');
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ expect(view.$el.children().text()).to.equal('somethingsomethingsomethingsomething');
+ compareViews();
+ });
+ it('should restore referenced templates', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{collection tag="ul" empty-template="letter-empty" item-template="letter-item"}}')
+ });
+ server = new View({collection: collection1});
+ view = new View({collection: collection2});
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(collection2);
+ expect(collectionView.itemTemplate).to.equal(Thorax.Util.getTemplate('letter-item'));
+ expect(collectionView.emptyTemplate).to.equal(Thorax.Util.getTemplate('letter-empty'));
+
+ expect(_.keys(collectionView.children).length).to.equal(0);
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ compareViews();
+ });
+ it('should restore implicit named templates', function() {
+ var _ItemView = Thorax.Views['letter-item'];
+ Thorax.Views['letter-item'] = undefined;
+
+ var View = Thorax.View.extend({
+ name: 'letter',
+ template: Handlebars.compile('{{collection tag="ul"}}')
+ });
+ server = new View({collection: collection1});
+ view = new View({collection: collection2});
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(collection2);
+ expect(collectionView.itemTemplate).to.equal(Thorax.Util.getTemplate('letter-item'));
+ expect(collectionView.emptyTemplate).to.equal(Thorax.Util.getTemplate('letter-empty'));
+
+ expect(_.keys(collectionView.children).length).to.equal(0);
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ compareViews();
+
+ Thorax.Views['letter-item'] = _ItemView;
+ });
+ it('should restore implicit item views', function() {
+ var View = Thorax.View.extend({
+ name: 'letter',
+ template: Handlebars.compile('{{collection tag="ul"}}')
+ });
+ server = new View({collection: collection1});
+ view = new View({collection: collection2});
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(collection2);
+ expect(collectionView.itemView).to.equal(Thorax.Util.getViewClass('letter-item'));
+ expect(collectionView.emptyView).to.equal(Thorax.Util.getViewClass('letter-empty'));
+
+ expect(_.keys(collectionView.children).length).to.equal(4);
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ compareViews();
+ });
+ it('should restore referenced item-view', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{collection tag="ul" empty-view="letter-empty" item-view=ChildView}}', {trackIds: true}),
+ ChildView: Thorax.Util.getViewClass('letter-item')
+ });
+ server = new View({collection: collection1});
+ view = new View({collection: collection2});
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.itemView).to.equal(view.ChildView);
+ expect(collectionView.emptyView).to.equal('letter-empty');
+
+ expect(_.keys(collectionView.children).length).to.equal(4);
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ compareViews();
+ });
+ it('should restore registry item-view', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{collection tag="ul" empty-view="letter-empty" item-view="letter-item"}}')
+ });
+ server = new View({collection: collection1});
+ view = new View({collection: collection2});
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.itemView).to.equal('letter-item');
+ expect(collectionView.emptyView).to.equal('letter-empty');
+
+ expect(_.keys(collectionView.children).length).to.equal(4);
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ compareViews();
+ });
+ it('should restore renderItem views', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{collection}}')
+ });
+ server = new View({
+ collection: collection1,
+ renderItem: function() {
+ return new Counter();
+ }
+ });
+ view = new View({
+ collection: collection2,
+ renderItem: function() {
+ return new SomethingElse();
+ }
+ });
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(_.keys(collectionView.children).length).to.equal(4);
+ expect(_.every(collectionView.children, function(value) {
+ return value._name === 'somethingelse';
+ })).to.be['true'];
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ compareViews();
+ });
+ it('should restore empty-template', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{collection tag="ul" empty-template="letter-empty" item-template="letter-item"}}')
+ });
+ server = new View({collection: new Thorax.Collection()});
+ view = new View({collection: new Thorax.Collection()});
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(view.collection);
+ expect(collectionView.itemTemplate).to.equal(Thorax.Util.getTemplate('letter-item'));
+ expect(collectionView.emptyTemplate).to.equal(Thorax.Util.getTemplate('letter-empty'));
+
+ expect(_.keys(collectionView.children).length).to.equal(0);
+
+ compareViews();
+ });
+ it('should restore empty-view', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{collection tag="ul" empty-view="letter-empty" item-template="letter-item"}}')
+ });
+ server = new View({collection: new Thorax.Collection()});
+ view = new View({collection: new Thorax.Collection()});
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(view.collection);
+ expect(collectionView.itemTemplate).to.equal(Thorax.Util.getTemplate('letter-item'));
+ expect(collectionView.emptyView).to.equal('letter-empty');
+
+ expect(_.keys(collectionView.children).length).to.equal(1);
+
+ compareViews();
+ });
+ it('should restore over non-default collection name', function() {
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{collection foo tag="ul" empty-view="letter-empty" item-view="letter-item"}}', {trackIds: true})
+ });
+ server = new View({foo: collection1});
+ view = new View({foo: collection2});
+ restoreView();
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.itemView).to.equal('letter-item');
+ expect(collectionView.emptyView).to.equal('letter-empty');
+
+ expect(_.keys(collectionView.children).length).to.equal(4);
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ compareViews();
+ });
+
+ it('should rerender items for inline view helper with depth', function() {
+ server = new Thorax.View({
+ template: Handlebars.compile('
this
{{#collection}}{{../foo}}something{{/collection}}', {trackIds: true}),
+ collection: collection1
+ });
+ view = new Thorax.View({
+ template: Handlebars.compile('
that
{{#collection}}{{../foo}}somethingelse{{/collection}}', {trackIds: true}),
+ collection: collection2
+ });
+ restoreView(true);
+
+ expect(view.$el.children().length).to.equal(2);
+ expect(view.$el.children().eq(0).text()).to.equal('that');
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(collection2);
+ expect(_.keys(collectionView.children).length).to.equal(0);
+ expect(collectionView.itemTemplate()).to.equal('somethingelse');
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+
+ expect(view.$el.text()).to.equal('thatsomethingelsesomethingelsesomethingelsesomethingelse');
+ });
+ it.skip('should rerender block view helper with data', function() {
+ server = new Thorax.View({
+ template: Handlebars.compile('
this
{{#collection}}{{@foo}}something{{/collection}}', {trackIds: true}),
+ collection: collection1
+ });
+ view = new Thorax.View({
+ template: Handlebars.compile('
that
{{#collection}}{{@foo}}somethingelse{{/collection}}', {trackIds: true}),
+ collection: collection2
+ });
+ restoreView(true);
+
+ expect(view.$el.children().length).to.equal(2);
+ expect(view.$el.children().eq(0).text()).to.equal('that');
+
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(collection2);
+ expect(_.keys(collectionView.children).length).to.equal(0);
+ expect(collectionView.itemTemplate()).to.equal('somethingelse');
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+
+ expect(view.$el.text()).to.equal('thatsomethingelsesomethingelsesomethingelsesomethingelse');
+ });
+
+ it('should rerender collections where elements are missing ids', function() {
+ collection2 = new Thorax.Collection([{id: 1}, {id: 6}, {id: 3}, {id: 4}]);
+
+ server = new Thorax.View({
+ template: Handlebars.compile('{{#collection}}{{id}}. something{{/collection}}', {trackIds: true}),
+ collection: collection1
+ });
+ view = new Thorax.View({
+ template: Handlebars.compile('{{#collection}}somethingelse{{/collection}}', {trackIds: true}),
+ collection: collection2
+ });
+ restoreView();
+ expect(_.keys(view.children).length).to.equal(1);
+
+ var collectionView = _.values(view.children)[0];
+ expect(collectionView.collection).to.equal(collection2);
+ expect(_.keys(collectionView.children).length).to.equal(0);
+ expect(collectionView.itemTemplate()).to.equal('somethingelse');
+
+ var viewCids = _.map(collectionView.$('[data-model-cid]'), function(el) {
+ return el.getAttribute('data-model-cid');
+ });
+ expect(viewCids).to.eql(collection2.map(function(model) { return model.cid; }));
+ expect(view.$el.children().text()).to.equal('1. somethingsomethingelse3. something4. something');
+ });
+ });
+
+ it('should cooperate with custom restore events', function() {
+ window.$serverSide = true;
+
+ var View = Thorax.View.extend({
+ template: Handlebars.compile('{{view child}}', {trackIds: true})
+ });
+ var child = new SomethingElse({
+ restore: function(el) {
+ this.setElement(el);
+
+ this.$el.text('winning!');
+ return true;
+ }
+ });
+
+ server = new View({
+ child: new Counter(),
+ context: function() {
+ return {
+ child: this.child
+ };
+ }
+ });
+ view = new View({
+ context: function() {
+ return {
+ child: child
+ };
+ }
+ });
+ restoreView();
+
+ expect(view.$el.text()).to.equal('winning!');
+ expect(child.$el.attr('data-view-cid')).to.equal(server.child.cid);
+ });
+
+ it('should recover views on client-side parent rerender', function() {
+ window.$serverSide = true;
+
+ server = new Thorax.View({
+ foo: {
+ yes: false
+ },
+ template: Handlebars.compile('{{view "registry" key=4 bar=foo}}', {trackIds: true})
+ });
+ view = new SomethingElse({
+ foo: {
+ yes: true
+ },
+ template: Handlebars.compile('{{view "registry" key=4 bar=foo}}', {trackIds: true})
+ });
+ restoreView();
+
+ var restored = _.values(view.children)[0];
+ expect(restored.key).to.equal(4);
+ expect(restored.bar).to.equal(view.foo);
+
+ view.render();
+ expect(_.values(view.children)).to.eql([restored]);
+ });
+ });
});