From a8746dd3e1e4955c0e505d8b89ed3c5d0a99fd7a Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Thu, 12 Mar 2015 21:31:35 -0700 Subject: [PATCH] Upgrade htmlbars to 0.11.1, re-wrote each helper and added basic tests for it. --- Gulpfile.js | 3 +- package.json | 4 +- .../rebound-compiler/lib/rebound-compiler.js | 2 +- packages/rebound-component/lib/component.js | 2 +- packages/rebound-component/lib/helpers.js | 71 ++++----- packages/rebound-component/lib/hooks.js | 8 +- packages/rebound-component/lib/lazy-value.js | 4 +- .../test/rebound_helpers_test.js | 138 ++++++++++++++---- .../test/rebound_precompiler_test.js | 2 +- test/demo/demo.html | 3 + 10 files changed, 149 insertions(+), 88 deletions(-) diff --git a/Gulpfile.js b/Gulpfile.js index 1022878..cc2974f 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -124,7 +124,8 @@ gulp.task('runtime', ['amd'], function() { 'wrap/start.frag', 'bower_components/almond/almond.js', 'node_modules/htmlbars/dist/amd/htmlbars-util.amd.js', - 'node_modules/htmlbars/dist/amd/morph.amd.js', + 'node_modules/htmlbars/dist/amd/morph-attr.amd.js', + 'node_modules/htmlbars/dist/amd/dom-helper.amd.js', 'dist/rebound.runtime.js', 'wrap/end.runtime.frag' ]) diff --git a/package.json b/package.json index 8544afd..7718d65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reboundjs", - "version": "0.0.57", + "version": "0.0.58", "description": "Automatic data binding for Backbone using HTMLBars.", "main": "dist/cjs/rebound-precompiler/rebound-precompiler.js", "scripts": { @@ -22,7 +22,7 @@ "package.json" ], "dependencies": { - "htmlbars": "^0.8.3" + "htmlbars": "^0.11.1" }, "devDependencies": { "bower": "~1.3.3", diff --git a/packages/rebound-compiler/lib/rebound-compiler.js b/packages/rebound-compiler/lib/rebound-compiler.js index a613caa..8eee206 100644 --- a/packages/rebound-compiler/lib/rebound-compiler.js +++ b/packages/rebound-compiler/lib/rebound-compiler.js @@ -3,7 +3,7 @@ import { compile as htmlbarsCompile, compileSpec as htmlbarsCompileSpec } from "htmlbars-compiler/compiler"; import { merge } from "htmlbars-util/object-utils"; -import DOMHelper from "morph/dom-helper"; +import DOMHelper from "dom-helper"; import helpers from "rebound-component/helpers"; import hooks from "rebound-component/hooks"; diff --git a/packages/rebound-component/lib/component.js b/packages/rebound-component/lib/component.js index bfc832d..5c1daa9 100644 --- a/packages/rebound-component/lib/component.js +++ b/packages/rebound-component/lib/component.js @@ -1,7 +1,7 @@ // Rebound Component // ---------------- -import DOMHelper from "morph/dom-helper"; +import DOMHelper from "dom-helper"; import hooks from "rebound-component/hooks"; import helpers from "rebound-component/helpers"; import $ from "rebound-component/utils"; diff --git a/packages/rebound-component/lib/helpers.js b/packages/rebound-component/lib/helpers.js index 2b05e5c..eca2dc9 100644 --- a/packages/rebound-component/lib/helpers.js +++ b/packages/rebound-component/lib/helpers.js @@ -201,64 +201,43 @@ helpers.each = function(params, hash, options, env){ if(_.isNull(params[0]) || _.isUndefined(params[0])){ console.warn('Undefined value passed to each helper! Maybe try providing a default value?', options.context); return null; } var value = (params[0].isCollection) ? params[0].models : params[0], // Accepts collections or arrays - start, end, // used below to remove trailing junk morphs from the dom + morph = options.placeholder.firstChildMorph, obj, next, lazyValue, nmorph, i, // used below to remove trailing junk morphs from the dom position, // Stores the iterated element's integer position in the dom list currentModel = function(element, index, array, cid){ return element.cid === cid; // Returns true if currently observed element is the current model. }; - // Create our morph array if it doesnt exist - options.placeholder.morphs = options.placeholder.morphs || []; + // For each item in this collection + for(i=0;i < value.length;i++){ + obj = value[i]; + next = (morph) ? morph.nextMorph : null; - _.each(value, function(obj, key, list){ + // If this morph is the rendered version of this model, continue to the next one. + if(morph && morph.cid == obj.cid){ morph = next; continue; } - if(!_.isFunction(obj.set)){ return console.error('Model ', obj, 'has no method .set()!'); } + // Create a lazyvalue whos value is the content inside our block helper rendered in the context of this current list object. Returns the rendered dom for this list item. + lazyValue = new LazyValue(function(){ + return options.template.render(obj, options, options.placeholder.contextualElement, [obj]); + }, {morph: options.placeholder}); - position = findIndex(options.placeholder.morphs, currentModel, obj.cid); + // Insert our newly rendered value (a document tree) into our placeholder (the containing element) at its requested position (where we currently are in the object list) + nmorph = options.placeholder.insertContentBeforeMorph(lazyValue.value(), morph); - // TODO: These need to be re-added in as data attributes - // Even if rendered already, update each element's index, key, first and last in case of order changes or element removals - // if(_.isArray(value)){ - // obj.set({'@index': key, '@first': (key === 0), '@last': (key === value.length-1)}, {silent: true}); - // } - // - // if(!_.isArray(value) && _.isObject(value)){ - // obj.set({'@key' : key}, {silent: true}); - // } + // Label the inserted morph element with this model's cid + nmorph.cid = obj.cid; - // If this model is not the morph element at this index - if(position !== key){ + // Destroy the old morph that was here + morph && morph.destroy(); - // Create a lazyvalue whos value is the content inside our block helper rendered in the context of this current list object. Returns the rendered dom for this list element. - var lazyValue = new LazyValue(function(){ - return options.template.render(((options.template.blockParams === 0)?obj:options.context), options, (options.morph.contextualElement || options.morph.element), [obj]); - }, {morph: options.placeholder}); - - // If this model is rendered somewhere else in the list, destroy it - if(position > -1){ - options.placeholder.morphs[position].destroy(); - } - - // Destroy the morph we're replacing - if(options.placeholder.morphs[key]){ - options.placeholder.morphs[key].destroy(); - } - - // Insert our newly rendered value (a document tree) into our placeholder (the containing element) at its requested position (where we currently are in the object list) - options.placeholder.insert(key, lazyValue.value()); - - // Label the inserted morph element with this model's cid - options.placeholder.morphs[key].cid = obj.cid; - - } - - }, this); + // Move on to the next morph + morph = next; + } - // If any more morphs are left over, remove them. We've already gone through all the models. - start = value.length; - end = options.placeholder.morphs.length - 1; - for(end; start <= end; end--){ - options.placeholder.morphs[end].destroy(); + // // If any more morphs are left over, remove them. We've already gone through all the models. + while(morph){ + next = morph.nextMorph; + morph.destroy(); + morph = next; } // Return null prevent's re-rending of our placeholder. Our placeholder (containing element) now has all the dom we need. diff --git a/packages/rebound-component/lib/hooks.js b/packages/rebound-component/lib/hooks.js index a80e37e..5ac90a1 100644 --- a/packages/rebound-component/lib/hooks.js +++ b/packages/rebound-component/lib/hooks.js @@ -227,7 +227,7 @@ hooks.block = function block(env, morph, context, path, params, hash, template, value = lazyValue.value(); value = (_.isUndefined(value)) ? '' : value; - if(!_.isNull(value)){ morph.append(value); } + if(!_.isNull(value)){ morph.appendContent(value); } // Observe this content morph's parent's children. // When the morph element's containing element (morph) is removed, clean up the lazyvalue. @@ -272,7 +272,7 @@ hooks.inline = function inline(env, morph, context, path, params, hash) { value = lazyValue.value(); value = (_.isUndefined(value)) ? '' : value; - if(!_.isNull(value)){ morph.append(value); } + if(!_.isNull(value)){ morph.appendContent(value); } // Observe this content morph's parent's children. // When the morph element's containing element (morph) is removed, clean up the lazyvalue. @@ -311,7 +311,7 @@ hooks.content = function content(env, morph, context, path) { value = lazyValue.value(); value = (_.isUndefined(value)) ? '' : value; - if(!_.isNull(value)){ morph.append(value); } + if(!_.isNull(value)){ morph.appendContent(value); } // Observe this content morph's parent's children. // When the morph element's containing element (morph) is removed, clean up the lazyvalue. @@ -555,7 +555,7 @@ hooks.component = function(env, morph, context, tagName, contextData, template) }); value = lazyValue.value(); - if(value !== undefined){ morph.append(value); } + if(value !== undefined){ morph.appendContent(value); } } }; diff --git a/packages/rebound-component/lib/lazy-value.js b/packages/rebound-component/lib/lazy-value.js index 1704cc2..f9f95c2 100644 --- a/packages/rebound-component/lib/lazy-value.js +++ b/packages/rebound-component/lib/lazy-value.js @@ -89,10 +89,10 @@ LazyValue.prototype = { }, notify: function(sender) { - // TODO: This check won't be necessary once removed DOM has been cleaned of any bindings. + // TODO: This check won't be necessary once removed DOM has been cleaned of any bindings. // If this lazyValue's morph does not have an immediate parentNode, it has been removed from the dom tree. Destroy it. // Right now, DOM that contains morphs throw an error if it is removed by another lazyvalue before those morphs re-evaluate. - if(this.morph && this.morph.start && !this.morph.start.parentNode) return this.destroy(); + if(this.morph && this.morph.firstNode && !this.morph.firstNode.parentNode) return this.destroy(); var cache = this.cache, parent, subscribers; diff --git a/packages/rebound-component/test/rebound_helpers_test.js b/packages/rebound-component/test/rebound_helpers_test.js index 14c2051..9600eb2 100644 --- a/packages/rebound-component/test/rebound_helpers_test.js +++ b/packages/rebound-component/test/rebound_helpers_test.js @@ -132,10 +132,10 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- dom = template.render(data); data.bar = 'bar'; notify(data, ['bar']); - equal(dom.value, 'bar', 'Value of text input is two way data bound data -> element'); + equal(dom.firstChild.value, 'bar', 'Value of text input is two way data bound data -> element'); equalTokens(dom, '', 'Value Attribute on text input is two way data bound element -> data'); - dom.value = 'Hello World'; - dom.dispatchEvent(evt); + dom.firstChild.value = 'Hello World'; + dom.firstChild.dispatchEvent(evt); notify(data, ['bar']); equal(data.bar, 'Hello World', 'Value on text input is two way data bound element -> data'); @@ -146,10 +146,10 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- dom = template.render(data); data.bar = 'bar'; notify(data, ['bar']); - equal(dom.value, 'bar', 'Value of email input is two way data bound data -> element'); + equal(dom.firstChild.value, 'bar', 'Value of email input is two way data bound data -> element'); equalTokens(dom, '', 'Value Attribute on email input is two way data bound element -> data'); - dom.value = 'Hello World'; - dom.dispatchEvent(evt); + dom.firstChild.value = 'Hello World'; + dom.firstChild.dispatchEvent(evt); notify(data, ['bar']); equal(data.bar, 'Hello World', 'Value on email input is two way data bound element -> data'); @@ -160,10 +160,10 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- dom = template.render(data); data.bar = 'bar'; notify(data, ['bar']); - equal(dom.value, 'bar', 'Value of password input is two way data bound data -> element'); + equal(dom.firstChild.value, 'bar', 'Value of password input is two way data bound data -> element'); equalTokens(dom, '', 'Value Attribute on password input is two way data bound element -> data'); - dom.value = 'Hello World'; - dom.dispatchEvent(evt); + dom.firstChild.value = 'Hello World'; + dom.firstChild.dispatchEvent(evt); notify(data, ['bar']); equal(data.bar, 'Hello World', 'Value on password input is two way data bound element -> data'); @@ -174,10 +174,10 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- dom = template.render(data); data.bar = 'bar'; notify(data, ['bar']); - equal(dom.value, 'bar', 'Value of search input is two way data bound data -> element'); + equal(dom.firstChild.value, 'bar', 'Value of search input is two way data bound data -> element'); equalTokens(dom, '', 'Value Attribute on search input is two way data bound element -> data'); - dom.value = 'Hello World'; - dom.dispatchEvent(evt); + dom.firstChild.value = 'Hello World'; + dom.firstChild.dispatchEvent(evt); notify(data, ['bar']); equal(data.bar, 'Hello World', 'Value on search input is two way data bound element -> data'); @@ -188,10 +188,10 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- dom = template.render(data); data.bar = 'bar'; notify(data, ['bar']); - equal(dom.value, 'bar', 'Value of url input is two way data bound data -> element'); + equal(dom.firstChild.value, 'bar', 'Value of url input is two way data bound data -> element'); equalTokens(dom, '', 'Value Attribute on url input is two way data bound element -> data'); - dom.value = 'Hello World'; - dom.dispatchEvent(evt); + dom.firstChild.value = 'Hello World'; + dom.firstChild.dispatchEvent(evt); notify(data, ['bar']); equal(data.bar, 'Hello World', 'Value on url input is two way data bound element -> data'); @@ -202,10 +202,10 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- dom = template.render(data); data.bar = 'bar'; notify(data, ['bar']); - equal(dom.value, 'bar', 'Value of tel input is two way data bound data -> element'); + equal(dom.firstChild.value, 'bar', 'Value of tel input is two way data bound data -> element'); equalTokens(dom, '', 'Value Attribute on tel input is two way data bound element -> data'); - dom.value = 'Hello World'; - dom.dispatchEvent(evt); + dom.firstChild.value = 'Hello World'; + dom.firstChild.dispatchEvent(evt); notify(data, ['bar']); equal(data.bar, 'Hello World', 'Value on tel input is two way data bound element -> data'); @@ -301,7 +301,7 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- template = compiler.compile('
{{#if bool}}{{foo}}{{/if}}
', {name: 'test/partial'}); dom = template.render({foo:'bar', bar:'foo', bool: false}); - equalTokens(dom, '
', 'Block If helper without else block - false'); + equalTokens(dom, '
', 'Block If helper without else block - false'); @@ -346,7 +346,7 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- template = compiler.compile('
{{if bool foo}}
', {name: 'test/partial'}); dom = template.render({foo:'bar', bar:'foo', bool: false}); - equalTokens(dom, '
', 'Inline If helper in content without else term - false'); + equalTokens(dom, '
', 'Inline If helper in content without else term - false'); @@ -392,10 +392,10 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- template = compiler.compile('
{{#if bool}}{{#if bool}}{{val}}{{else}}{{val2}}{{/if}}{{else}}{{val2}}{{/if}}
', {name: 'test/partial'}); data = new Model({bool: true, val: 'true', val2: 'false'}); dom = template.render(data); - equal(dom.innerHTML, 'true', 'If helpers that are the immediate children of if helpers render on first run.'); + equal(dom.firstChild.innerHTML, 'true', 'If helpers that are the immediate children of if helpers render on first run.'); data.set('bool', false); notify(data, 'bool'); - equal(dom.innerHTML, 'false', 'If helpers that are the immediate children of if helpers re-render successfully on change.'); + equal(dom.firstChild.innerHTML, 'false', 'If helpers that are the immediate children of if helpers re-render successfully on change.'); @@ -465,7 +465,7 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- template = compiler.compile('
{{unless bool foo}}
', {name: 'test/partial'}); dom = template.render({foo:'bar', bar:'foo', bool: true}); - equalTokens(dom, '
', 'Inline Unless helper in content without else term - true'); + equalTokens(dom, '
', 'Inline Unless helper in content without else term - true'); @@ -552,7 +552,7 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- dom = template.render(data, {helpers: {__callOnComponent: function(name, event){ return data[name].call(data, event); }}}); - $(dom).trigger('click'); + $(dom.firstChild).trigger('click'); /*******************************************************************/ @@ -570,14 +570,96 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- var template, data, dom; + // End Modifications template = compiler.compile('
{{#each arr}}{{val}}{{/each}}
', {name: 'test/partial'}); data = new Model({arr: [{val: 1}, {val: 2}, {val: 3}]}); dom = template.render(data); - equal(dom.innerHTML, '123', 'Each helper will render a list of values.'); + equal(dom.firstChild.innerHTML, '123', 'Each helper will render a list of values.'); + data.get('arr').add({val: 4}); notify(data, 'arr'); - equal(dom.innerHTML, '1234', 'Each helper will re-render on add.'); + equal(dom.firstChild.innerHTML, '1234', 'Each helper will re-render on add to end.'); + + data.get('arr').pop(); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '123', 'Each helper will re-render on remove from end.'); + + + data.get('arr').add([{val: 4}, {val: 5}]); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '12345', 'Each helper will re-render on multiple add to end.'); + + var removeArr = []; + removeArr.push(data.get('arr[3]')); + removeArr.push(data.get('arr[4]')) + + data.get('arr').remove(removeArr); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '123', 'Each helper will re-render on multiple remove from end.'); + + // Begining Modification + + data.get('arr').unshift({val: 4}); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '4123', 'Each helper will re-render on add to begining.'); + + data.get('arr').shift(); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '123', 'Each helper will re-render on remove from begining.'); + + + data.get('arr').unshift([{val: 4}, {val: 5}]); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '45123', 'Each helper will re-render on multiple add to begining.'); + + removeArr = []; + removeArr.push(data.get('arr[0]')); + removeArr.push(data.get('arr[1]')) + + data.get('arr').remove(removeArr); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '123', 'Each helper will re-render on multiple remove from begining.'); + + // Middle Modifications + + data.get('arr').add({val: 4}, {at: 2}); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '1243', 'Each helper will re-render on add to middle.'); + + data.get('arr').remove(data.get('arr[2]')); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '123', 'Each helper will re-render on remove from middle.'); + + + data.get('arr').add([{val: 4}, {val: 5}], {at: 2}); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '12453', 'Each helper will re-render on multiple add to middle.'); + + removeArr = []; + removeArr.push(data.get('arr[2]')); + removeArr.push(data.get('arr[3]')) + + data.get('arr').remove(removeArr); + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '123', 'Each helper will re-render on multiple remove from middle.'); + + // Multiple Modifications + + data.get('arr').add({val: 'a'}, {at: 0}); + data.get('arr').add({val: 'b'}, {at: 3}); + data.get('arr').add({val: 'c'}, {at: 5}); + + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, 'a12b3c', 'Each helper will re-render after multiple adds.'); + + data.get('arr').remove(data.get('arr[0]')); + data.get('arr').remove(data.get('arr[2]')); + data.get('arr').remove(data.get('arr[3]')); + + notify(data, 'arr'); + equal(dom.firstChild.innerHTML, '123', 'Each helper will re-render after multiple removes.'); + }); @@ -585,8 +667,4 @@ require(['rebound-compiler/rebound-compiler', 'simple-html-tokenizer', 'rebound- // TODO: Add length helper tests - // TODO: Add on helper tests - - // TODO: Computed properties passed to helpers evaluate properly (specifically they break lazyvalue caches when their dependancies re evaluate) - }); diff --git a/packages/rebound-precompiler/test/rebound_precompiler_test.js b/packages/rebound-precompiler/test/rebound_precompiler_test.js index f09b395..79c3578 100644 --- a/packages/rebound-precompiler/test/rebound_precompiler_test.js +++ b/packages/rebound-precompiler/test/rebound_precompiler_test.js @@ -34,7 +34,7 @@ require(['rebound-precompiler/rebound-precompiler'], function(compiler){ var out, exp; out = compiler.precompile('
', {name: 'test/filepath'}).replace(/(\r\n|\n|\r)/gm," ").replace(/\s+/g," "); - exp = 'define( [], function(){ (function(){var template = (function() { return { isHTMLBars: true, blockParams: 0, cachedFragment: null, hasRendered: false, build: function build(dom) { var el0 = dom.createElement(\"div\"); return el0; }, render: function render(context, env, contextualElement) { var dom = env.dom; dom.detectNamespace(contextualElement); var fragment; if (env.useFragmentCache && dom.canClone) { if (this.cachedFragment === null) { fragment = this.build(dom); if (this.hasRendered) { this.cachedFragment = fragment; } else { this.hasRendered = true; } } if (this.cachedFragment) { fragment = dom.cloneNode(this.cachedFragment, true); } } else { fragment = this.build(dom); } return fragment; } }; }()) window.Rebound.registerPartial( \"test/filepath\", template);})(); });'; + exp = 'define( [], function(){ (function(){var template = (function() { return { isHTMLBars: true, revision: \"HTMLBars@VERSION_STRING_PLACEHOLDER\", blockParams: 0, cachedFragment: null, hasRendered: false, build: function build(dom) { var el0 = dom.createDocumentFragment(); var el1 = dom.createElement(\"div\"); dom.appendChild(el0, el1); return el0; }, render: function render(context, env, contextualElement) { var dom = env.dom; dom.detectNamespace(contextualElement); var fragment; if (env.useFragmentCache && dom.canClone) { if (this.cachedFragment === null) { fragment = this.build(dom); if (this.hasRendered) { this.cachedFragment = fragment; } else { this.hasRendered = true; } } if (this.cachedFragment) { fragment = dom.cloneNode(this.cachedFragment, true); } } else { fragment = this.build(dom); } return fragment; } }; }()) window.Rebound.registerPartial( \"test/filepath\", template);})(); });'; equal(out, exp, 'Pre-compiler can handle partials'); diff --git a/test/demo/demo.html b/test/demo/demo.html index 3ab315f..7510ca4 100644 --- a/test/demo/demo.html +++ b/test/demo/demo.html @@ -173,6 +173,9 @@

{{firstTodo.title}}

}, filterList: function(filter){ this.set('filter', filter) + }, + test: function(event){ + } }); \ No newline at end of file