From b833c26f757545cb9c9a1d3ec2ee1ebc585b5c33 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 4 Jan 2014 01:14:44 -0600 Subject: [PATCH 01/18] Basic restore implementation --- src/thorax.js | 30 +++++++++- test/src/server-side.js | 118 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/src/thorax.js b/src/thorax.js index 99907c65..02d07ded 100644 --- a/src/thorax.js +++ b/src/thorax.js @@ -25,7 +25,8 @@ if (!$.fn.forEach) { var viewNameAttributeName = 'data-view-name', viewCidAttributeName = 'data-view-cid', - viewHelperAttributeName = 'data-view-helper'; + viewHelperAttributeName = 'data-view-helper', + viewServerAttribute = 'data-view-server'; //view instances var viewsIndexedByCid = {}; @@ -91,6 +92,14 @@ 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); }, @@ -157,6 +166,18 @@ Thorax.View = Backbone.View.extend({ this._helperOptions = undefined; }, + restore: function(element) { + this.setElement(element); + if (!$serverSide && $(element).attr(viewServerAttribute)) { + this._renderCount = 1; + this.trigger('restore'); + + return true; + } else { + this.render(); + } + }, + render: function(output) { var self = this; // NOP for destroyed views @@ -210,6 +231,13 @@ Thorax.View = Backbone.View.extend({ }, self); self._previousHelpers = undefined; + + if ($serverSide) { + self.$el.attr(viewServerAttribute, $serverSide); + } else { + self.$el.removeAttr(viewServerAttribute); + } + //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..a84c4723 100644 --- a/test/src/server-side.js +++ b/test/src/server-side.js @@ -76,4 +76,122 @@ describe('serverSide', function() { expect(end.called).to.be(false); }); }); + + describe('rendering', function() { + it('should track server-side rendering vs. not'); + }); + + describe('restore', function() { + var fixture; + beforeEach(function() { + window.$serverSide = false; + + fixture = $('
'); + $('body').append(fixture); + }); + 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'); + + 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); + }); + } + + describe('view helper', function() { + describe('registry', function() { + it('should restore views instantiated through the registry'); + it('should include view args when instantiating view'); + it('should invalidate views with complex args'); + }); + it('should restore named references'); + it('should restore pathed named references'); + it('should restore named references within iterators'); + it('should handle block view helpers'); // {{#view foo}}shit{{/view}} + }); + describe('collection views', function() { + it('should restore inline views'); + it('should restore referenced views'); + it('should restore renderItem views'); + it('should restore over non-default collection name'); + }); + it('should restore helper views'); + + it('should replace and log on restore of rendered view'); + }); }); From e784d982e06d2d4bf01c20f30ad38a2e9dc1c18b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 7 Jan 2014 16:17:37 -0600 Subject: [PATCH 02/18] Add bulk of failing view restore tests --- test/src/server-side.js | 386 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 374 insertions(+), 12 deletions(-) diff --git a/test/src/server-side.js b/test/src/server-side.js index a84c4723..cf8c5403 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; @@ -78,16 +80,73 @@ describe('serverSide', function() { }); describe('rendering', function() { - it('should track server-side rendering vs. not'); + 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-server')).to.equal('true'); + + window.$serverSide = false; + view = new Thorax.View({template: function() { return 'foo'; }}); + view.render(); + expect(view.$el.attr('data-view-server')).to.not.exist; + }); }); describe('restore', function() { - var fixture; + if ($serverSide) { + return; + } + + var count, + Counter = Thorax.View.extend({ + template: function() { + return 'foo_' + count++; + } + }), + SomethingElse = Thorax.View.extend({ + template: function() { return 'somethingelse'; } + }), + render, + fixture, + server, + view; + + function cleanIds(view) { + view.$('[data-view-cid]').each(function() { + $(this).removeAttr('data-view-cid'); + }); + view.$('[data-view-server]').each(function() { + $(this).removeAttr('data-view-server'); + }); + 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; fixture = $('
'); $('body').append(fixture); + + render = this.spy(Thorax.View.prototype, 'render'); }); afterEach(function() { fixture.remove(); @@ -103,7 +162,10 @@ describe('serverSide', function() { 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'; @@ -173,16 +235,317 @@ describe('serverSide', function() { }); } + 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-server')).to.not.exist; + }); + + it('should restore on setView'); + describe('view helper', function() { + beforeEach(function() { + window.$serverSide = true; + count = 0; + }); describe('registry', function() { - it('should restore views instantiated through the registry'); - it('should include view args when instantiating view'); - it('should invalidate views with complex args'); - }); - it('should restore named references'); - it('should restore pathed named references'); - it('should restore named references within iterators'); - it('should handle block view helpers'); // {{#view foo}}shit{{/view}} + beforeEach(function() { + Thorax.Views.registry = Thorax.View.extend({ + name: 'registry', + template: function() { + return 'foo ' + count++; + } + }); + }); + + it('should restore 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); + + /* + TODO: This case might be a framework bug. Find out. + view.child.render(); + expect(view.child.$el.html()).to.equal('something'); + */ + }); + + 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); + }); }); describe('collection views', function() { it('should restore inline views'); @@ -190,8 +553,7 @@ describe('serverSide', function() { it('should restore renderItem views'); it('should restore over non-default collection name'); }); - it('should restore helper views'); - it('should replace and log on restore of rendered view'); + it('should cooperate with custom restore events'); }); }); From 0f63f0ef8966e18b6c813a2125bc57bfa96a9cc9 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 7 Jan 2014 14:44:08 -0600 Subject: [PATCH 03/18] Handle rerender cases in restore --- src/thorax.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/thorax.js b/src/thorax.js index 02d07ded..49d21132 100644 --- a/src/thorax.js +++ b/src/thorax.js @@ -167,8 +167,13 @@ Thorax.View = Backbone.View.extend({ }, restore: function(element) { + if (this._renderCount) { + $(element).replaceWith(this.$el); + return; + } + this.setElement(element); - if (!$serverSide && $(element).attr(viewServerAttribute)) { + if (!$serverSide && $(element).attr(viewServerAttribute) === 'true') { this._renderCount = 1; this.trigger('restore'); @@ -233,7 +238,9 @@ Thorax.View = Backbone.View.extend({ if ($serverSide) { - self.$el.attr(viewServerAttribute, $serverSide); + if (self.$el.attr(viewServerAttribute) !== 'false') { + self.$el.attr(viewServerAttribute, $serverSide); + } } else { self.$el.removeAttr(viewServerAttribute); } From 641f5c4c46ec7e501be9e1f3f7f6177cd3b0eba2 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 11 Jan 2014 03:08:46 -0600 Subject: [PATCH 04/18] Implement restore on setView --- src/layout.js | 21 +++++++++++++- test/src/server-side.js | 63 +++++++++++++++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/layout.js b/src/layout.js index e42d5d30..177d0e38 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,19 @@ 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], + used; + if (existing) { + used = view.restore(existing); + } + if (!used) { + $(this._layoutViewEl).empty(); + } + } this.ensureRendered(); + var oldView = this._view, append, remove, complete; if (view === oldView) { return false; @@ -61,6 +77,7 @@ Thorax.LayoutView = Thorax.View.extend({ }, this); complete = _.bind(function() { + this.hasBeenSet = true; this.trigger('change:view:end', view, oldView, options); }, this); @@ -80,6 +97,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/test/src/server-side.js b/test/src/server-side.js index cf8c5403..fe7e44ad 100644 --- a/test/src/server-side.js +++ b/test/src/server-side.js @@ -142,11 +142,19 @@ describe('serverSide', function() { 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(); @@ -250,24 +258,55 @@ describe('serverSide', function() { expect(view.$el.attr('data-view-server')).to.not.exist; }); - it('should restore on setView'); + 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); + }); + it('should restore 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); + }); + it('should restore 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); + }); + }); describe('view helper', function() { beforeEach(function() { window.$serverSide = true; - count = 0; }); describe('registry', function() { - beforeEach(function() { - Thorax.Views.registry = Thorax.View.extend({ - name: 'registry', - template: function() { - return 'foo ' + count++; - } - }); - }); - - it('should restore anonymous block', function() { + it('should rerender anonymous block', function() { var View = Thorax.View.extend({ template: Handlebars.compile('{{#view}}something{{/view}}', {trackIds: true}) }); From 3e142221d4e74ff2bf5e7737c7dd72c68655611b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 15 Jan 2014 01:44:42 -0600 Subject: [PATCH 05/18] Track model id attribute for server-side elements --- src/collection.js | 14 ++++++++++++-- src/data-object.js | 11 +++++++++-- src/model.js | 4 +++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/collection.js b/src/collection.js index 6bc1bb89..e89c98c1 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, @@ -154,6 +158,9 @@ Thorax.CollectionView = Thorax.View.extend({ }); if (model) { + if ($serverSide) { + itemElement.attr(modelIdAttributeName, model.id); + } itemElement.attr(modelCidAttributeName, model.cid); } var previousModel = index > 0 ? collection.at(index - 1) : false; @@ -166,7 +173,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) { 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/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 }); From 6da784170e9063b95fb598ac8ca927a48b7cc48e Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 21 Jan 2014 22:51:09 -0600 Subject: [PATCH 06/18] Handle cid remap in containHandleToCurrentView --- src/event.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/event.js b/src/event.js index 0f822b3f..0e1e4679 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,10 @@ pushDomEvents([ 'focus', 'blur' ]); -function containHandlerToCurentView(handler, cid) { +function containHandlerToCurentView(handler, current) { 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); } From dd0974b09a6e683c4f25d5176215e2cb41d58b8f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sun, 26 Jan 2014 19:08:31 -0600 Subject: [PATCH 07/18] Implement additional collection restore test cases --- test/src/server-side.js | 352 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 341 insertions(+), 11 deletions(-) diff --git a/test/src/server-side.js b/test/src/server-side.js index fe7e44ad..aa740ac9 100644 --- a/test/src/server-side.js +++ b/test/src/server-side.js @@ -104,6 +104,7 @@ describe('serverSide', function() { } }), SomethingElse = Thorax.View.extend({ + _name: 'somethingelse', template: function() { return 'somethingelse'; } }), render, @@ -112,8 +113,8 @@ describe('serverSide', function() { view; function cleanIds(view) { - view.$('[data-view-cid]').each(function() { - $(this).removeAttr('data-view-cid'); + view.$('[data-model-cid]').each(function() { + $(this).removeAttr('data-model-cid'); }); view.$('[data-view-server]').each(function() { $(this).removeAttr('data-view-server'); @@ -508,12 +509,43 @@ describe('serverSide', function() { 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 + }); - /* - TODO: This case might be a framework bug. Find out. - view.child.render(); - expect(view.child.$el.html()).to.equal('something'); - */ + 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() { @@ -585,12 +617,310 @@ describe('serverSide', function() { 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() { - it('should restore inline views'); - it('should restore referenced views'); - it('should restore renderItem views'); - it('should restore over non-default collection name'); + 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 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'); From 935c37fd8d7e178edf050207506c5034c11b029c Mon Sep 17 00:00:00 2001 From: kpdecker Date: Tue, 4 Mar 2014 02:25:28 -0600 Subject: [PATCH 08/18] Reassign cid values after creation on restore --- src/thorax.js | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/thorax.js b/src/thorax.js index 49d21132..c80ca64b 100644 --- a/src/thorax.js +++ b/src/thorax.js @@ -106,10 +106,23 @@ Thorax.View = Backbone.View.extend({ setElement : function() { 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]) { @@ -168,12 +181,24 @@ Thorax.View = Backbone.View.extend({ 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; } + var $element = $(element), + existingCid = $element.attr('data-view-cid'); + if (existingCid) { + this._assignCid(existingCid); + } this.setElement(element); - if (!$serverSide && $(element).attr(viewServerAttribute) === 'true') { + + if (!$serverSide && $element.attr(viewServerAttribute) === 'true') { this._renderCount = 1; this.trigger('restore'); From c37d677f92883b6a74e1486577c7e1994ca13857 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 5 Mar 2014 01:17:27 -0600 Subject: [PATCH 09/18] Force rerender on partial render --- src/thorax.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/thorax.js b/src/thorax.js index c80ca64b..5a5feabc 100644 --- a/src/thorax.js +++ b/src/thorax.js @@ -202,6 +202,14 @@ Thorax.View = Backbone.View.extend({ this._renderCount = 1; this.trigger('restore'); + var remainingViews = this.$('[data-view-server=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(); From d92b75bc1dc03f3e84a6d5087273ccdf8bbd2f3b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 12 Mar 2014 00:27:54 -0500 Subject: [PATCH 10/18] Break helper view init into helper routines --- src/helper-view.js | 111 ++++++++++++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/src/helper-view.js b/src/helper-view.js index 0e17754d..91abab29 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); + helperTemplate(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,20 +99,9 @@ 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 = helperInstance(args, viewOptions, ViewClass); + if (!instance) { + return ''; } instance.$el.attr('data-view-helper-restore', name); @@ -146,12 +121,7 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) { } } - 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'); @@ -181,6 +151,65 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) { return helper; }; +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 helperTemplate(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 helperInstance(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; From 28cb76881bb4abafbb982a0155443c1c7814d78e Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 12 Mar 2014 01:23:05 -0500 Subject: [PATCH 11/18] Implement helper view restore --- src/helper-view.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/helper-view.js b/src/helper-view.js index 91abab29..5a2b344b 100644 --- a/src/helper-view.js +++ b/src/helper-view.js @@ -148,9 +148,68 @@ 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); + helperTemplate(viewOptions, options, ViewClass); + + if (viewOptionWhiteList) { + _.each(viewOptionWhiteList, function(dest, source) { + if (!_.isUndefined(attrs[source])) { + viewOptions[dest] = attrs[source]; + } + }); + } + + var instance = helperInstance(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-server') === '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, From 14d4f6323ad693f66848dbc4fd44d5c5fe3963ff Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 12 Mar 2014 04:29:34 -0500 Subject: [PATCH 12/18] Implement collection restore --- src/collection.js | 75 ++++++++++++++++++++++++++++++++++++--- src/helper-view.js | 2 +- src/helpers/collection.js | 14 ++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/collection.js b/src/collection.js index e89c98c1..bd44563f 100644 --- a/src/collection.js +++ b/src/collection.js @@ -111,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) @@ -158,10 +203,10 @@ Thorax.CollectionView = Thorax.View.extend({ }); if (model) { - if ($serverSide) { - itemElement.attr(modelIdAttributeName, model.id); - } - 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) { @@ -268,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', { @@ -304,6 +357,18 @@ Thorax.CollectionView = Thorax.View.extend({ return this.renderTemplate(this.itemTemplate, this.itemContext.call(this, model, i)); } }, + restoreItem: function(model, i, el) { + if (this.itemView || this.renderItem !== Thorax.CollectionView.prototype.renderItem) { + var child = this.renderItem(model, i); + el.setAttribute(modelCidAttributeName, model.cid); + + child.restore(el); + this._addChild(child); + return child; + } else { + el.setAttribute(modelCidAttributeName, model.cid); + } + }, itemContext: function(model /*, i */) { return model.attributes; }, diff --git a/src/helper-view.js b/src/helper-view.js index 5a2b344b..7e486c92 100644 --- a/src/helper-view.js +++ b/src/helper-view.js @@ -106,7 +106,7 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) { instance.$el.attr('data-view-helper-restore', name); - if ($serverSide) { + if ($serverSide && instance.$el.attr('data-view-server') !== 'false') { try { ServerMarshal.store(instance.$el, 'args', args, options.ids, options); ServerMarshal.store(instance.$el, 'attrs', options.hash, options.hashIds, options); diff --git a/src/helpers/collection.js b/src/helpers/collection.js index 2dfadf68..e5ab6181 100644 --- a/src/helpers/collection.js +++ b/src/helpers/collection.js @@ -19,6 +19,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 +35,18 @@ Thorax.CollectionHelperView = Thorax.CollectionView.extend({ if (!options.itemTemplate && options.template && options.template !== Handlebars.VM.noop) { options.itemTemplate = options.template; options.template = Handlebars.VM.noop; + + 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 +82,10 @@ Thorax.CollectionHelperView = Thorax.CollectionView.extend({ } } + if (!restorable) { + this.$el.attr('data-view-server', 'false'); + } + return response; }, setAsPrimaryCollectionHelper: function() { From e99ef858dfcb0b538b273c3ca16c8475f304ee90 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 12 Mar 2014 04:46:08 -0500 Subject: [PATCH 13/18] Ensure view reuse on parent rerender --- src/helper-view.js | 4 +++- test/src/server-side.js | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/helper-view.js b/src/helper-view.js index 7e486c92..3872ce7c 100644 --- a/src/helper-view.js +++ b/src/helper-view.js @@ -299,6 +299,8 @@ 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'); + ret.fn = ret.fn || undefined; + ret.inverse = ret.inverse || undefined; return ret; } @@ -334,7 +336,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/test/src/server-side.js b/test/src/server-side.js index aa740ac9..dd17d532 100644 --- a/test/src/server-side.js +++ b/test/src/server-side.js @@ -924,5 +924,29 @@ describe('serverSide', function() { }); it('should cooperate with custom restore events'); + 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]); + }); }); }); From 8a84829e3d9a1cff01112cc53b4b5011d0f569f7 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 12 Mar 2014 16:36:38 -0500 Subject: [PATCH 14/18] Fix improper cleanup on setView rerender --- src/layout.js | 8 +++----- test/src/server-side.js | 7 +++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/layout.js b/src/layout.js index 177d0e38..354ed0ec 100644 --- a/src/layout.js +++ b/src/layout.js @@ -28,12 +28,10 @@ Thorax.LayoutView = Thorax.View.extend({ } if (!$serverSide && !this.hasBeenSet) { - var existing = this.$('[' + viewNameAttributeName + '="' + view.name + '"]')[0], - used; + var existing = this.$('[' + viewNameAttributeName + '="' + view.name + '"]')[0]; if (existing) { - used = view.restore(existing); - } - if (!used) { + view.restore(existing); + } else { $(this._layoutViewEl).empty(); } } diff --git a/test/src/server-side.js b/test/src/server-side.js index dd17d532..121ffacc 100644 --- a/test/src/server-side.js +++ b/test/src/server-side.js @@ -281,8 +281,9 @@ describe('serverSide', function() { 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 restore non-matching views', function() { + it('should rerender non-matching views', function() { var $el = $('
winning
'); layout.$el.append($el); @@ -290,8 +291,9 @@ describe('serverSide', function() { 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 restore non-server views', function() { + it('should rerender non-server views', function() { var $el = $('
winning
'); layout.$el.append($el); @@ -299,6 +301,7 @@ describe('serverSide', function() { 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'); }); }); From 33cdf17183a73780215e240ff60319faa0ce40db Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 12 Mar 2014 23:13:38 -0500 Subject: [PATCH 15/18] Improve API for custom restore methods --- src/thorax.js | 14 ++++++++------ test/src/server-side.js | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/thorax.js b/src/thorax.js index 5a5feabc..7088ae6e 100644 --- a/src/thorax.js +++ b/src/thorax.js @@ -104,7 +104,13 @@ Thorax.View = Backbone.View.extend({ }, - 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); // Use a hash here to avoid multiple DOM operations @@ -191,13 +197,9 @@ Thorax.View = Backbone.View.extend({ return; } - var $element = $(element), - existingCid = $element.attr('data-view-cid'); - if (existingCid) { - this._assignCid(existingCid); - } this.setElement(element); + var $element = $(element); if (!$serverSide && $element.attr(viewServerAttribute) === 'true') { this._renderCount = 1; this.trigger('restore'); diff --git a/test/src/server-side.js b/test/src/server-side.js index 121ffacc..e3d336a4 100644 --- a/test/src/server-side.js +++ b/test/src/server-side.js @@ -926,7 +926,42 @@ describe('serverSide', function() { }); }); - it('should cooperate with custom restore events'); + 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; From f3db3c11108fd633c194fb12c0dc527a732c5ec8 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 13 Mar 2014 13:15:06 -0500 Subject: [PATCH 16/18] Add test to cover implicit collection view/templ --- test/src/server-side.js | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/src/server-side.js b/test/src/server-side.js index e3d336a4..dded3ff3 100644 --- a/test/src/server-side.js +++ b/test/src/server-side.js @@ -704,6 +704,59 @@ describe('serverSide', function() { 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}), From e7472c53d1564586e3ca121573079b6125a0167f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 17 Mar 2014 15:50:05 -0500 Subject: [PATCH 17/18] Simplify collection.restoreItem --- src/collection.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/collection.js b/src/collection.js index bd44563f..f2698a94 100644 --- a/src/collection.js +++ b/src/collection.js @@ -358,15 +358,15 @@ Thorax.CollectionView = Thorax.View.extend({ } }, 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); - el.setAttribute(modelCidAttributeName, model.cid); child.restore(el); this._addChild(child); - return child; - } else { - el.setAttribute(modelCidAttributeName, model.cid); } }, itemContext: function(model /*, i */) { From 5d722cd611e50b4567732ef9242c2d6000fb8610 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Mon, 17 Mar 2014 17:06:29 -0500 Subject: [PATCH 18/18] Update docs/naming per review --- src/event.js | 2 ++ src/helper-view.js | 22 +++++++++++++--------- src/helpers/collection.js | 10 +++++++--- src/thorax.js | 18 ++++++++++++------ test/src/server-side.js | 28 ++++++++++++++-------------- 5 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/event.js b/src/event.js index 0e1e4679..72c8d6eb 100644 --- a/src/event.js +++ b/src/event.js @@ -169,6 +169,8 @@ pushDomEvents([ ]); 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 === current.cid) { diff --git a/src/helper-view.js b/src/helper-view.js index 3872ce7c..46f47251 100644 --- a/src/helper-view.js +++ b/src/helper-view.js @@ -61,7 +61,7 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) { var expandTokens = expandHash(this, options.hash); var viewOptions = createViewOptions(name, args, options, declaringView); - helperTemplate(viewOptions, options, ViewClass); + setHelperTemplate(viewOptions, options, ViewClass); normalizeHTMLAttributeOptions(options.hash); var htmlAttributes = _.clone(options.hash); @@ -99,14 +99,14 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) { // Create the instance if we don't already have one if (!instance) { - instance = helperInstance(args, viewOptions, ViewClass); + instance = getHelperInstance(args, viewOptions, ViewClass); if (!instance) { return ''; } instance.$el.attr('data-view-helper-restore', name); - if ($serverSide && instance.$el.attr('data-view-server') !== 'false') { + 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); @@ -117,7 +117,7 @@ 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'); } } @@ -167,7 +167,7 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) { } var viewOptions = createViewOptions(name, args, options, declaringView); - helperTemplate(viewOptions, options, ViewClass); + setHelperTemplate(viewOptions, options, ViewClass); if (viewOptionWhiteList) { _.each(viewOptionWhiteList, function(dest, source) { @@ -177,7 +177,7 @@ Handlebars.registerViewHelper = function(name, ViewClass, callback) { }); } - var instance = helperInstance(args, viewOptions, ViewClass); + var instance = getHelperInstance(args, viewOptions, ViewClass); instance._assignCid(el.getAttribute('data-view-cid')); helperInit(args, instance, callback, viewOptions); @@ -202,7 +202,7 @@ Thorax.View.on('restore', function() { } parent.$('[data-view-helper-restore]').each(filterAncestors(parent, function() { - if (this.getAttribute('data-view-server') === 'true') { + 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); @@ -224,7 +224,7 @@ function createViewOptions(name, args, options, declaringView) { }; } -function helperTemplate(viewOptions, options, ViewClass) { +function setHelperTemplate(viewOptions, options, ViewClass) { if (options.fn) { // Only assign if present, allow helper view class to // declare template @@ -236,7 +236,7 @@ function helperTemplate(viewOptions, options, ViewClass) { } } -function helperInstance(args, viewOptions, ViewClass) { +function getHelperInstance(args, viewOptions, ViewClass) { var instance; if (ViewClass.factory) { @@ -299,8 +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; } diff --git a/src/helpers/collection.js b/src/helpers/collection.js index e5ab6181..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({ @@ -36,6 +38,8 @@ Thorax.CollectionHelperView = Thorax.CollectionView.extend({ 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; } @@ -82,8 +86,8 @@ Thorax.CollectionHelperView = Thorax.CollectionView.extend({ } } - if (!restorable) { - this.$el.attr('data-view-server', 'false'); + if ($serverSide && !restorable) { + this.$el.attr(viewRestoreAttribute, 'false'); } return response; diff --git a/src/thorax.js b/src/thorax.js index 7088ae6e..9ea77e99 100644 --- a/src/thorax.js +++ b/src/thorax.js @@ -26,7 +26,13 @@ if (!$.fn.forEach) { var viewNameAttributeName = 'data-view-name', viewCidAttributeName = 'data-view-cid', viewHelperAttributeName = 'data-view-helper', - viewServerAttribute = 'data-view-server'; + + // 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 = {}; @@ -200,11 +206,11 @@ Thorax.View = Backbone.View.extend({ this.setElement(element); var $element = $(element); - if (!$serverSide && $element.attr(viewServerAttribute) === 'true') { + if (!$serverSide && $element.attr(viewRestoreAttribute) === 'true') { this._renderCount = 1; this.trigger('restore'); - var remainingViews = this.$('[data-view-server=false]'), + var remainingViews = this.$('[data-view-restore=false]'), rerender = _.any(remainingViews, function(el) { return $(el).parent().view({el: true, helper: true})[0] === $element[0]; }); @@ -273,11 +279,11 @@ Thorax.View = Backbone.View.extend({ if ($serverSide) { - if (self.$el.attr(viewServerAttribute) !== 'false') { - self.$el.attr(viewServerAttribute, $serverSide); + if (self.$el.attr(viewRestoreAttribute) !== 'false') { + self.$el.attr(viewRestoreAttribute, $serverSide); } } else { - self.$el.removeAttr(viewServerAttribute); + self.$el.removeAttr(viewRestoreAttribute); } //accept a view, string, Handlebars.SafeString or DOM element diff --git a/test/src/server-side.js b/test/src/server-side.js index dded3ff3..f5093301 100644 --- a/test/src/server-side.js +++ b/test/src/server-side.js @@ -83,12 +83,12 @@ describe('serverSide', 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-server')).to.equal('true'); + 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-server')).to.not.exist; + expect(view.$el.attr('data-view-restore')).to.not.exist; }); }); @@ -116,8 +116,8 @@ describe('serverSide', function() { view.$('[data-model-cid]').each(function() { $(this).removeAttr('data-model-cid'); }); - view.$('[data-view-server]').each(function() { - $(this).removeAttr('data-view-server'); + view.$('[data-view-restore]').each(function() { + $(this).removeAttr('data-view-restore'); }); view.$('[data-server-data]').each(function() { $(this).removeAttr('data-server-data'); @@ -162,7 +162,7 @@ describe('serverSide', function() { }); it('should restore views explicitly', function() { - var el = $('
bar
'); + var el = $('
bar
'); fixture.append(el); var view = new Thorax.View({ @@ -173,7 +173,7 @@ describe('serverSide', function() { expect(el.html()).to.equal('bar'); el.remove(); - el = $('
bar
'); + el = $('
bar
'); fixture.append(el); view = new Thorax.View({ el: function() { @@ -200,7 +200,7 @@ describe('serverSide', function() { }); it('should restore views with a passed el', function() { - var el = $('
bar
'); + var el = $('
bar
'); fixture.append(el); var view = new Thorax.View({}); @@ -212,7 +212,7 @@ describe('serverSide', function() { }); it('should update view attributes on restore', function() { - var el = $('
bar
'); + var el = $('
bar
'); fixture.append(el); var spy = this.spy(); @@ -228,7 +228,7 @@ describe('serverSide', function() { if (!$serverSide) { it('should restore DOM events', function() { - var el = $('
bar
'); + var el = $('
bar
'); fixture.append(el); var spy = this.spy(); @@ -245,7 +245,7 @@ describe('serverSide', function() { } it('should replace restore of rendered view', function() { - var el = $('
bar
'); + var el = $('
bar
'); fixture.append(el); var view = new Thorax.View({}); @@ -256,7 +256,7 @@ describe('serverSide', function() { 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-server')).to.not.exist; + expect(view.$el.attr('data-view-restore')).to.not.exist; }); describe('setView', function() { @@ -274,7 +274,7 @@ describe('serverSide', function() { }); }); it('should restore matching views', function() { - var $el = $('
winning
'); + var $el = $('
winning
'); layout.$el.append($el); layout.setView(view); @@ -284,7 +284,7 @@ describe('serverSide', function() { expect(layout.$el.text()).to.equal('winning'); }); it('should rerender non-matching views', function() { - var $el = $('
winning
'); + var $el = $('
winning
'); layout.$el.append($el); layout.setView(view); @@ -294,7 +294,7 @@ describe('serverSide', function() { expect(layout.$el.text()).to.equal('not winning'); }); it('should rerender non-server views', function() { - var $el = $('
winning
'); + var $el = $('
winning
'); layout.$el.append($el); layout.setView(view);