From ba63f61fc9ed0a1219f9aa08da13833a73a90595 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Mon, 2 Feb 2015 17:44:26 -0800 Subject: [PATCH 01/27] Add ability for templates to reuse morphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, if you wanted to re-render an HTMLBars template, the template would create a brand new clone of the cached fragment and create a new set of morphs. This resulted in unnecessary work, since the static portions of the rendered DOM already exist, and do not need to be re-cloned and re-inserted into DOM. This change begins the process of allowing an HTMLBars template to update the output of a previous render, rather than starting from scratch. It introduces the concept of a (very WIP atm) “result” of a previous render, which is passed back in to subsequent renders. At the moment, the “result” is the fragment and morphs of the previous render. Soon, it will become a more opaque data structure that includes the results of sub-programs. --- .../lib/hydration-javascript-compiler.js | 40 ++-- .../lib/hydration-opcode-compiler.js | 4 +- .../lib/template-compiler.js | 8 +- .../tests/html-compiler-test.js | 187 ++++++++++++------ .../tests/hydration-opcode-compiler-test.js | 10 +- .../tests/template-compiler-test.js | 4 +- packages/htmlbars-runtime/lib/helpers.js | 2 +- packages/htmlbars-runtime/lib/hooks.js | 2 +- 8 files changed, 162 insertions(+), 95 deletions(-) diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index f2d051d6..d68aea30 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -40,16 +40,28 @@ prototype.compile = function(opcodes, options) { this.source.unshift(this.indent+" dom.insertBoundary(fragment, null);\n"); } - var i, l; + var i, l, morphs; + + var indent = this.indent + ' '; + if (this.morphs.length) { - var morphs = ""; - for (i = 0, l = this.morphs.length; i < l; ++i) { - var morph = this.morphs[i]; - morphs += this.indent+' var '+morph[0]+' = '+morph[1]+';\n'; - } - this.source.unshift(morphs); + morphs = + indent+'var morphs = env.morphs;\n' + + indent+'if (!morphs) {\n' + + indent+' morphs = new Array(' + this.morphs.length + ');\n'; + + for (i = 0, l = this.morphs.length; i < l; ++i) { + var morph = this.morphs[i]; + morphs += indent+' morphs['+i+'] = '+morph+';\n'; + } + + morphs += indent+'}\n'; + } else { + morphs = indent+'var morphs;\n'; } + this.source.unshift(morphs); + if (this.fragmentProcessing.length) { var processing = ""; for (i = 0, l = this.fragmentProcessing.length; i < l; ++i) { @@ -148,7 +160,7 @@ prototype.printSetHook = function(name, index) { prototype.printBlockHook = function(morphNum, templateId, inverseId) { this.printHook('block', [ 'env', - 'morph' + morphNum, + 'morphs[' + morphNum + ']', 'context', this.stack.pop(), // path this.stack.pop(), // params @@ -161,7 +173,7 @@ prototype.printBlockHook = function(morphNum, templateId, inverseId) { prototype.printInlineHook = function(morphNum) { this.printHook('inline', [ 'env', - 'morph' + morphNum, + 'morphs[' + morphNum + ']', 'context', this.stack.pop(), // path this.stack.pop(), // params @@ -172,7 +184,7 @@ prototype.printInlineHook = function(morphNum) { prototype.printContentHook = function(morphNum) { this.printHook('content', [ 'env', - 'morph' + morphNum, + 'morphs[' + morphNum + ']', 'context', this.stack.pop() // path ]); @@ -181,7 +193,7 @@ prototype.printContentHook = function(morphNum) { prototype.printComponentHook = function(morphNum, templateId) { this.printHook('component', [ 'env', - 'morph' + morphNum, + 'morphs[' + morphNum + ']', 'context', this.stack.pop(), // path this.stack.pop(), // attrs @@ -192,7 +204,7 @@ prototype.printComponentHook = function(morphNum, templateId) { prototype.printAttributeHook = function(attrMorphNum, elementNum) { this.printHook('attribute', [ 'env', - 'attrMorph' + attrMorphNum, + 'morphs[' + attrMorphNum + ']', 'element' + elementNum, this.stack.pop(), // name this.stack.pop() // value @@ -220,13 +232,13 @@ prototype.createMorph = function(morphNum, parentPath, startIndex, endIndex, esc ","+(endIndex === null ? "-1" : endIndex)+ (isRoot ? ",contextualElement)" : ")"); - this.morphs.push(['morph' + morphNum, morph]); + this.morphs[morphNum] = morph; }; prototype.createAttrMorph = function(attrMorphNum, elementNum, name, escaped, namespace) { var morphMethod = escaped ? 'createAttrMorph' : 'createUnsafeAttrMorph'; var morph = "dom."+morphMethod+"(element"+elementNum+", '"+name+(namespace ? "', '"+namespace : '')+"')"; - this.morphs.push(['attrMorph' + attrMorphNum, morph]); + this.morphs[attrMorphNum] = morph; }; prototype.repairClonedNode = function(blankChildTextNodes, isElementChecked) { diff --git a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js index 650a0a29..fa894393 100644 --- a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js @@ -28,7 +28,6 @@ function HydrationOpcodeCompiler() { this.currentDOMChildIndex = 0; this.morphs = []; this.morphNum = 0; - this.attrMorphNum = 0; this.element = null; this.elementNum = -1; } @@ -60,7 +59,6 @@ HydrationOpcodeCompiler.prototype.startProgram = function(program, c, blankChild this.templateId = 0; this.currentDOMChildIndex = -1; this.morphNum = 0; - this.attrMorphNum = 0; var blockParams = program.blockParams || []; @@ -212,7 +210,7 @@ HydrationOpcodeCompiler.prototype.attribute = function(attr) { this.element = null; } - var attrMorphNum = this.attrMorphNum++; + var attrMorphNum = this.morphNum++; this.opcode('createAttrMorph', attrMorphNum, this.elementNum, attr.name, escaped, namespace); this.opcode('printAttributeHook', attrMorphNum, this.elementNum); }; diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index bcd0c55f..35c80225 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -101,8 +101,8 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' var dom = env.dom;\n' + this.getHydrationHooks(indent + ' ', this.hydrationCompiler.hooks) + indent+' dom.detectNamespace(contextualElement);\n' + - indent+' var fragment;\n' + - indent+' if (env.useFragmentCache && dom.canClone) {\n' + + indent+' var fragment = env.target;\n' + + indent+' if (!fragment && env.useFragmentCache && dom.canClone) {\n' + indent+' if (this.cachedFragment === null) {\n' + indent+' fragment = this.build(dom);\n' + indent+' if (this.hasRendered) {\n' + @@ -114,11 +114,11 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' if (this.cachedFragment) {\n' + indent+' fragment = dom.cloneNode(this.cachedFragment, true);\n' + indent+' }\n' + - indent+' } else {\n' + + indent+' } else if (!fragment) {\n' + indent+' fragment = this.build(dom);\n' + indent+' }\n' + hydrationProgram + - indent+' return fragment;\n' + + indent+' return { fragment: fragment, morphs: morphs };\n' + indent+' }\n' + indent+' };\n' + indent+'}())'; diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index a104be3c..fadfb22f 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -43,7 +43,7 @@ function registerPartial(name, html) { function compilesTo(html, expected, context) { var template = compile(html); - var fragment = template.render(context, env, document.body); + var fragment = template.render(context, env, document.body).fragment; equalTokens(fragment, expected === undefined ? html : expected); return fragment; } @@ -62,10 +62,13 @@ function generateTokens(fragmentOrHtml) { div2.innerHTML = div.innerHTML; div.innerHTML = div2.innerHTML; } - return tokenize(div.innerHTML); + return { tokens: tokenize(div.innerHTML), html: div.innerHTML }; } function equalTokens(fragment, html) { + if (fragment.fragment) { fragment = fragment.fragment; } + if (html.fragment) { html = html.fragment; } + var fragTokens = generateTokens(fragment); var htmlTokens = generateTokens(html); @@ -83,10 +86,10 @@ function equalTokens(fragment, html) { } } - forEach(fragTokens, normalizeTokens); - forEach(htmlTokens, normalizeTokens); + forEach(fragTokens.tokens, normalizeTokens); + forEach(htmlTokens.tokens, normalizeTokens); - deepEqual(fragTokens, htmlTokens); + deepEqual(fragTokens.tokens, htmlTokens.tokens, "Expected: " + html + "; Actual: " + fragTokens.html); } function commonSetup() { @@ -109,56 +112,72 @@ QUnit.module("HTML-based compiler (output)", { test("Simple content produces a document fragment", function() { var template = compile("content"); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; equalTokens(fragment, "content"); }); test("Simple elements are created", function() { var template = compile("

hello!

content
"); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; + + equalTokens(fragment, "

hello!

content
"); +}); + +test("Simple elements can be re-rendered", function() { + var template = compile("

hello!

content
"); + var result = template.render({}, env); + var fragment = result.fragment; + var morphs = result.morphs; + + var oldFirstChild = fragment.firstChild; + env.morphs = morphs; + env.target = fragment; + fragment = template.render({}, env).fragment; + + strictEqual(fragment.firstChild, oldFirstChild); equalTokens(fragment, "

hello!

content
"); }); test("Simple elements can have attributes", function() { var template = compile("
content
"); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; equalTokens(fragment, '
content
'); }); test("Simple elements can have an empty attribute", function() { var template = compile("
content
"); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; equalTokens(fragment, '
content
'); }); test("presence of `disabled` attribute without value marks as disabled", function() { var template = compile(''); - var inputNode = template.render({}, env).firstChild; + var inputNode = template.render({}, env).fragment.firstChild; ok(inputNode.disabled, 'disabled without value set as property is true'); }); test("Null quoted attribute value calls toString on the value", function() { var template = compile(''); - var inputNode = template.render({isDisabled: null}, env).firstChild; + var inputNode = template.render({isDisabled: null}, env).fragment.firstChild; ok(inputNode.disabled, 'string of "null" set as property is true'); }); test("Null unquoted attribute value removes that attribute", function() { var template = compile(''); - var inputNode = template.render({isDisabled: null}, env).firstChild; + var inputNode = template.render({isDisabled: null}, env).fragment.firstChild; equalTokens(inputNode, ''); }); test("unquoted attribute string is just that", function() { var template = compile(''); - var inputNode = template.render({}, env).firstChild; + var inputNode = template.render({}, env).fragment.firstChild; equal(inputNode.tagName, 'INPUT', 'input tag'); equal(inputNode.value, 'funstuff', 'value is set as property'); @@ -166,7 +185,7 @@ test("unquoted attribute string is just that", function() { test("unquoted attribute expression is string", function() { var template = compile(''); - var inputNode = template.render({funstuff: "oh my"}, env).firstChild; + var inputNode = template.render({funstuff: "oh my"}, env).fragment.firstChild; equal(inputNode.tagName, 'INPUT', 'input tag'); equal(inputNode.value, 'oh my', 'string is set to property'); @@ -174,7 +193,7 @@ test("unquoted attribute expression is string", function() { test("unquoted attribute expression works when followed by another attribute", function() { var template = compile('
'); - var divNode = template.render({funstuff: "oh my"}, env).firstChild; + var divNode = template.render({funstuff: "oh my"}, env).fragment.firstChild; equalTokens(divNode, '
'); }); @@ -194,13 +213,13 @@ test("Unquoted attribute value with multiple nodes throws an exception", functio test("Simple elements can have arbitrary attributes", function() { var template = compile("
content
"); - var divNode = template.render({}, env).firstChild; + var divNode = template.render({}, env).fragment.firstChild; equalTokens(divNode, '
content
'); }); test("checked attribute and checked property are present after clone and hydrate", function() { var template = compile(""); - var inputNode = template.render({}, env).firstChild; + var inputNode = template.render({}, env).fragment.firstChild; equal(inputNode.tagName, 'INPUT', 'input tag'); equal(inputNode.checked, true, 'input tag is checked'); }); @@ -209,7 +228,7 @@ test("checked attribute and checked property are present after clone and hydrate function shouldBeVoid(tagName) { var html = "<" + tagName + " data-foo='bar'>

hello

"; var template = compile(html); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; var div = document.createElement("div"); @@ -234,7 +253,7 @@ test("Void elements are self-closing", function() { test("The compiler can handle nesting", function() { var html = '

hi!

 More content'; var template = compile(html); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; equalTokens(fragment, html); }); @@ -309,7 +328,7 @@ test("The compiler can handle top-level unescaped HTML", function() { test("The compiler can handle top-level unescaped tr", function() { var template = compile('{{{html}}}'); var context = { html: 'Yo' }; - var fragment = template.render(context, env, document.createElement('table')); + var fragment = template.render(context, env, document.createElement('table')).fragment; equal( fragment.firstChild.nextSibling.tagName, 'TR', @@ -319,7 +338,7 @@ test("The compiler can handle top-level unescaped tr", function() { test("The compiler can handle top-level unescaped td inside tr contextualElement", function() { var template = compile('{{{html}}}'); var context = { html: 'Yo' }; - var fragment = template.render(context, env, document.createElement('tr')); + var fragment = template.render(context, env, document.createElement('tr')).fragment; equal( fragment.firstChild.nextSibling.tagName, 'TD', @@ -328,12 +347,12 @@ test("The compiler can handle top-level unescaped td inside tr contextualElement test("The compiler can handle unescaped tr in top of content", function() { registerHelper('test', function(params, hash, options, env) { - return options.template.render(this, env, options.morph.contextualElement); + return options.template.render(this, env, options.morph.contextualElement).fragment; }); var template = compile('{{#test}}{{{html}}}{{/test}}'); var context = { html: 'Yo' }; - var fragment = template.render(context, env, document.createElement('table')); + var fragment = template.render(context, env, document.createElement('table')).fragment; equal( fragment.firstChild.nextSibling.nextSibling.tagName, 'TR', @@ -342,12 +361,12 @@ test("The compiler can handle unescaped tr in top of content", function() { test("The compiler can handle unescaped tr inside fragment table", function() { registerHelper('test', function(params, hash, options, env) { - return options.template.render(this, env, options.morph.contextualElement); + return options.template.render(this, env, options.morph.contextualElement).fragment; }); var template = compile('{{#test}}{{{html}}}{{/test}}
'); var context = { html: 'Yo' }; - var fragment = template.render(context, env, document.createElement('div')); + var fragment = template.render(context, env, document.createElement('div')).fragment; var tableNode = fragment.firstChild; equal( @@ -436,10 +455,47 @@ test("Simple data binding on fragments", function() { equalTokens(fragment, '

brown cow

to the world
'); }); +test("Simple data binding on fragments - re-rendering", function() { + hooks.content = function(env, morph, context, path) { + morph.escaped = false; + morph.setContent(context[path]); + }; + + var object = { title: '

hello

to the' }; + var template = compile('
{{title}} world
'); + var result = template.render(object, env); + + var fragment = result.fragment; + var morphs = result.morphs; + + // After the first render, save the returned fragment and + // morphs to be re-used for subsequent renders. + env.target = fragment; + env.morphs = morphs; + + equalTokens(fragment, '

hello

to the world
'); + + object.title = '

goodbye

to the'; + + var oldFirstChild = fragment.firstChild; + + template.render(object, env); + + strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); + equalTokens(fragment, '

goodbye

to the world
'); + + object.title = '

brown cow

to the'; + + template.render(object, env); + + strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); + equalTokens(fragment, '

brown cow

to the world
'); +}); + test("second render respects whitespace", function () { var template = compile('Hello {{ foo }} '); template.render({}, env, document.createElement('div')); - var fragment = template.render({}, env, document.createElement('div')); + var fragment = template.render({}, env, document.createElement('div')).fragment; equal(fragment.childNodes.length, 3, 'fragment contains 3 text nodes'); equal(getTextContent(fragment.childNodes[0]), 'Hello ', 'first text node ends with one space character'); equal(getTextContent(fragment.childNodes[2]), ' ', 'last text node contains one space character'); @@ -477,7 +533,7 @@ test("Morphs are escaped correctly", function() { equal(options.morph.parseTextAsHTML, false); if (options.template) { - return options.template.render({}, env, options.morph.contextualElement); + return options.template.render({}, env, options.morph.contextualElement).fragment; } return params[0]; @@ -678,7 +734,7 @@ test("Attribute runs can contain helpers", function() { */ test("A simple block helper can return the default document fragment", function() { registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env); + return options.template.render(this, env).fragment; }); compilesTo('{{#testing}}
123
{{/testing}}', '
123
'); @@ -686,7 +742,7 @@ test("A simple block helper can return the default document fragment", function( test("A simple block helper can return text", function() { registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env); + return options.template.render(this, env).fragment; }); compilesTo('{{#testing}}test{{else}}not shown{{/testing}}', 'test'); @@ -694,7 +750,7 @@ test("A simple block helper can return text", function() { test("A block helper can have an else block", function() { registerHelper('testing', function(params, hash, options, env) { - return options.inverse.render(this, env); + return options.inverse.render(this, env).fragment; }); compilesTo('{{#testing}}Nope{{else}}
123
{{/testing}}', '
123
'); @@ -703,7 +759,7 @@ test("A block helper can have an else block", function() { test("A block helper can pass a context to be used in the child", function() { registerHelper('testing', function(params, hash, options, env) { var context = { title: 'Rails is omakase' }; - return options.template.render(context, env); + return options.template.render(context, env).fragment; }); compilesTo('{{#testing}}
{{title}}
{{/testing}}', '
Rails is omakase
'); @@ -712,7 +768,7 @@ test("A block helper can pass a context to be used in the child", function() { test("Block helpers receive hash arguments", function() { registerHelper('testing', function(params, hash, options, env) { if (hash.truth) { - return options.template.render(this, env); + return options.template.render(this, env).fragment; } }); @@ -793,7 +849,7 @@ test("Node helpers can be used for attribute bindings", function() { test('Components - Called as helpers', function () { registerHelper('x-append', function(params, hash, options, env) { - var fragment = options.template.render(this, env, options.morph.contextualElement); + var fragment = options.template.render(this, env, options.morph.contextualElement).fragment; fragment.appendChild(document.createTextNode(hash.text)); return fragment; }); @@ -833,7 +889,7 @@ test('Repaired text nodes are ensured in the right place', function () { test("Simple elements can have dashed attributes", function() { var template = compile("
content
"); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; equalTokens(fragment, '
content
'); }); @@ -842,19 +898,19 @@ test("Block params", function() { registerHelper('a', function(params, hash, options, env) { var context = createObject(this); var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, document.body, ['W', 'X1'])); + span.appendChild(options.template.render(context, env, document.body, ['W', 'X1']).fragment); return 'A(' + span.innerHTML + ')'; }); registerHelper('b', function(params, hash, options, env) { var context = createObject(this); var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, document.body, ['X2', 'Y'])); + span.appendChild(options.template.render(context, env, document.body, ['X2', 'Y']).fragment); return 'B(' + span.innerHTML + ')'; }); registerHelper('c', function(params, hash, options, env) { var context = createObject(this); var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, document.body, ['Z'])); + span.appendChild(options.template.render(context, env, document.body, ['Z']).fragment); return 'C(' + span.innerHTML + ')'; // return "C(" + options.template.render() + ")"; }); @@ -879,7 +935,7 @@ test('Block params in HTML syntax', function () { registerHelper('x-bar', function(params, hash, options, env) { var context = createObject(this); var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, document.body, ['Xerxes', 'York', 'Zed'])); + span.appendChild(options.template.render(context, env, document.body, ['Xerxes', 'York', 'Zed']).fragment); return 'BAR(' + span.innerHTML + ')'; }); compilesTo('{{zee}},{{y}},{{x}}', 'BAR(Zed,York,Xerxes)', {}); @@ -899,7 +955,7 @@ test('Block params in HTML syntax - Throws exception if given zero parameters', test('Block params in HTML syntax - Works with a single parameter', function () { registerHelper('x-bar', function(params, hash, options, env) { - return options.template.render({}, env, document.body, ['Xerxes']); + return options.template.render({}, env, document.body, ['Xerxes']).fragment; }); compilesTo('{{x}}', 'Xerxes', {}); }); @@ -915,7 +971,7 @@ test('Block params in HTML syntax - Ignores whitespace', function () { expect(3); registerHelper('x-bar', function(params, hash, options) { - return options.template.render({}, env, document.body, ['Xerxes', 'York']); + return options.template.render({}, env, document.body, ['Xerxes', 'York']).fragment; }); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); @@ -1066,7 +1122,7 @@ QUnit.module("HTML-based compiler (output, svg)", { test("Simple elements can have namespaced attributes", function() { var template = compile("content"); - var svgNode = template.render({}, env).firstChild; + var svgNode = template.render({}, env).fragment.firstChild; equalTokens(svgNode, 'content'); equal(svgNode.attributes[0].namespaceURI, 'http://www.w3.org/1999/xlink'); @@ -1074,7 +1130,7 @@ test("Simple elements can have namespaced attributes", function() { test("Simple elements can have bound namespaced attributes", function() { var template = compile("content"); - var svgNode = template.render({title: 'svg-title'}, env).firstChild; + var svgNode = template.render({title: 'svg-title'}, env).fragment.firstChild; equalTokens(svgNode, 'content'); equal(svgNode.attributes[0].namespaceURI, 'http://www.w3.org/1999/xlink'); @@ -1082,14 +1138,14 @@ test("Simple elements can have bound namespaced attributes", function() { test("SVG element can have capitalized attributes", function() { var template = compile(""); - var svgNode = template.render({}, env).firstChild; + var svgNode = template.render({}, env).fragment.firstChild; equalTokens(svgNode, ''); }); test("The compiler can handle namespaced elements", function() { var html = ''; var template = compile(html); - var svgNode = template.render({}, env).firstChild; + var svgNode = template.render({}, env).fragment.firstChild; equal(svgNode.namespaceURI, svgNamespace, "creates the svg element with a namespace"); equalTokens(svgNode, html); @@ -1098,7 +1154,7 @@ test("The compiler can handle namespaced elements", function() { test("The compiler sets namespaces on nested namespaced elements", function() { var html = ''; var template = compile(html); - var svgNode = template.render({}, env).firstChild; + var svgNode = template.render({}, env).fragment.firstChild; equal( svgNode.childNodes[0].namespaceURI, svgNamespace, "creates the path element with a namespace" ); @@ -1108,7 +1164,7 @@ test("The compiler sets namespaces on nested namespaced elements", function() { test("The compiler sets a namespace on an HTML integration point", function() { var html = 'Hi'; var template = compile(html); - var svgNode = template.render({}, env).firstChild; + var svgNode = template.render({}, env).fragment.firstChild; equal( svgNode.namespaceURI, svgNamespace, "creates the svg element with a namespace" ); @@ -1120,7 +1176,7 @@ test("The compiler sets a namespace on an HTML integration point", function() { test("The compiler does not set a namespace on an element inside an HTML integration point", function() { var html = '
'; var template = compile(html); - var svgNode = template.render({}, env).firstChild; + var svgNode = template.render({}, env).fragment.firstChild; equal( svgNode.childNodes[0].childNodes[0].namespaceURI, xhtmlNamespace, "creates the div inside the foreignObject without a namespace" ); @@ -1130,7 +1186,7 @@ test("The compiler does not set a namespace on an element inside an HTML integra test("The compiler pops back to the correct namespace", function() { var html = '
'; var template = compile(html); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; equal( fragment.childNodes[0].namespaceURI, svgNamespace, "creates the first svg element with a namespace" ); @@ -1143,7 +1199,7 @@ test("The compiler pops back to the correct namespace", function() { test("The compiler pops back to the correct namespace even if exiting last child", function () { var html = '
'; - var fragment = compile(html).render({}, env); + var fragment = compile(html).render({}, env).fragment; equal(fragment.firstChild.namespaceURI, xhtmlNamespace, "first div's namespace is xhtmlNamespace"); equal(fragment.firstChild.firstChild.namespaceURI, svgNamespace, "svg's namespace is svgNamespace"); @@ -1153,7 +1209,7 @@ test("The compiler pops back to the correct namespace even if exiting last child test("The compiler preserves capitalization of tags", function() { var html = ''; var template = compile(html); - var fragment = template.render({}, env); + var fragment = template.render({}, env).fragment; equalTokens(fragment, html); }); @@ -1161,7 +1217,7 @@ test("The compiler preserves capitalization of tags", function() { test("svg can live with hydration", function() { var template = compile('{{name}}'); - var fragment = template.render({ name: 'Milly' }, env, document.body); + var fragment = template.render({ name: 'Milly' }, env, document.body).fragment; equal( fragment.childNodes[0].namespaceURI, svgNamespace, "svg namespace inside a block is present" ); @@ -1169,7 +1225,7 @@ test("svg can live with hydration", function() { test("top-level unsafe morph uses the correct namespace", function() { var template = compile('{{{foo}}}'); - var fragment = template.render({ foo: 'FOO' }, env, document.body); + var fragment = template.render({ foo: 'FOO' }, env, document.body).fragment; equal(getTextContent(fragment), 'FOO', 'element from unsafe morph is displayed'); equal(fragment.childNodes[1].namespaceURI, xhtmlNamespace, 'element from unsafe morph has correct namespace'); @@ -1177,7 +1233,7 @@ test("top-level unsafe morph uses the correct namespace", function() { test("nested unsafe morph uses the correct namespace", function() { var template = compile('{{{foo}}}
'); - var fragment = template.render({ foo: '' }, env, document.body); + var fragment = template.render({ foo: '' }, env, document.body).fragment; equal(fragment.childNodes[0].childNodes[0].namespaceURI, svgNamespace, 'element from unsafe morph has correct namespace'); @@ -1186,7 +1242,7 @@ test("nested unsafe morph uses the correct namespace", function() { test("svg can take some hydration", function() { var template = compile('
{{name}}
'); - var fragment = template.render({ name: 'Milly' }, env); + var fragment = template.render({ name: 'Milly' }, env).fragment; equal( fragment.firstChild.childNodes[0].namespaceURI, svgNamespace, "svg namespace inside a block is present" ); @@ -1196,8 +1252,9 @@ test("svg can take some hydration", function() { test("root svg can take some hydration", function() { var template = compile('{{name}}'); - var fragment = template.render({ name: 'Milly' }, env); + var fragment = template.render({ name: 'Milly' }, env).fragment; var svgNode = fragment.firstChild; + equal( svgNode.namespaceURI, svgNamespace, "svg namespace inside a block is present" ); @@ -1211,21 +1268,21 @@ test("Block helper allows interior namespace", function() { registerHelper('testing', function(params, hash, options, env) { var morph = options.morph; if (isTrue) { - return options.template.render(this, env, morph.contextualElement); + return options.template.render(this, env, morph.contextualElement).fragment; } else { - return options.inverse.render(this, env, morph.contextualElement); + return options.inverse.render(this, env, morph.contextualElement).fragment; } }); var template = compile('{{#testing}}{{else}}
{{/testing}}'); - var fragment = template.render({ isTrue: true }, env, document.body); + var fragment = template.render({ isTrue: true }, env, document.body).fragment; equal( fragment.firstChild.nextSibling.namespaceURI, svgNamespace, "svg namespace inside a block is present" ); isTrue = false; - fragment = template.render({ isTrue: false }, env, document.body); + fragment = template.render({ isTrue: false }, env, document.body).fragment; equal( fragment.firstChild.nextSibling.namespaceURI, xhtmlNamespace, "inverse block path has a normal namespace"); @@ -1237,12 +1294,12 @@ test("Block helper allows interior namespace", function() { test("Block helper allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { var morph = options.morph; - return options.template.render(this, env, morph.contextualElement); + return options.template.render(this, env, morph.contextualElement).fragment; }); var template = compile('
{{#testing}}{{/testing}}
'); - var fragment = template.render({ isTrue: true }, env); + var fragment = template.render({ isTrue: true }, env).fragment; var svgNode = fragment.firstChild.firstChild; equal( svgNode.namespaceURI, svgNamespace, "svg tag has an svg namespace" ); @@ -1253,12 +1310,12 @@ test("Block helper allows namespace to bleed through", function() { test("Block helper with root svg allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { var morph = options.morph; - return options.template.render(this, env, morph.contextualElement); + return options.template.render(this, env, morph.contextualElement).fragment; }); var template = compile('{{#testing}}{{/testing}}'); - var fragment = template.render({ isTrue: true }, env); + var fragment = template.render({ isTrue: true }, env).fragment; var svgNode = fragment.firstChild; equal( svgNode.namespaceURI, svgNamespace, "svg tag has an svg namespace" ); @@ -1269,12 +1326,12 @@ test("Block helper with root svg allows namespace to bleed through", function() test("Block helper with root foreignObject allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { var morph = options.morph; - return options.template.render(this, env, morph.contextualElement); + return options.template.render(this, env, morph.contextualElement).fragment; }); var template = compile('{{#testing}}
{{/testing}}
'); - var fragment = template.render({ isTrue: true }, env, document.createElementNS(svgNamespace, 'svg')); + var fragment = template.render({ isTrue: true }, env, document.createElementNS(svgNamespace, 'svg')).fragment; var svgNode = fragment.firstChild; equal( svgNode.namespaceURI, svgNamespace, "foreignObject tag has an svg namespace" ); diff --git a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js index a4832d2d..845022da 100644 --- a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js +++ b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js @@ -282,17 +282,17 @@ test("attribute helpers", function() { [ "createAttrMorph", [ 1, 0, 'id', true, null ] ], [ "printAttributeHook", [ 1, 0 ] ], [ "popParent", [] ], - [ "createMorph", [ 0, [], 1, 1, true ] ], + [ "createMorph", [ 2, [], 0, 1, true ] ], [ "pushLiteral", [ 'morphThing' ] ], - [ "printContentHook", [ 0 ] ], - [ "consumeParent", [ 2 ] ], + [ "printContentHook", [ 2 ] ], + [ "consumeParent", [ 1 ] ], [ "pushGetHook", [ 'ohMy' ] ], [ "prepareArray", [ 1 ] ], [ "pushConcatHook", [] ], [ "pushLiteral", [ 'class' ] ], [ "shareElement", [ 1 ] ], - [ "createAttrMorph", [ 2, 1, 'class', true, null ] ], - [ "printAttributeHook", [ 2, 1 ] ], + [ "createAttrMorph", [ 3, 1, 'class', true, null ] ], + [ "printAttributeHook", [ 3, 1 ] ], [ "popParent", [] ] ]); }); diff --git a/packages/htmlbars-compiler/tests/template-compiler-test.js b/packages/htmlbars-compiler/tests/template-compiler-test.js index 866f8b79..f7429331 100644 --- a/packages/htmlbars-compiler/tests/template-compiler-test.js +++ b/packages/htmlbars-compiler/tests/template-compiler-test.js @@ -37,7 +37,7 @@ test("it works", function testFunction() { env.helpers['if'] = function(params, hash, options) { if (params[0]) { - return options.template.render(context, env, options.morph.contextualElement); + return options.template.render(context, env, options.morph.contextualElement).fragment; } }; @@ -46,7 +46,7 @@ test("it works", function testFunction() { firstName: 'Kris', lastName: 'Selden' }; - var frag = template.render(context, env, document.body); + var frag = template.render(context, env, document.body).fragment; equalHTML(frag, '
Hello Kris Selden!
'); }); diff --git a/packages/htmlbars-runtime/lib/helpers.js b/packages/htmlbars-runtime/lib/helpers.js index 241795a3..02e1dedb 100644 --- a/packages/htmlbars-runtime/lib/helpers.js +++ b/packages/htmlbars-runtime/lib/helpers.js @@ -1,6 +1,6 @@ export function partial(params, hash, options, env) { var template = env.partials[params[0]]; - return template.render(this, env, options.morph.contextualElement); + return template.render(this, env, options.morph.contextualElement).fragment; } export default { diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index f7f02c0f..eca15263 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -103,7 +103,7 @@ function componentFallback(env, morph, context, tagName, attrs, template) { for (var name in attrs) { element.setAttribute(name, attrs[name]); } - element.appendChild(template.render(context, env, morph.contextualElement)); + element.appendChild(template.render(context, env, morph.contextualElement).fragment); return element; } From 7d7aa98325a1071e5bd49e0c57a3605b551e8a0e Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Mon, 2 Feb 2015 18:51:32 -0800 Subject: [PATCH 02/27] Encapsulate state needed for re-render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the notion of a “result” data type, which encapsulates all of the state needed to be passed to a compiled template in order to successfully re-render with an existing element. Rather than returning a document fragment, `render()` now returns this result type. For compatibility, consumers of this API will need to use the `fragment` property of the result rather than the result directly. In order to re-render, you can pass the last result as the `lastResult` option, and it will use that element and morph to update. One small additional change: contextual elements must now be passed in the options hash. We decided to make this change as the list of arguments was growing unwieldy, and we wanted to make this durable to changes in the future. We will also investigate incorporating block params into the options hash. --- .../lib/hydration-javascript-compiler.js | 9 +- .../lib/template-compiler.js | 9 +- .../tests/html-compiler-test.js | 104 ++++++++---------- .../tests/template-compiler-test.js | 2 +- packages/htmlbars-runtime/lib/hooks.js | 22 +++- 5 files changed, 75 insertions(+), 71 deletions(-) diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index d68aea30..26862d58 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -40,13 +40,14 @@ prototype.compile = function(opcodes, options) { this.source.unshift(this.indent+" dom.insertBoundary(fragment, null);\n"); } - var i, l, morphs; + var i, l; var indent = this.indent + ' '; + var morphs = indent+'var morphs;\n'; + if (this.morphs.length) { - morphs = - indent+'var morphs = env.morphs;\n' + + morphs += indent+'if (!morphs) {\n' + indent+' morphs = new Array(' + this.morphs.length + ');\n'; @@ -56,8 +57,6 @@ prototype.compile = function(opcodes, options) { } morphs += indent+'}\n'; - } else { - morphs = indent+'var morphs;\n'; } this.source.unshift(morphs); diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index 35c80225..b41ca96e 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -83,7 +83,7 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { var blockParams = program.blockParams || []; - var templateSignature = 'context, env, contextualElement'; + var templateSignature = 'context, env, options'; if (blockParams.length > 0) { templateSignature += ', blockArguments'; } @@ -100,8 +100,11 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' render: function render(' + templateSignature + ') {\n' + indent+' var dom = env.dom;\n' + this.getHydrationHooks(indent + ' ', this.hydrationCompiler.hooks) + + indent+' var contextualElement = options && options.contextualElement;\n' + + indent+' var lastResult = options && options.lastResult;\n' + indent+' dom.detectNamespace(contextualElement);\n' + - indent+' var fragment = env.target;\n' + + indent+' var fragment = lastResult ? lastResult.fragment : null;\n' + + indent+' var morphs = lastResult ? lastResult.morphs : null;\n' + indent+' if (!fragment && env.useFragmentCache && dom.canClone) {\n' + indent+' if (this.cachedFragment === null) {\n' + indent+' fragment = this.build(dom);\n' + @@ -118,7 +121,7 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' fragment = this.build(dom);\n' + indent+' }\n' + hydrationProgram + - indent+' return { fragment: fragment, morphs: morphs };\n' + + indent+' return lastResult || { fragment: fragment, morphs: morphs };\n' + indent+' }\n' + indent+' };\n' + indent+'}())'; diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index fadfb22f..d77bc096 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -43,7 +43,7 @@ function registerPartial(name, html) { function compilesTo(html, expected, context) { var template = compile(html); - var fragment = template.render(context, env, document.body).fragment; + var fragment = template.render(context, env, { contextualElement: document.body }).fragment; equalTokens(fragment, expected === undefined ? html : expected); return fragment; } @@ -128,13 +128,10 @@ test("Simple elements can be re-rendered", function() { var template = compile("

hello!

content
"); var result = template.render({}, env); var fragment = result.fragment; - var morphs = result.morphs; var oldFirstChild = fragment.firstChild; - env.morphs = morphs; - env.target = fragment; - fragment = template.render({}, env).fragment; + fragment = template.render({}, env, { lastResult: result }).fragment; strictEqual(fragment.firstChild, oldFirstChild); equalTokens(fragment, "

hello!

content
"); @@ -328,7 +325,7 @@ test("The compiler can handle top-level unescaped HTML", function() { test("The compiler can handle top-level unescaped tr", function() { var template = compile('{{{html}}}'); var context = { html: 'Yo' }; - var fragment = template.render(context, env, document.createElement('table')).fragment; + var fragment = template.render(context, env, { contextualElement: document.createElement('table') }).fragment; equal( fragment.firstChild.nextSibling.tagName, 'TR', @@ -338,7 +335,7 @@ test("The compiler can handle top-level unescaped tr", function() { test("The compiler can handle top-level unescaped td inside tr contextualElement", function() { var template = compile('{{{html}}}'); var context = { html: 'Yo' }; - var fragment = template.render(context, env, document.createElement('tr')).fragment; + var fragment = template.render(context, env, { contextualElement: document.createElement('tr') }).fragment; equal( fragment.firstChild.nextSibling.tagName, 'TD', @@ -347,12 +344,12 @@ test("The compiler can handle top-level unescaped td inside tr contextualElement test("The compiler can handle unescaped tr in top of content", function() { registerHelper('test', function(params, hash, options, env) { - return options.template.render(this, env, options.morph.contextualElement).fragment; + return options.template.render(this, env, { contextualElement: options.morph.contextualElement }); }); var template = compile('{{#test}}{{{html}}}{{/test}}'); var context = { html: 'Yo' }; - var fragment = template.render(context, env, document.createElement('table')).fragment; + var fragment = template.render(context, env, { contextualElement: document.createElement('table') }).fragment; equal( fragment.firstChild.nextSibling.nextSibling.tagName, 'TR', @@ -361,12 +358,12 @@ test("The compiler can handle unescaped tr in top of content", function() { test("The compiler can handle unescaped tr inside fragment table", function() { registerHelper('test', function(params, hash, options, env) { - return options.template.render(this, env, options.morph.contextualElement).fragment; + return options.template.render(this, env, { contextualElement: options.morph.contextualElement }); }); var template = compile('{{#test}}{{{html}}}{{/test}}
'); var context = { html: 'Yo' }; - var fragment = template.render(context, env, document.createElement('div')).fragment; + var fragment = template.render(context, env, { contextualElement: document.createElement('div') }).fragment; var tableNode = fragment.firstChild; equal( @@ -466,12 +463,6 @@ test("Simple data binding on fragments - re-rendering", function() { var result = template.render(object, env); var fragment = result.fragment; - var morphs = result.morphs; - - // After the first render, save the returned fragment and - // morphs to be re-used for subsequent renders. - env.target = fragment; - env.morphs = morphs; equalTokens(fragment, '

hello

to the world
'); @@ -479,14 +470,14 @@ test("Simple data binding on fragments - re-rendering", function() { var oldFirstChild = fragment.firstChild; - template.render(object, env); + template.render(object, env, { lastResult: result }); strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); equalTokens(fragment, '

goodbye

to the world
'); object.title = '

brown cow

to the'; - template.render(object, env); + template.render(object, env, { lastResult: result }); strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); equalTokens(fragment, '

brown cow

to the world
'); @@ -494,8 +485,8 @@ test("Simple data binding on fragments - re-rendering", function() { test("second render respects whitespace", function () { var template = compile('Hello {{ foo }} '); - template.render({}, env, document.createElement('div')); - var fragment = template.render({}, env, document.createElement('div')).fragment; + template.render({}, env, { contextualElement: document.createElement('div') }); + var fragment = template.render({}, env, { contextualElement: document.createElement('div') }).fragment; equal(fragment.childNodes.length, 3, 'fragment contains 3 text nodes'); equal(getTextContent(fragment.childNodes[0]), 'Hello ', 'first text node ends with one space character'); equal(getTextContent(fragment.childNodes[2]), ' ', 'last text node contains one space character'); @@ -533,7 +524,7 @@ test("Morphs are escaped correctly", function() { equal(options.morph.parseTextAsHTML, false); if (options.template) { - return options.template.render({}, env, options.morph.contextualElement).fragment; + return options.template.render({}, env, { contextualElement: options.morph.contextualElement }); } return params[0]; @@ -734,15 +725,16 @@ test("Attribute runs can contain helpers", function() { */ test("A simple block helper can return the default document fragment", function() { registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env).fragment; + return options.template.render(this, env, { lastResult: options.lastResult }); }); compilesTo('{{#testing}}
123
{{/testing}}', '
123
'); }); +// TODO: NEXT test("A simple block helper can return text", function() { registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env).fragment; + return options.template.render(this, env); }); compilesTo('{{#testing}}test{{else}}not shown{{/testing}}', 'test'); @@ -750,7 +742,7 @@ test("A simple block helper can return text", function() { test("A block helper can have an else block", function() { registerHelper('testing', function(params, hash, options, env) { - return options.inverse.render(this, env).fragment; + return options.inverse.render(this, env); }); compilesTo('{{#testing}}Nope{{else}}
123
{{/testing}}', '
123
'); @@ -759,7 +751,7 @@ test("A block helper can have an else block", function() { test("A block helper can pass a context to be used in the child", function() { registerHelper('testing', function(params, hash, options, env) { var context = { title: 'Rails is omakase' }; - return options.template.render(context, env).fragment; + return options.template.render(context, env); }); compilesTo('{{#testing}}
{{title}}
{{/testing}}', '
Rails is omakase
'); @@ -768,7 +760,7 @@ test("A block helper can pass a context to be used in the child", function() { test("Block helpers receive hash arguments", function() { registerHelper('testing', function(params, hash, options, env) { if (hash.truth) { - return options.template.render(this, env).fragment; + return options.template.render(this, env); } }); @@ -849,9 +841,9 @@ test("Node helpers can be used for attribute bindings", function() { test('Components - Called as helpers', function () { registerHelper('x-append', function(params, hash, options, env) { - var fragment = options.template.render(this, env, options.morph.contextualElement).fragment; - fragment.appendChild(document.createTextNode(hash.text)); - return fragment; + var result = options.template.render(this, env, { contextualElement: options.morph.contextualElement }); + result.fragment.appendChild(document.createTextNode(hash.text)); + return result; }); var object = { bar: 'e', baz: 'c' }; compilesTo('ab{{baz}}f','abcdef', object); @@ -898,19 +890,19 @@ test("Block params", function() { registerHelper('a', function(params, hash, options, env) { var context = createObject(this); var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, document.body, ['W', 'X1']).fragment); + span.appendChild(options.template.render(context, env, { contextualElement: document.body }, ['W', 'X1']).fragment); return 'A(' + span.innerHTML + ')'; }); registerHelper('b', function(params, hash, options, env) { var context = createObject(this); var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, document.body, ['X2', 'Y']).fragment); + span.appendChild(options.template.render(context, env, { contextualElement: document.body }, ['X2', 'Y']).fragment); return 'B(' + span.innerHTML + ')'; }); registerHelper('c', function(params, hash, options, env) { var context = createObject(this); var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, document.body, ['Z']).fragment); + span.appendChild(options.template.render(context, env, { contextualElement: document.body }, ['Z']).fragment); return 'C(' + span.innerHTML + ')'; // return "C(" + options.template.render() + ")"; }); @@ -925,17 +917,17 @@ test("Block params - Helper should know how many block params it was called with equal(options.template.blockParams, this.count, 'Helpers should receive the correct number of block params in options.template.blockParams.'); }); - compile('{{#count-block-params}}{{/count-block-params}}').render({ count: 0 }, env, document.body); - compile('{{#count-block-params as |x|}}{{/count-block-params}}').render({ count: 1 }, env, document.body); - compile('{{#count-block-params as |x y|}}{{/count-block-params}}').render({ count: 2 }, env, document.body); - compile('{{#count-block-params as |x y z|}}{{/count-block-params}}').render({ count: 3 }, env, document.body); + compile('{{#count-block-params}}{{/count-block-params}}').render({ count: 0 }, env, { contextualElement: document.body }); + compile('{{#count-block-params as |x|}}{{/count-block-params}}').render({ count: 1 }, env, { contextualElement: document.body }); + compile('{{#count-block-params as |x y|}}{{/count-block-params}}').render({ count: 2 }, env, { contextualElement: document.body }); + compile('{{#count-block-params as |x y z|}}{{/count-block-params}}').render({ count: 3 }, env, { contextualElement: document.body }); }); test('Block params in HTML syntax', function () { registerHelper('x-bar', function(params, hash, options, env) { var context = createObject(this); var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, document.body, ['Xerxes', 'York', 'Zed']).fragment); + span.appendChild(options.template.render(context, env, { contextualElement: document.body }, ['Xerxes', 'York', 'Zed']).fragment); return 'BAR(' + span.innerHTML + ')'; }); compilesTo('{{zee}},{{y}},{{x}}', 'BAR(Zed,York,Xerxes)', {}); @@ -955,7 +947,7 @@ test('Block params in HTML syntax - Throws exception if given zero parameters', test('Block params in HTML syntax - Works with a single parameter', function () { registerHelper('x-bar', function(params, hash, options, env) { - return options.template.render({}, env, document.body, ['Xerxes']).fragment; + return options.template.render({}, env, { contextualElement: document.body }, ['Xerxes']); }); compilesTo('{{x}}', 'Xerxes', {}); }); @@ -964,14 +956,14 @@ test('Block params in HTML syntax - Works with other attributes', function () { registerHelper('x-bar', function(params, hash) { deepEqual(hash, {firstName: 'Alice', lastName: 'Smith'}); }); - compile('').render({}, env, document.body); + compile('').render({}, env, { contextualElement: document.body }); }); test('Block params in HTML syntax - Ignores whitespace', function () { expect(3); registerHelper('x-bar', function(params, hash, options) { - return options.template.render({}, env, document.body, ['Xerxes', 'York']).fragment; + return options.template.render({}, env, { contextualElement: document.body }, ['Xerxes', 'York']); }); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); @@ -985,10 +977,10 @@ test('Block params in HTML syntax - Helper should know how many block params it equal(options.template.blockParams, this.count, 'Helpers should receive the correct number of block params in options.template.blockParams.'); }); - compile('').render({ count: 0 }, env, document.body); - compile('').render({ count: 1 }, env, document.body); - compile('').render({ count: 2 }, env, document.body); - compile('').render({ count: 3 }, env, document.body); + compile('').render({ count: 0 }, env, { contextualElement: document.body }); + compile('').render({ count: 1 }, env, { contextualElement: document.body }); + compile('').render({ count: 2 }, env, { contextualElement: document.body }); + compile('').render({ count: 3 }, env, { contextualElement: document.body }); }); test("Block params in HTML syntax - Throws an error on invalid block params syntax", function() { @@ -1217,7 +1209,7 @@ test("The compiler preserves capitalization of tags", function() { test("svg can live with hydration", function() { var template = compile('{{name}}'); - var fragment = template.render({ name: 'Milly' }, env, document.body).fragment; + var fragment = template.render({ name: 'Milly' }, env, { contextualElement: document.body }).fragment; equal( fragment.childNodes[0].namespaceURI, svgNamespace, "svg namespace inside a block is present" ); @@ -1225,7 +1217,7 @@ test("svg can live with hydration", function() { test("top-level unsafe morph uses the correct namespace", function() { var template = compile('{{{foo}}}'); - var fragment = template.render({ foo: 'FOO' }, env, document.body).fragment; + var fragment = template.render({ foo: 'FOO' }, env, { contextualElement: document.body }).fragment; equal(getTextContent(fragment), 'FOO', 'element from unsafe morph is displayed'); equal(fragment.childNodes[1].namespaceURI, xhtmlNamespace, 'element from unsafe morph has correct namespace'); @@ -1233,7 +1225,7 @@ test("top-level unsafe morph uses the correct namespace", function() { test("nested unsafe morph uses the correct namespace", function() { var template = compile('{{{foo}}}
'); - var fragment = template.render({ foo: '' }, env, document.body).fragment; + var fragment = template.render({ foo: '' }, env, { contextualElement: document.body }).fragment; equal(fragment.childNodes[0].childNodes[0].namespaceURI, svgNamespace, 'element from unsafe morph has correct namespace'); @@ -1268,21 +1260,21 @@ test("Block helper allows interior namespace", function() { registerHelper('testing', function(params, hash, options, env) { var morph = options.morph; if (isTrue) { - return options.template.render(this, env, morph.contextualElement).fragment; + return options.template.render(this, env, { contextualElement: morph.contextualElement }); } else { - return options.inverse.render(this, env, morph.contextualElement).fragment; + return options.inverse.render(this, env, { contextualElement: morph.contextualElement }); } }); var template = compile('{{#testing}}{{else}}
{{/testing}}'); - var fragment = template.render({ isTrue: true }, env, document.body).fragment; + var fragment = template.render({ isTrue: true }, env, { contextualElement: document.body }).fragment; equal( fragment.firstChild.nextSibling.namespaceURI, svgNamespace, "svg namespace inside a block is present" ); isTrue = false; - fragment = template.render({ isTrue: false }, env, document.body).fragment; + fragment = template.render({ isTrue: false }, env, { contextualElement: document.body }).fragment; equal( fragment.firstChild.nextSibling.namespaceURI, xhtmlNamespace, "inverse block path has a normal namespace"); @@ -1294,7 +1286,7 @@ test("Block helper allows interior namespace", function() { test("Block helper allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { var morph = options.morph; - return options.template.render(this, env, morph.contextualElement).fragment; + return options.template.render(this, env, { contextualElement: morph.contextualElement }); }); var template = compile('
{{#testing}}{{/testing}}
'); @@ -1310,7 +1302,7 @@ test("Block helper allows namespace to bleed through", function() { test("Block helper with root svg allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { var morph = options.morph; - return options.template.render(this, env, morph.contextualElement).fragment; + return options.template.render(this, env, { contextualElement: morph.contextualElement }); }); var template = compile('{{#testing}}{{/testing}}'); @@ -1326,12 +1318,12 @@ test("Block helper with root svg allows namespace to bleed through", function() test("Block helper with root foreignObject allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { var morph = options.morph; - return options.template.render(this, env, morph.contextualElement).fragment; + return options.template.render(this, env, { contextualElement: morph.contextualElement }); }); var template = compile('{{#testing}}
{{/testing}}
'); - var fragment = template.render({ isTrue: true }, env, document.createElementNS(svgNamespace, 'svg')).fragment; + var fragment = template.render({ isTrue: true }, env, { contextualElement: document.createElementNS(svgNamespace, 'svg') }).fragment; var svgNode = fragment.firstChild; equal( svgNode.namespaceURI, svgNamespace, "foreignObject tag has an svg namespace" ); diff --git a/packages/htmlbars-compiler/tests/template-compiler-test.js b/packages/htmlbars-compiler/tests/template-compiler-test.js index f7429331..a741a7ca 100644 --- a/packages/htmlbars-compiler/tests/template-compiler-test.js +++ b/packages/htmlbars-compiler/tests/template-compiler-test.js @@ -37,7 +37,7 @@ test("it works", function testFunction() { env.helpers['if'] = function(params, hash, options) { if (params[0]) { - return options.template.render(context, env, options.morph.contextualElement).fragment; + return options.template.render(context, env, { contextualElement: options.morph.contextualElement }); } }; diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index eca15263..3e7c4da9 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -2,13 +2,23 @@ export function block(env, morph, context, path, params, hash, template, inverse var options = { morph: morph, template: template, - inverse: inverse + inverse: inverse, + lastResult: morph.lastResult }; var helper = lookupHelper(env, context, path); - var value = helper.call(context, params, hash, options, env); + var result = helper.call(context, params, hash, options, env); - morph.setContent(value); + setResultOnMorph(morph, result); +} + +function setResultOnMorph(morph, result) { + if (typeof result !== 'object') { + morph.setContent(result); + } else { + morph.lastResult = result; + morph.setContent(result.fragment); + } } export function inline(env, morph, context, path, params, hash) { @@ -83,11 +93,11 @@ export function component(env, morph, context, tagName, attrs, template) { }; value = helper.call(context, [], attrs, options, env); + setResultOnMorph(morph, value); } else { value = componentFallback(env, morph, context, tagName, attrs, template); + morph.setContent(value); } - - morph.setContent(value); } export function concat(env, params) { @@ -103,7 +113,7 @@ function componentFallback(env, morph, context, tagName, attrs, template) { for (var name in attrs) { element.setAttribute(name, attrs[name]); } - element.appendChild(template.render(context, env, morph.contextualElement).fragment); + element.appendChild(template.render(context, env, { contextualElement: morph.contextualElement }).fragment); return element; } From d65b07c8da68218a1cc58d7aa749fbac7fec7b74 Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Wed, 4 Feb 2015 12:12:40 -0800 Subject: [PATCH 03/27] Extract static cached fragment logic to runtime --- demos/compile-and-run.html | 2 +- .../lib/template-compiler.js | 16 +------------- packages/htmlbars-runtime/lib/hooks.js | 22 +++++++++++++++++++ packages/htmlbars-runtime/tests/main-test.js | 1 + 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/demos/compile-and-run.html b/demos/compile-and-run.html index 3daac5e5..abf1642f 100644 --- a/demos/compile-and-run.html +++ b/demos/compile-and-run.html @@ -73,7 +73,7 @@ if (!skipRender.checked) { var env = { dom: new DOMHelper(), hooks: hooks, helpers: helpers }; var template = compiler.compile(source, compileOptions); - var dom = template.render(data, env, output); + var dom = template.render(data, env, { contextualElement: output }).fragment; output.innerHTML += '
' + JSON.stringify(data) + '

'; output.appendChild(dom); diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index b41ca96e..e04654aa 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -105,21 +105,7 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' dom.detectNamespace(contextualElement);\n' + indent+' var fragment = lastResult ? lastResult.fragment : null;\n' + indent+' var morphs = lastResult ? lastResult.morphs : null;\n' + - indent+' if (!fragment && env.useFragmentCache && dom.canClone) {\n' + - indent+' if (this.cachedFragment === null) {\n' + - indent+' fragment = this.build(dom);\n' + - indent+' if (this.hasRendered) {\n' + - indent+' this.cachedFragment = fragment;\n' + - indent+' } else {\n' + - indent+' this.hasRendered = true;\n' + - indent+' }\n' + - indent+' }\n' + - indent+' if (this.cachedFragment) {\n' + - indent+' fragment = dom.cloneNode(this.cachedFragment, true);\n' + - indent+' }\n' + - indent+' } else if (!fragment) {\n' + - indent+' fragment = this.build(dom);\n' + - indent+' }\n' + + indent+' fragment = env.hooks.getCachedFragment(this, fragment, env);\n' + hydrationProgram + indent+' return lastResult || { fragment: fragment, morphs: morphs };\n' + indent+' }\n' + diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 3e7c4da9..fc2b404c 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -1,3 +1,24 @@ +export function getCachedFragment(template, fragment, env) { + var dom = env.dom; + if (!fragment && env.useFragmentCache && dom.canClone) { + if (template.cachedFragment === null) { + fragment = template.build(dom); + if (template.hasRendered) { + template.cachedFragment = fragment; + } else { + template.hasRendered = true; + } + } + if (template.cachedFragment) { + fragment = dom.cloneNode(template.cachedFragment, true); + } + } else if (!fragment) { + fragment = template.build(dom); + } + + return fragment; +} + export function block(env, morph, context, path, params, hash, template, inverse) { var options = { morph: morph, @@ -122,6 +143,7 @@ function lookupHelper(env, context, helperName) { } export default { + getCachedFragment: getCachedFragment, content: content, block: block, inline: inline, diff --git a/packages/htmlbars-runtime/tests/main-test.js b/packages/htmlbars-runtime/tests/main-test.js index eeba830b..5af3a21e 100644 --- a/packages/htmlbars-runtime/tests/main-test.js +++ b/packages/htmlbars-runtime/tests/main-test.js @@ -14,6 +14,7 @@ function keys(obj) { test("hooks are present", function () { var hookNames = [ + "getCachedFragment", "content", "inline", "block", From a3d711259307e21a43876c8713743f8fd93cbe5c Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Wed, 4 Feb 2015 13:23:23 -0800 Subject: [PATCH 04/27] Break apart hydration code into phases --- .../lib/hydration-javascript-compiler.js | 13 ++++++++++--- packages/htmlbars-compiler/lib/template-compiler.js | 6 ++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index 26862d58..14ff8de8 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -45,6 +45,13 @@ prototype.compile = function(opcodes, options) { var indent = this.indent + ' '; var morphs = indent+'var morphs;\n'; + var result = { + createMorphsProgram: '', + hydrateMorphsProgram: '', + fragmentProcessingProgram: '' + }; + + result.hydrateMorphsProgram = this.source.join(''); if (this.morphs.length) { morphs += @@ -59,17 +66,17 @@ prototype.compile = function(opcodes, options) { morphs += indent+'}\n'; } - this.source.unshift(morphs); + result.createMorphsProgram = morphs; if (this.fragmentProcessing.length) { var processing = ""; for (i = 0, l = this.fragmentProcessing.length; i < l; ++i) { processing += this.indent+' '+this.fragmentProcessing[i]+'\n'; } - this.source.unshift(processing); + result.fragmentProcessingProgram = processing; } - return this.source.join(''); + return result; }; prototype.prepareArray = function(length) { diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index e04654aa..eb1884d9 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -76,7 +76,7 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { ); // function hydrate(fragment) { return mustaches; } - var hydrationProgram = this.hydrationCompiler.compile( + var hydrationPrograms = this.hydrationCompiler.compile( this.hydrationOpcodeCompiler.opcodes, options ); @@ -106,7 +106,9 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' var fragment = lastResult ? lastResult.fragment : null;\n' + indent+' var morphs = lastResult ? lastResult.morphs : null;\n' + indent+' fragment = env.hooks.getCachedFragment(this, fragment, env);\n' + - hydrationProgram + + hydrationPrograms.fragmentProcessingProgram + + hydrationPrograms.createMorphsProgram + + hydrationPrograms.hydrateMorphsProgram + indent+' return lastResult || { fragment: fragment, morphs: morphs };\n' + indent+' }\n' + indent+' };\n' + From b3730bd594bc442cc743a95863a66130f9498ba9 Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Wed, 4 Feb 2015 18:24:15 -0800 Subject: [PATCH 05/27] Introduce element morphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change is an important step towards our efforts to offer rerendering of an existing element, rather than creating a new document fragment each time a template’s context changes. In the old compiled templates, the monolithic render method did all of the following: 1. Decide whether to use a cached fragment or build a new one from the build method. 2. Clone the fragment if possible / necessary 3. Create variables pointing at the fragment’s children (element0, etc.) 4. Create a bunch of pointers (morphs) with those element variables 5. Fill in the pointers (“populate”) Because all of the “pointers” necessary to update the DOM in the future were created as variables in the function scope, access to them was lost as soon as the function exited. This change is designed to make it possible to run step (4) above over and over again on the same piece of DOM. In order to make that possible, we changed the first few steps so that the pointers into the fragment are returned as an array, where they can be passed into a standalone “populate” function. For future readers, the “pointers” into the DOM are now called “render nodes”. These render nodes can point at either elements, attributes, or ranges. Expect these nodes to gain more responsibilities in the future. We also extracted the cached fragment logic into a hook, rather than repeating the completely static content in every template, significantly shrinking the size of compiled templates. The next step is to break up the `render` method into multiple pieces, so that the part that fills in the render nodes is completely standalone, and can be called by passing in the previous render nodes. (We wrote down our original design that we are still more-or-less iterating towards at https://gist.github.com/wycats/fc53be32abee6c5ca0f4.) --- packages/dom-helper/lib/main.js | 10 +++ .../lib/hydration-javascript-compiler.js | 44 +++++++----- .../lib/hydration-opcode-compiler.js | 21 +++--- .../lib/template-compiler.js | 15 ++-- .../tests/html-compiler-test.js | 58 +++++++-------- .../tests/hydration-opcode-compiler-test.js | 72 +++++++++++-------- packages/htmlbars-runtime/lib/hooks.js | 6 +- 7 files changed, 133 insertions(+), 93 deletions(-) diff --git a/packages/dom-helper/lib/main.js b/packages/dom-helper/lib/main.js index 3af2d6ac..bb39d038 100644 --- a/packages/dom-helper/lib/main.js +++ b/packages/dom-helper/lib/main.js @@ -105,6 +105,12 @@ function buildSVGDOM(html, dom){ return div.firstChild.childNodes; } +function ElementMorph(element, dom, namespace) { + this.element = element; + this.dom = dom; + this.namespace = namespace; +} + /* * A class wrapping DOM functions to address environment compatibility, * namespaces, contextual elements for morph un-escaped content @@ -346,6 +352,10 @@ prototype.createAttrMorph = function(element, attrName, namespace){ return new AttrMorph(element, attrName, this, namespace); }; +prototype.createElementMorph = function(element, namespace){ + return new ElementMorph(element, this, namespace); +}; + prototype.createUnsafeAttrMorph = function(element, attrName, namespace){ var morph = this.createAttrMorph(element, attrName, namespace); morph.escaped = false; diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index 14ff8de8..736ad621 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -42,40 +42,47 @@ prototype.compile = function(opcodes, options) { var i, l; - var indent = this.indent + ' '; + var indent = this.indent; + + var morphs; - var morphs = indent+'var morphs;\n'; var result = { createMorphsProgram: '', hydrateMorphsProgram: '', - fragmentProcessingProgram: '' + fragmentProcessingProgram: '', + hasMorphs: false }; result.hydrateMorphsProgram = this.source.join(''); if (this.morphs.length) { - morphs += - indent+'if (!morphs) {\n' + - indent+' morphs = new Array(' + this.morphs.length + ');\n'; + result.hasMorphs = true; + morphs = + indent+'var morphs = new Array(' + this.morphs.length + ');\n'; for (i = 0, l = this.morphs.length; i < l; ++i) { var morph = this.morphs[i]; - morphs += indent+' morphs['+i+'] = '+morph+';\n'; + morphs += indent+'morphs['+i+'] = '+morph+';\n'; } - - morphs += indent+'}\n'; } - result.createMorphsProgram = morphs; - if (this.fragmentProcessing.length) { var processing = ""; for (i = 0, l = this.fragmentProcessing.length; i < l; ++i) { - processing += this.indent+' '+this.fragmentProcessing[i]+'\n'; + processing += this.indent+this.fragmentProcessing[i]+'\n'; } result.fragmentProcessingProgram = processing; } + if (result.hasMorphs) { + result.createMorphsProgram = + ' function renderNodes(dom, fragment, contextualElement) {\n' + + result.fragmentProcessingProgram + + morphs + + ' return morphs;\n' + + ' }\n'; + } + return result; }; @@ -207,20 +214,19 @@ prototype.printComponentHook = function(morphNum, templateId) { ]); }; -prototype.printAttributeHook = function(attrMorphNum, elementNum) { +prototype.printAttributeHook = function(attrMorphNum) { this.printHook('attribute', [ 'env', 'morphs[' + attrMorphNum + ']', - 'element' + elementNum, this.stack.pop(), // name this.stack.pop() // value ]); }; -prototype.printElementHook = function(elementNum) { +prototype.printElementHook = function(morphNum) { this.printHook('element', [ 'env', - 'element' + elementNum, + 'morphs[' + morphNum + ']', 'context', this.stack.pop(), // path this.stack.pop(), // params @@ -247,6 +253,12 @@ prototype.createAttrMorph = function(attrMorphNum, elementNum, name, escaped, na this.morphs[attrMorphNum] = morph; }; +prototype.createElementMorph = function(morphNum, elementNum ) { + var morphMethod = 'createElementMorph'; + var morph = "dom."+morphMethod+"(element"+elementNum+")"; + this.morphs[morphNum] = morph; +}; + prototype.repairClonedNode = function(blankChildTextNodes, isElementChecked) { var parent = this.getParent(), processing = 'if (this.cachedFragment) { dom.repairClonedNode('+parent+','+ diff --git a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js index fa894393..63937c3c 100644 --- a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js @@ -28,6 +28,7 @@ function HydrationOpcodeCompiler() { this.currentDOMChildIndex = 0; this.morphs = []; this.morphNum = 0; + this.elementMorphNum = null; this.element = null; this.elementNum = -1; } @@ -93,8 +94,7 @@ HydrationOpcodeCompiler.prototype.openElement = function(element, pos, len, must // If our parent reference will be used more than once, cache its reference. if (mustacheCount > 1) { - this.opcode('shareElement', ++this.elementNum); - this.element = null; // Set element to null so we don't cache it twice + shareElement(this); } var isElementChecked = detectIsElementChecked(element); @@ -206,13 +206,12 @@ HydrationOpcodeCompiler.prototype.attribute = function(attr) { this.opcode('pushLiteral', attr.name); if (this.element !== null) { - this.opcode('shareElement', ++this.elementNum); - this.element = null; + shareElement(this); } var attrMorphNum = this.morphNum++; this.opcode('createAttrMorph', attrMorphNum, this.elementNum, attr.name, escaped, namespace); - this.opcode('printAttributeHook', attrMorphNum, this.elementNum); + this.opcode('printAttributeHook', attrMorphNum); }; HydrationOpcodeCompiler.prototype.elementHelper = function(sexpr) { @@ -220,11 +219,10 @@ HydrationOpcodeCompiler.prototype.elementHelper = function(sexpr) { // If we have a helper in a node, and this element has not been cached, cache it if (this.element !== null) { - this.opcode('shareElement', ++this.elementNum); - this.element = null; // Reset element so we don't cache it more than once + shareElement(this); } - this.opcode('printElementHook', this.elementNum); + this.opcode('printElementHook', this.elementMorphNum); }; HydrationOpcodeCompiler.prototype.pushMorphPlaceholderNode = function(childIndex, childCount) { @@ -293,6 +291,13 @@ function prepareSexpr(compiler, sexpr) { preparePath(compiler, sexpr.path); } +function shareElement(compiler) { + compiler.opcode('shareElement', ++compiler.elementNum); + var morphNum = compiler.elementMorphNum = compiler.morphNum++; + compiler.opcode('createElementMorph', morphNum, compiler.elementNum); + compiler.element = null; // Set element to null so we don't cache it twice +} + function distributeMorphs(morphs, opcodes) { if (morphs.length === 0) { return; diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index eb1884d9..3bb40928 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -88,6 +88,10 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { templateSignature += ', blockArguments'; } + var renderNodes = hydrationPrograms.hasMorphs ? + indent+' var morphs = renderNodes(dom, fragment, contextualElement);\n' : + indent+' var morphs = null;'; + var template = '(function() {\n' + this.getChildTemplateVars(indent + ' ') + @@ -101,17 +105,14 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' var dom = env.dom;\n' + this.getHydrationHooks(indent + ' ', this.hydrationCompiler.hooks) + indent+' var contextualElement = options && options.contextualElement;\n' + - indent+' var lastResult = options && options.lastResult;\n' + indent+' dom.detectNamespace(contextualElement);\n' + - indent+' var fragment = lastResult ? lastResult.fragment : null;\n' + - indent+' var morphs = lastResult ? lastResult.morphs : null;\n' + - indent+' fragment = env.hooks.getCachedFragment(this, fragment, env);\n' + - hydrationPrograms.fragmentProcessingProgram + - hydrationPrograms.createMorphsProgram + + indent+' var fragment = env.hooks.getCachedFragment(this, fragment, env);\n' + + renderNodes + hydrationPrograms.hydrateMorphsProgram + - indent+' return lastResult || { fragment: fragment, morphs: morphs };\n' + + indent+' return { fragment: fragment, morphs: morphs };\n' + indent+' }\n' + indent+' };\n' + + hydrationPrograms.createMorphsProgram + indent+'}())'; this.templates.push(template); diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index d77bc096..d6012bba 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -124,18 +124,18 @@ test("Simple elements are created", function() { equalTokens(fragment, "

hello!

content
"); }); -test("Simple elements can be re-rendered", function() { - var template = compile("

hello!

content
"); - var result = template.render({}, env); - var fragment = result.fragment; +//test("Simple elements can be re-rendered", function() { + //var template = compile("

hello!

content
"); + //var result = template.render({}, env); + //var fragment = result.fragment; - var oldFirstChild = fragment.firstChild; + //var oldFirstChild = fragment.firstChild; - fragment = template.render({}, env, { lastResult: result }).fragment; + //fragment = template.render({}, env, { lastResult: result }).fragment; - strictEqual(fragment.firstChild, oldFirstChild); - equalTokens(fragment, "

hello!

content
"); -}); + //strictEqual(fragment.firstChild, oldFirstChild); + //equalTokens(fragment, "

hello!

content
"); +//}); test("Simple elements can have attributes", function() { var template = compile("
content
"); @@ -452,36 +452,36 @@ test("Simple data binding on fragments", function() { equalTokens(fragment, '

brown cow

to the world
'); }); -test("Simple data binding on fragments - re-rendering", function() { - hooks.content = function(env, morph, context, path) { - morph.escaped = false; - morph.setContent(context[path]); - }; +//test("Simple data binding on fragments - re-rendering", function() { + //hooks.content = function(env, morph, context, path) { + //morph.escaped = false; + //morph.setContent(context[path]); + //}; - var object = { title: '

hello

to the' }; - var template = compile('
{{title}} world
'); - var result = template.render(object, env); + //var object = { title: '

hello

to the' }; + //var template = compile('
{{title}} world
'); + //var result = template.render(object, env); - var fragment = result.fragment; + //var fragment = result.fragment; - equalTokens(fragment, '

hello

to the world
'); + //equalTokens(fragment, '

hello

to the world
'); - object.title = '

goodbye

to the'; + //object.title = '

goodbye

to the'; - var oldFirstChild = fragment.firstChild; + //var oldFirstChild = fragment.firstChild; - template.render(object, env, { lastResult: result }); + //template.render(object, env, { lastResult: result }); - strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); - equalTokens(fragment, '

goodbye

to the world
'); + //strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); + //equalTokens(fragment, '

goodbye

to the world
'); - object.title = '

brown cow

to the'; + //object.title = '

brown cow

to the'; - template.render(object, env, { lastResult: result }); + //template.render(object, env, { lastResult: result }); - strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); - equalTokens(fragment, '

brown cow

to the world
'); -}); + //strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); + //equalTokens(fragment, '

brown cow

to the world
'); +//}); test("second render respects whitespace", function () { var template = compile('Hello {{ foo }} '); diff --git a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js index 845022da..d72d7b53 100644 --- a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js +++ b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js @@ -15,12 +15,13 @@ test("simple example", function() { deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], [ "shareElement", [ 0 ] ], - [ "createMorph", [ 0, [ 0 ], 0, 0, true ] ], - [ "createMorph", [ 1, [ 0 ], 2, 2, true ] ], + [ "createMorph", [ 1, [ 0 ], 0, 0, true ] ], + [ "createMorph", [ 2, [ 0 ], 2, 2, true ] ], + [ "createElementMorph", [ 0, 0 ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], - [ "pushLiteral", [ "baz" ] ], [ "printContentHook", [ 1 ] ], + [ "pushLiteral", [ "baz" ] ], + [ "printContentHook", [ 2 ] ], [ "popParent", [] ] ]); }); @@ -121,18 +122,20 @@ test("back to back mustaches should have a text node inserted between them", fun deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], [ "shareElement", [ 0 ] ], - [ "createMorph", [ 0, [0], 0, 0, true ] ], - [ "createMorph", [ 1, [0], 1, 1, true ] ], - [ "createMorph", [ 2, [0], 2, 2, true ] ], - [ "createMorph", [ 3, [0], 4, 4, true] ], + [ "createMorph", [ 1, [0], 0, 0, true ] ], + [ "createMorph", [ 2, [0], 1, 1, true ] ], + [ "createMorph", [ 3, [0], 2, 2, true ] ], + [ "createMorph", [ 4, [0], 4, 4, true] ], + [ "createElementMorph", [ 0, 0 ] ], + [ "repairClonedNode", [ [ 0, 1 ], false ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], - [ "pushLiteral", [ "bar" ] ], [ "printContentHook", [ 1 ] ], - [ "pushLiteral", [ "baz" ] ], + [ "pushLiteral", [ "bar" ] ], [ "printContentHook", [ 2 ] ], - [ "pushLiteral", [ "qux" ] ], + [ "pushLiteral", [ "baz" ] ], [ "printContentHook", [ 3 ] ], + [ "pushLiteral", [ "qux" ] ], + [ "printContentHook", [ 4 ] ], [ "popParent", [] ] ]); }); @@ -162,6 +165,7 @@ test("node mustache", function() { [ "prepareArray", [ 0 ] ], [ "pushLiteral", [ "foo" ] ], [ "shareElement", [ 0 ] ], + [ "createElementMorph", [ 0, 0 ] ], [ "printElementHook", [ 0 ] ], [ "popParent", [] ] ]); @@ -176,6 +180,7 @@ test("node helper", function() { [ "prepareArray", [ 1 ] ], [ "pushLiteral", [ "foo" ] ], [ "shareElement", [ 0 ] ], + [ "createElementMorph", [ 0, 0 ] ], [ "printElementHook", [ 0 ] ], [ "popParent", [] ] ]); @@ -192,8 +197,9 @@ test("attribute mustache", function() { [ "pushConcatHook", [ ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0, 0 ] ], + [ "createElementMorph", [ 0, 0 ] ], + [ "createAttrMorph", [ 1, 0, "class", true, null ] ], + [ "printAttributeHook", [ 1 ] ], [ "popParent", [] ] ]); }); @@ -207,8 +213,9 @@ test("quoted attribute mustache", function() { [ "pushConcatHook", [ ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0, 0 ] ], + [ "createElementMorph", [ 0, 0 ] ], + [ "createAttrMorph", [ 1, 0, "class", true, null ] ], + [ "printAttributeHook", [ 1 ] ], [ "popParent", [] ] ]); }); @@ -220,8 +227,9 @@ test("safe bare attribute mustache", function() { [ "pushGetHook", [ "foo" ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0, 0 ] ], + [ "createElementMorph", [ 0, 0 ] ], + [ "createAttrMorph", [ 1, 0, "class", true, null ] ], + [ "printAttributeHook", [ 1 ] ], [ "popParent", [] ] ]); }); @@ -233,8 +241,9 @@ test("unsafe bare attribute mustache", function() { [ "pushGetHook", [ "foo" ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createAttrMorph", [ 0, 0, "class", false, null ] ], - [ "printAttributeHook", [ 0, 0 ] ], + [ "createElementMorph", [ 0, 0 ] ], + [ "createAttrMorph", [ 1, 0, "class", false, null ] ], + [ "printAttributeHook", [ 1 ] ], [ "popParent", [] ] ]); }); @@ -254,8 +263,9 @@ test("attribute helper", function() { [ "pushConcatHook", [ ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0, 0 ] ], + [ "createElementMorph", [ 0, 0 ] ], + [ "createAttrMorph", [ 1, 0, "class", true, null ] ], + [ "printAttributeHook", [ 1 ] ], [ "popParent", [] ] ]); }); @@ -265,6 +275,7 @@ test("attribute helpers", function() { deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], [ "shareElement", [ 0 ] ], + [ "createElementMorph", [ 0, 0 ] ], [ "pushLiteral", [ " after" ] ], [ "prepareObject", [ 0 ] ], [ "pushLiteral", [ "bar" ] ], @@ -275,24 +286,25 @@ test("attribute helpers", function() { [ "prepareArray", [ 3 ] ], [ "pushConcatHook", [ ] ], [ "pushLiteral", [ "class" ] ], - [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0, 0 ] ], + [ "createAttrMorph", [ 1, 0, "class", true, null ] ], + [ "printAttributeHook", [ 1 ] ], [ "pushGetHook", [ 'bare' ] ], [ "pushLiteral", [ 'id' ] ], - [ "createAttrMorph", [ 1, 0, 'id', true, null ] ], - [ "printAttributeHook", [ 1, 0 ] ], + [ "createAttrMorph", [ 2, 0, 'id', true, null ] ], + [ "printAttributeHook", [ 2 ] ], [ "popParent", [] ], - [ "createMorph", [ 2, [], 0, 1, true ] ], + [ "createMorph", [ 3, [], 0, 1, true ] ], [ "pushLiteral", [ 'morphThing' ] ], - [ "printContentHook", [ 2 ] ], + [ "printContentHook", [ 3 ] ], [ "consumeParent", [ 1 ] ], [ "pushGetHook", [ 'ohMy' ] ], [ "prepareArray", [ 1 ] ], [ "pushConcatHook", [] ], [ "pushLiteral", [ 'class' ] ], [ "shareElement", [ 1 ] ], - [ "createAttrMorph", [ 3, 1, 'class', true, null ] ], - [ "printAttributeHook", [ 3, 1 ] ], + [ "createElementMorph", [ 4, 1 ] ], + [ "createAttrMorph", [ 5, 1, 'class', true, null ] ], + [ "printAttributeHook", [ 5 ] ], [ "popParent", [] ] ]); }); diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index fc2b404c..54194393 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -62,14 +62,14 @@ export function content(env, morph, context, path) { morph.setContent(value); } -export function element(env, domElement, context, path, params, hash) { +export function element(env, morph, context, path, params, hash) { var helper = lookupHelper(env, context, path); if (helper) { - helper.call(context, params, hash, { element: domElement }, env); + helper.call(context, params, hash, { element: morph.element }, env); } } -export function attribute(env, attrMorph, domElement, name, value) { +export function attribute(env, attrMorph, name, value) { attrMorph.setContent(value); } From 2b0312f5ae68f6be83adfb8580a6818aaf4a979c Mon Sep 17 00:00:00 2001 From: Tom Dale Date: Wed, 4 Feb 2015 18:34:42 -0800 Subject: [PATCH 06/27] Fix failing tests --- .../htmlbars-compiler/tests/hydration-opcode-compiler-test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js index d72d7b53..8a2c0171 100644 --- a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js +++ b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js @@ -127,7 +127,6 @@ test("back to back mustaches should have a text node inserted between them", fun [ "createMorph", [ 3, [0], 2, 2, true ] ], [ "createMorph", [ 4, [0], 4, 4, true] ], [ "createElementMorph", [ 0, 0 ] ], - [ "repairClonedNode", [ [ 0, 1 ], false ] ], [ "pushLiteral", [ "foo" ] ], [ "printContentHook", [ 1 ] ], [ "pushLiteral", [ "bar" ] ], @@ -296,7 +295,7 @@ test("attribute helpers", function() { [ "createMorph", [ 3, [], 0, 1, true ] ], [ "pushLiteral", [ 'morphThing' ] ], [ "printContentHook", [ 3 ] ], - [ "consumeParent", [ 1 ] ], + [ "consumeParent", [ 2 ] ], [ "pushGetHook", [ 'ohMy' ] ], [ "prepareArray", [ 1 ] ], [ "pushConcatHook", [] ], From 1066f32e6af267627cfd87c03ee51dc62a167184 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Thu, 5 Feb 2015 11:12:17 -0800 Subject: [PATCH 07/27] renderNodes -> buildRenderNodes --- packages/htmlbars-compiler/lib/hydration-javascript-compiler.js | 2 +- packages/htmlbars-compiler/lib/template-compiler.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index 736ad621..c10c0c36 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -76,7 +76,7 @@ prototype.compile = function(opcodes, options) { if (result.hasMorphs) { result.createMorphsProgram = - ' function renderNodes(dom, fragment, contextualElement) {\n' + + ' function buildRenderNodes(dom, fragment, contextualElement) {\n' + result.fragmentProcessingProgram + morphs + ' return morphs;\n' + diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index 3bb40928..257cc3bf 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -89,7 +89,7 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { } var renderNodes = hydrationPrograms.hasMorphs ? - indent+' var morphs = renderNodes(dom, fragment, contextualElement);\n' : + indent+' var morphs = buildRenderNodes(dom, fragment, contextualElement);\n' : indent+' var morphs = null;'; var template = From fe8096e34d02903cea87c3beff53d065ab8007e8 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Thu, 5 Feb 2015 11:15:14 -0800 Subject: [PATCH 08/27] Too many hooks. --- packages/htmlbars-compiler/lib/compiler.js | 26 ---------------------- 1 file changed, 26 deletions(-) diff --git a/packages/htmlbars-compiler/lib/compiler.js b/packages/htmlbars-compiler/lib/compiler.js index 6b57c9e7..6d1e9c90 100644 --- a/packages/htmlbars-compiler/lib/compiler.js +++ b/packages/htmlbars-compiler/lib/compiler.js @@ -67,29 +67,3 @@ export function template(templateSpec) { export function compile(string, options) { return template(compileSpec(string, options)); } - -/* - * Compile a string into a template spec string. The template spec is a string - * representation of a template. Usually, you would use compileSpec for - * pre-compilation of a template on the server. - * - * Example usage: - * - * var templateSpec = compileSpec("Howdy {{name}}"); - * // This next step is basically what plain compile does - * var template = new Function("return " + templateSpec)(); - * - * @method compileSpec - * @param {String} string An htmlbars template string - * @return {Function} A template spec string - */ -export function compileSpec(string, options) { - var ast = preprocess(string, options); - var compiler = new TemplateCompiler(options); - var program = compiler.compile(ast); - return program; -} - -export function template(program) { - return new Function("return " + program)(); -} From 97f0caf9295f9c91181c0a117d292e13e263e728 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Thu, 5 Feb 2015 13:56:29 -0800 Subject: [PATCH 09/27] Guard if no parent and no contextualElement --- packages/dom-helper/lib/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dom-helper/lib/main.js b/packages/dom-helper/lib/main.js index bb39d038..5a12983e 100644 --- a/packages/dom-helper/lib/main.js +++ b/packages/dom-helper/lib/main.js @@ -367,7 +367,7 @@ prototype.createMorph = function(parent, start, end, contextualElement){ throw new Error("Cannot pass a fragment as the contextual element to createMorph"); } - if (!contextualElement && parent.nodeType === 1) { + if (!contextualElement && parent && parent.nodeType === 1) { contextualElement = parent; } var morph = new Morph(this, contextualElement); From 81b787ac0d7c6a857a50de07785b6bb10bc91927 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Thu, 5 Feb 2015 14:15:03 -0800 Subject: [PATCH 10/27] Expose public API for easy rerendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit finishes the process of breaking apart the template into discrete phases: 1. Building the static DOM “skeleton” 2. Creating morphs (render nodes) that point into the dynamic portions of the DOM skeleton 3. Filling in the dynamic portions with content from the context object By breaking these into discrete steps, we can extract the code that coordinates the steps into library code, significantly reducing the size of compiled templates. This also finally unlocks a very nice API for rerendering a template that reuses the previously generated DOM, rather than starting from a new “skeleton” each time. Not only is updating an existing DOM much less expensive, it allows us to rerender large portions of an application’s UI without losing state like cursor position or scroll position. This technique was pioneered by React. Calling `render()` on a template returns a new object that has both a `fragment` property on it as well as a `rerender()` method. To use re-rendering, call `render(ctx, env, options)` on a template, then save the result. You can put the fragment into the DOM, and when you want to update the template, simply call the `rerender()` method, passing the appropriate context. ```js var result = template.render(model, env, options); document.appendChild(result.fragment); // … later result.rerender(newModel); ``` --- packages/htmlbars-compiler/lib/compiler.js | 3 +- .../lib/hydration-javascript-compiler.js | 3 + .../lib/template-compiler.js | 11 +--- .../tests/html-compiler-test.js | 58 +++++++++---------- .../tests/template-compiler-test.js | 39 ------------- packages/htmlbars-runtime/lib/hooks.js | 35 +++++------ packages/htmlbars-runtime/lib/main.js | 4 +- packages/htmlbars-runtime/lib/render.js | 44 ++++++++++++++ packages/htmlbars-runtime/tests/main-test.js | 1 - 9 files changed, 98 insertions(+), 100 deletions(-) create mode 100644 packages/htmlbars-runtime/lib/render.js diff --git a/packages/htmlbars-compiler/lib/compiler.js b/packages/htmlbars-compiler/lib/compiler.js index 6d1e9c90..fbcf6243 100644 --- a/packages/htmlbars-compiler/lib/compiler.js +++ b/packages/htmlbars-compiler/lib/compiler.js @@ -1,6 +1,7 @@ /*jshint evil:true*/ import { preprocess } from "../htmlbars-syntax/parser"; import TemplateCompiler from "./template-compiler"; +import { wrap } from "../htmlbars-runtime/hooks"; /* * Compile a string into a template spec string. The template spec is a string @@ -65,5 +66,5 @@ export function template(templateSpec) { * @return {Template} A function for rendering the template */ export function compile(string, options) { - return template(compileSpec(string, options)); + return wrap(template(compileSpec(string, options))); } diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index c10c0c36..65e6faa5 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -81,6 +81,9 @@ prototype.compile = function(opcodes, options) { morphs + ' return morphs;\n' + ' }\n'; + } else { + result.createMorphsProgram = + ' function buildRenderNodes() { return []; }\n'; } return result; diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index 257cc3bf..7653d695 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -83,15 +83,11 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { var blockParams = program.blockParams || []; - var templateSignature = 'context, env, options'; + var templateSignature = 'context, rootNode, env, options'; if (blockParams.length > 0) { templateSignature += ', blockArguments'; } - var renderNodes = hydrationPrograms.hasMorphs ? - indent+' var morphs = buildRenderNodes(dom, fragment, contextualElement);\n' : - indent+' var morphs = null;'; - var template = '(function() {\n' + this.getChildTemplateVars(indent + ' ') + @@ -101,15 +97,14 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' cachedFragment: null,\n' + indent+' hasRendered: false,\n' + indent+' build: ' + fragmentProgram + ',\n' + + indent+' buildRenderNodes: buildRenderNodes,\n' + indent+' render: function render(' + templateSignature + ') {\n' + indent+' var dom = env.dom;\n' + this.getHydrationHooks(indent + ' ', this.hydrationCompiler.hooks) + indent+' var contextualElement = options && options.contextualElement;\n' + indent+' dom.detectNamespace(contextualElement);\n' + - indent+' var fragment = env.hooks.getCachedFragment(this, fragment, env);\n' + - renderNodes + + indent+' var morphs = rootNode.childNodes;\n' + hydrationPrograms.hydrateMorphsProgram + - indent+' return { fragment: fragment, morphs: morphs };\n' + indent+' }\n' + indent+' };\n' + hydrationPrograms.createMorphsProgram + diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index d6012bba..e4aee5f4 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -124,18 +124,18 @@ test("Simple elements are created", function() { equalTokens(fragment, "

hello!

content
"); }); -//test("Simple elements can be re-rendered", function() { - //var template = compile("

hello!

content
"); - //var result = template.render({}, env); - //var fragment = result.fragment; +test("Simple elements can be re-rendered", function() { + var template = compile("

hello!

content
"); + var result = template.render({}, env); + var fragment = result.fragment; - //var oldFirstChild = fragment.firstChild; + var oldFirstChild = fragment.firstChild; - //fragment = template.render({}, env, { lastResult: result }).fragment; + result.rerender(); - //strictEqual(fragment.firstChild, oldFirstChild); - //equalTokens(fragment, "

hello!

content
"); -//}); + strictEqual(fragment.firstChild, oldFirstChild); + equalTokens(fragment, "

hello!

content
"); +}); test("Simple elements can have attributes", function() { var template = compile("
content
"); @@ -452,36 +452,36 @@ test("Simple data binding on fragments", function() { equalTokens(fragment, '

brown cow

to the world
'); }); -//test("Simple data binding on fragments - re-rendering", function() { - //hooks.content = function(env, morph, context, path) { - //morph.escaped = false; - //morph.setContent(context[path]); - //}; +test("Simple data binding on fragments - re-rendering", function() { + hooks.content = function(env, morph, context, path) { + morph.parseTextAsHTML = true; + morph.setContent(context[path]); + }; - //var object = { title: '

hello

to the' }; - //var template = compile('
{{title}} world
'); - //var result = template.render(object, env); + var object = { title: '

hello

to the' }; + var template = compile('
{{title}} world
'); + var result = template.render(object, env); - //var fragment = result.fragment; + var fragment = result.fragment; - //equalTokens(fragment, '

hello

to the world
'); + equalTokens(fragment, '

hello

to the world
'); - //object.title = '

goodbye

to the'; + object.title = '

goodbye

to the'; - //var oldFirstChild = fragment.firstChild; + var oldFirstChild = fragment.firstChild; - //template.render(object, env, { lastResult: result }); + result.rerender(object); - //strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); - //equalTokens(fragment, '

goodbye

to the world
'); + strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); + equalTokens(fragment, '

goodbye

to the world
'); - //object.title = '

brown cow

to the'; + object.title = '

brown cow

to the'; - //template.render(object, env, { lastResult: result }); + result.rerender(object); - //strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); - //equalTokens(fragment, '

brown cow

to the world
'); -//}); + strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); + equalTokens(fragment, '

brown cow

to the world
'); +}); test("second render respects whitespace", function () { var template = compile('Hello {{ foo }} '); diff --git a/packages/htmlbars-compiler/tests/template-compiler-test.js b/packages/htmlbars-compiler/tests/template-compiler-test.js index a741a7ca..295c7870 100644 --- a/packages/htmlbars-compiler/tests/template-compiler-test.js +++ b/packages/htmlbars-compiler/tests/template-compiler-test.js @@ -1,15 +1,8 @@ import TemplateCompiler from "../htmlbars-compiler/template-compiler"; import { preprocess } from "../htmlbars-syntax/parser"; -import { equalHTML } from "../htmlbars-test-helpers"; -import defaultHooks from "../htmlbars-runtime/hooks"; -import defaultHelpers from "../htmlbars-runtime/helpers"; -import { merge } from "../htmlbars-util/object-utils"; -import DOMHelper from "../dom-helper"; QUnit.module("TemplateCompiler"); -var dom, hooks, helpers; - function countNamespaceChanges(template) { var ast = preprocess(template); var compiler = new TemplateCompiler(); @@ -18,38 +11,6 @@ function countNamespaceChanges(template) { return matches ? matches.length : 0; } -test("it works", function testFunction() { - /* jshint evil: true */ - var ast = preprocess('
{{#if working}}Hello {{firstName}} {{lastName}}!{{/if}}
'); - var compiler = new TemplateCompiler(); - var program = compiler.compile(ast); - var template = new Function("return " + program)(); - - dom = new DOMHelper(); - hooks = merge({}, defaultHooks); - helpers = merge({}, defaultHelpers); - - var env = { - dom: dom, - hooks: hooks, - helpers: helpers - }; - - env.helpers['if'] = function(params, hash, options) { - if (params[0]) { - return options.template.render(context, env, { contextualElement: options.morph.contextualElement }); - } - }; - - var context = { - working: true, - firstName: 'Kris', - lastName: 'Selden' - }; - var frag = template.render(context, env, document.body).fragment; - equalHTML(frag, '
Hello Kris Selden!
'); -}); - test("it omits unnecessary namespace changes", function () { equal(countNamespaceChanges('
'), 0); // sanity check equal(countNamespaceChanges('
'), 1); diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 54194393..a1287244 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -1,29 +1,22 @@ -export function getCachedFragment(template, fragment, env) { - var dom = env.dom; - if (!fragment && env.useFragmentCache && dom.canClone) { - if (template.cachedFragment === null) { - fragment = template.build(dom); - if (template.hasRendered) { - template.cachedFragment = fragment; - } else { - template.hasRendered = true; - } - } - if (template.cachedFragment) { - fragment = dom.cloneNode(template.cachedFragment, true); - } - } else if (!fragment) { - fragment = template.build(dom); - } +import render from "./render"; - return fragment; +export function wrap(template) { + if (template === null) { return null; } + + return { + isHTMLBars: true, + blockParams: template.blockParams, + render: function(context, env, options, blockArguments) { + return render(template, context, env, options, blockArguments); + } + }; } export function block(env, morph, context, path, params, hash, template, inverse) { var options = { morph: morph, - template: template, - inverse: inverse, + template: wrap(template), + inverse: wrap(inverse), lastResult: morph.lastResult }; @@ -105,6 +98,7 @@ export function set(env, context, name, value) { export function component(env, morph, context, tagName, attrs, template) { var helper = lookupHelper(env, context, tagName); + template = wrap(template); var value; if (helper) { @@ -143,7 +137,6 @@ function lookupHelper(env, context, helperName) { } export default { - getCachedFragment: getCachedFragment, content: content, block: block, inline: inline, diff --git a/packages/htmlbars-runtime/lib/main.js b/packages/htmlbars-runtime/lib/main.js index d8d144e1..2665f5dd 100644 --- a/packages/htmlbars-runtime/lib/main.js +++ b/packages/htmlbars-runtime/lib/main.js @@ -1,7 +1,9 @@ import hooks from 'htmlbars-runtime/hooks'; import helpers from 'htmlbars-runtime/helpers'; +import render from 'htmlbars-runtime/render'; export { hooks, - helpers + helpers, + render }; diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js new file mode 100644 index 00000000..3a79ebc5 --- /dev/null +++ b/packages/htmlbars-runtime/lib/render.js @@ -0,0 +1,44 @@ +export default function render(template, context, env, options, blockArguments) { + var dom = env.dom; + var contextualElement = options && options.contextualElement; + + dom.detectNamespace(contextualElement); + + var fragment = getCachedFragment(template, env); + var nodes = template.buildRenderNodes(dom, fragment, contextualElement); + + var rootNode = dom.createMorph(null, fragment.firstChild, fragment.lastChild, contextualElement); + rootNode.childNodes = nodes; + + template.render(context, rootNode, env, options, blockArguments); + + return { + root: rootNode, + fragment: fragment, + rerender: function(newContext, newEnv, newOptions) { + template.render(newContext, rootNode, newEnv || env, newOptions || options); + } + }; +} + +export function getCachedFragment(template, env) { + var dom = env.dom, fragment; + if (env.useFragmentCache && dom.canClone) { + if (template.cachedFragment === null) { + fragment = template.build(dom); + if (template.hasRendered) { + template.cachedFragment = fragment; + } else { + template.hasRendered = true; + } + } + if (template.cachedFragment) { + fragment = dom.cloneNode(template.cachedFragment, true); + } + } else if (!fragment) { + fragment = template.build(dom); + } + + return fragment; +} + diff --git a/packages/htmlbars-runtime/tests/main-test.js b/packages/htmlbars-runtime/tests/main-test.js index 5af3a21e..eeba830b 100644 --- a/packages/htmlbars-runtime/tests/main-test.js +++ b/packages/htmlbars-runtime/tests/main-test.js @@ -14,7 +14,6 @@ function keys(obj) { test("hooks are present", function () { var hookNames = [ - "getCachedFragment", "content", "inline", "block", From ddccc306547e08f0732aaa2af643cacac3136e98 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Thu, 5 Feb 2015 18:51:21 -0800 Subject: [PATCH 11/27] Block helpers mutate in place rather than return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, invoking a block helper would always return a string or element that would be inserted into the parent fragment. This model assumed that the root-most template was always being rendered exactly once. With the introduction of re-rendering, block helpers need to handle this case as well. Specifically, instead of just creating rendering a new element each time, they should populate a provided morph with the contents they want to display. As an optimization, you can also save off the result of the previous render and `rerender()` it. This change is necessary to support re-rendering, but we consider it a low-level API. We intended to create a high-level API for authors to use that handles the most frequent rendering and re-rendering patterns. In particular, the test titled “Templates with block helpers - re-rendering” shows a low-level pattern that can be used to support efficient re-rendering in general, and which should be easy to extract into something higher level once we have made sure we have handled all of the necessary cases. --- packages/dom-helper/lib/main.js | 2 + packages/htmlbars-compiler/lib/compiler.js | 3 +- .../tests/html-compiler-test.js | 120 ++++++++++++------ packages/htmlbars-runtime/lib/helpers.js | 2 +- packages/htmlbars-runtime/lib/hooks.js | 36 ++---- packages/htmlbars-runtime/lib/render.js | 8 +- packages/morph-attr/lib/main.js | 1 + 7 files changed, 106 insertions(+), 66 deletions(-) diff --git a/packages/dom-helper/lib/main.js b/packages/dom-helper/lib/main.js index 5a12983e..add1f370 100644 --- a/packages/dom-helper/lib/main.js +++ b/packages/dom-helper/lib/main.js @@ -109,6 +109,8 @@ function ElementMorph(element, dom, namespace) { this.element = element; this.dom = dom; this.namespace = namespace; + + this.state = {}; } /* diff --git a/packages/htmlbars-compiler/lib/compiler.js b/packages/htmlbars-compiler/lib/compiler.js index fbcf6243..e68a7c80 100644 --- a/packages/htmlbars-compiler/lib/compiler.js +++ b/packages/htmlbars-compiler/lib/compiler.js @@ -2,6 +2,7 @@ import { preprocess } from "../htmlbars-syntax/parser"; import TemplateCompiler from "./template-compiler"; import { wrap } from "../htmlbars-runtime/hooks"; +import render from "../htmlbars-runtime/render"; /* * Compile a string into a template spec string. The template spec is a string @@ -66,5 +67,5 @@ export function template(templateSpec) { * @return {Template} A function for rendering the template */ export function compile(string, options) { - return wrap(template(compileSpec(string, options))); + return wrap(template(compileSpec(string, options)), render); } diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index e4aee5f4..9f725f3c 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -344,7 +344,7 @@ test("The compiler can handle top-level unescaped td inside tr contextualElement test("The compiler can handle unescaped tr in top of content", function() { registerHelper('test', function(params, hash, options, env) { - return options.template.render(this, env, { contextualElement: options.morph.contextualElement }); + return options.template.render(this, env, options); }); var template = compile('{{#test}}{{{html}}}{{/test}}'); @@ -358,7 +358,7 @@ test("The compiler can handle unescaped tr in top of content", function() { test("The compiler can handle unescaped tr inside fragment table", function() { registerHelper('test', function(params, hash, options, env) { - return options.template.render(this, env, { contextualElement: options.morph.contextualElement }); + return options.template.render(this, env, options); }); var template = compile('{{#test}}{{{html}}}{{/test}}
'); @@ -483,6 +483,43 @@ test("Simple data binding on fragments - re-rendering", function() { equalTokens(fragment, '

brown cow

to the world
'); }); +test("Templates with block helpers - re-rendering", function() { + // This represents the internals of a higher-level helper API + registerHelper('if', function(params, hash, options, env) { + var renderNode = options.renderNode; + var state = renderNode.state; + var value = params[0]; + var normalized = !!value; + + if (state.condition !== normalized) { + state.condition = normalized; + + if (normalized) { + state.lastResult = options.template.render(this, env, options); + } else { + state.lastResult = options.inverse.render(this, env, options); + } + } else { + state.lastResult.rerender(this); + } + }); + + var object = { condition: true, value: 'hello world' }; + var template = compile('
{{#if condition}}

{{value}}

{{else}}

Nothing

{{/if}}
'); + var result = template.render(object, env); + + equalTokens(result.fragment, '

hello world

'); + + object.value = 'goodbye world'; + result.rerender(object); + equalTokens(result.fragment, '

goodbye world

'); + + object.condition = false; + result.rerender(object); + + equalTokens(result.fragment, '

Nothing

'); +}); + test("second render respects whitespace", function () { var template = compile('Hello {{ foo }} '); template.render({}, env, { contextualElement: document.createElement('div') }); @@ -515,16 +552,16 @@ test("Morphs are escaped correctly", function() { expect(10); registerHelper('testing-unescaped', function(params, hash, options) { - equal(options.morph.parseTextAsHTML, true); + equal(options.renderNode.parseTextAsHTML, true); return params[0]; }); registerHelper('testing-escaped', function(params, hash, options, env) { - equal(options.morph.parseTextAsHTML, false); + equal(options.renderNode.parseTextAsHTML, false); if (options.template) { - return options.template.render({}, env, { contextualElement: options.morph.contextualElement }); + return options.template.render({}, env, options); } return params[0]; @@ -725,7 +762,7 @@ test("Attribute runs can contain helpers", function() { */ test("A simple block helper can return the default document fragment", function() { registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env, { lastResult: options.lastResult }); + return options.template.render(this, env, options); }); compilesTo('{{#testing}}
123
{{/testing}}', '
123
'); @@ -734,7 +771,7 @@ test("A simple block helper can return the default document fragment", function( // TODO: NEXT test("A simple block helper can return text", function() { registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env); + return options.template.render(this, env, options); }); compilesTo('{{#testing}}test{{else}}not shown{{/testing}}', 'test'); @@ -742,7 +779,7 @@ test("A simple block helper can return text", function() { test("A block helper can have an else block", function() { registerHelper('testing', function(params, hash, options, env) { - return options.inverse.render(this, env); + return options.inverse.render(this, env, options); }); compilesTo('{{#testing}}Nope{{else}}
123
{{/testing}}', '
123
'); @@ -751,7 +788,7 @@ test("A block helper can have an else block", function() { test("A block helper can pass a context to be used in the child", function() { registerHelper('testing', function(params, hash, options, env) { var context = { title: 'Rails is omakase' }; - return options.template.render(context, env); + return options.template.render(context, env, options); }); compilesTo('{{#testing}}
{{title}}
{{/testing}}', '
Rails is omakase
'); @@ -760,7 +797,7 @@ test("A block helper can pass a context to be used in the child", function() { test("Block helpers receive hash arguments", function() { registerHelper('testing', function(params, hash, options, env) { if (hash.truth) { - return options.template.render(this, env); + return options.template.render(this, env, options); } }); @@ -840,10 +877,14 @@ test("Node helpers can be used for attribute bindings", function() { test('Components - Called as helpers', function () { + var xAppendComponent = compile('{{yield}}{{text}}'); + registerHelper('x-append', function(params, hash, options, env) { - var result = options.template.render(this, env, { contextualElement: options.morph.contextualElement }); - result.fragment.appendChild(document.createTextNode(hash.text)); - return result; + var rootNode = options.renderNode; + options.renderNode = null; + var result = options.template.render(this, env, options); + options.renderNode = rootNode; + xAppendComponent.render({ yield: result.fragment, text: hash.text }, env, options); }); var object = { bar: 'e', baz: 'c' }; compilesTo('ab{{baz}}f','abcdef', object); @@ -886,25 +927,32 @@ test("Simple elements can have dashed attributes", function() { equalTokens(fragment, '
content
'); }); +function yieldTemplate(parentTemplate, options, callback) { + var node = options.renderNode; + options.renderNode = null; + var child = callback(); + options.renderNode = node; + compile(parentTemplate).render({ yield: child.fragment }, env, options); +} + test("Block params", function() { registerHelper('a', function(params, hash, options, env) { var context = createObject(this); - var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, { contextualElement: document.body }, ['W', 'X1']).fragment); - return 'A(' + span.innerHTML + ')'; + yieldTemplate("A({{yield}})", options, function() { + return options.template.render(context, env, options, ['W', 'X1']); + }); }); registerHelper('b', function(params, hash, options, env) { var context = createObject(this); - var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, { contextualElement: document.body }, ['X2', 'Y']).fragment); - return 'B(' + span.innerHTML + ')'; + yieldTemplate("B({{yield}})", options, function() { + return options.template.render(context, env, options, ['X2', 'Y']); + }); }); registerHelper('c', function(params, hash, options, env) { var context = createObject(this); - var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, { contextualElement: document.body }, ['Z']).fragment); - return 'C(' + span.innerHTML + ')'; - // return "C(" + options.template.render() + ")"; + yieldTemplate("C({{yield}})", options, function() { + return options.template.render(context, env, options, ['Z']); + }); }); var t = '{{#a as |w x|}}{{w}},{{x}} {{#b as |x y|}}{{x}},{{y}}{{/b}} {{w}},{{x}} {{#c as |z|}}{{x}},{{z}}{{/c}}{{/a}}'; compilesTo(t, 'A(W,X1 B(X2,Y) W,X1 C(X1,Z))', {}); @@ -925,10 +973,10 @@ test("Block params - Helper should know how many block params it was called with test('Block params in HTML syntax', function () { registerHelper('x-bar', function(params, hash, options, env) { - var context = createObject(this); - var span = document.createElement('span'); - span.appendChild(options.template.render(context, env, { contextualElement: document.body }, ['Xerxes', 'York', 'Zed']).fragment); - return 'BAR(' + span.innerHTML + ')'; + var context = this; + yieldTemplate("BAR({{yield}})", options, function() { + return options.template.render(context, env, options, ['Xerxes', 'York', 'Zed']); + }); }); compilesTo('{{zee}},{{y}},{{x}}', 'BAR(Zed,York,Xerxes)', {}); }); @@ -947,7 +995,7 @@ test('Block params in HTML syntax - Throws exception if given zero parameters', test('Block params in HTML syntax - Works with a single parameter', function () { registerHelper('x-bar', function(params, hash, options, env) { - return options.template.render({}, env, { contextualElement: document.body }, ['Xerxes']); + return options.template.render({}, env, options, ['Xerxes']); }); compilesTo('{{x}}', 'Xerxes', {}); }); @@ -963,7 +1011,7 @@ test('Block params in HTML syntax - Ignores whitespace', function () { expect(3); registerHelper('x-bar', function(params, hash, options) { - return options.template.render({}, env, { contextualElement: document.body }, ['Xerxes', 'York']); + return options.template.render({}, env, options, ['Xerxes', 'York']); }); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); @@ -1258,11 +1306,10 @@ test("Block helper allows interior namespace", function() { var isTrue = true; registerHelper('testing', function(params, hash, options, env) { - var morph = options.morph; if (isTrue) { - return options.template.render(this, env, { contextualElement: morph.contextualElement }); + return options.template.render(this, env, options); } else { - return options.inverse.render(this, env, { contextualElement: morph.contextualElement }); + return options.inverse.render(this, env, options); } }); @@ -1285,8 +1332,7 @@ test("Block helper allows interior namespace", function() { test("Block helper allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { - var morph = options.morph; - return options.template.render(this, env, { contextualElement: morph.contextualElement }); + return options.template.render(this, env, options); }); var template = compile('
{{#testing}}{{/testing}}
'); @@ -1301,8 +1347,7 @@ test("Block helper allows namespace to bleed through", function() { test("Block helper with root svg allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { - var morph = options.morph; - return options.template.render(this, env, { contextualElement: morph.contextualElement }); + return options.template.render(this, env, options); }); var template = compile('{{#testing}}{{/testing}}'); @@ -1317,8 +1362,7 @@ test("Block helper with root svg allows namespace to bleed through", function() test("Block helper with root foreignObject allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options, env) { - var morph = options.morph; - return options.template.render(this, env, { contextualElement: morph.contextualElement }); + return options.template.render(this, env, options); }); var template = compile('{{#testing}}
{{/testing}}
'); diff --git a/packages/htmlbars-runtime/lib/helpers.js b/packages/htmlbars-runtime/lib/helpers.js index 02e1dedb..87c4f3d2 100644 --- a/packages/htmlbars-runtime/lib/helpers.js +++ b/packages/htmlbars-runtime/lib/helpers.js @@ -1,6 +1,6 @@ export function partial(params, hash, options, env) { var template = env.partials[params[0]]; - return template.render(this, env, options.morph.contextualElement).fragment; + return template.render(this, env, options.renderNode.contextualElement).fragment; } export default { diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index a1287244..cd314dbe 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -14,30 +14,19 @@ export function wrap(template) { export function block(env, morph, context, path, params, hash, template, inverse) { var options = { - morph: morph, + renderNode: morph, + contextualElement: morph.contextualElement, template: wrap(template), inverse: wrap(inverse), - lastResult: morph.lastResult }; var helper = lookupHelper(env, context, path); - var result = helper.call(context, params, hash, options, env); - - setResultOnMorph(morph, result); -} - -function setResultOnMorph(morph, result) { - if (typeof result !== 'object') { - morph.setContent(result); - } else { - morph.lastResult = result; - morph.setContent(result.fragment); - } + helper.call(context, params, hash, options, env); } export function inline(env, morph, context, path, params, hash) { var helper = lookupHelper(env, context, path); - var value = helper.call(context, params, hash, { morph: morph }, env); + var value = helper.call(context, params, hash, { renderNode: morph }, env); morph.setContent(value); } @@ -47,9 +36,9 @@ export function content(env, morph, context, path) { var value; if (helper) { - value = helper.call(context, [], {}, { morph: morph }, env); + value = helper.call(context, [], {}, { renderNode: morph }, env); } else { - value = get(env, context, path); + value = env.hooks.get(env, context, path); } morph.setContent(value); @@ -71,7 +60,7 @@ export function subexpr(env, context, helperName, params, hash) { if (helper) { return helper.call(context, params, hash, {}, env); } else { - return get(env, context, helperName); + return env.hooks.get(env, context, helperName); } } @@ -100,18 +89,15 @@ export function component(env, morph, context, tagName, attrs, template) { var helper = lookupHelper(env, context, tagName); template = wrap(template); - var value; if (helper) { var options = { - morph: morph, + renderNode: morph, template: template }; - value = helper.call(context, [], attrs, options, env); - setResultOnMorph(morph, value); + helper.call(context, [], attrs, options, env); } else { - value = componentFallback(env, morph, context, tagName, attrs, template); - morph.setContent(value); + componentFallback(env, morph, context, tagName, attrs, template); } } @@ -129,7 +115,7 @@ function componentFallback(env, morph, context, tagName, attrs, template) { element.setAttribute(name, attrs[name]); } element.appendChild(template.render(context, env, { contextualElement: morph.contextualElement }).fragment); - return element; + morph.setNode(element); } function lookupHelper(env, context, helperName) { diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index 3a79ebc5..830c5358 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -7,11 +7,17 @@ export default function render(template, context, env, options, blockArguments) var fragment = getCachedFragment(template, env); var nodes = template.buildRenderNodes(dom, fragment, contextualElement); - var rootNode = dom.createMorph(null, fragment.firstChild, fragment.lastChild, contextualElement); + var rootNode = (options && options.renderNode) || + dom.createMorph(null, fragment.firstChild, fragment.lastChild, contextualElement); + rootNode.childNodes = nodes; template.render(context, rootNode, env, options, blockArguments); + if (options && options.renderNode) { + rootNode.setContent(fragment); + } + return { root: rootNode, fragment: fragment, diff --git a/packages/morph-attr/lib/main.js b/packages/morph-attr/lib/main.js index 33da989e..bd8e92a7 100644 --- a/packages/morph-attr/lib/main.js +++ b/packages/morph-attr/lib/main.js @@ -27,6 +27,7 @@ function AttrMorph(element, attrName, domHelper, namespace) { this.element = element; this.domHelper = domHelper; this.namespace = namespace !== undefined ? namespace : getAttrNamespace(attrName); + this.state = {}; this.escaped = true; var normalizedAttrName = normalizeProperty(this.element, attrName); From de981e2426c7d4d4cf55ce2caa5d18ac5786f049 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Fri, 6 Feb 2015 13:25:59 -0800 Subject: [PATCH 12/27] Rename rerender to revalidate --- .../htmlbars-compiler/tests/html-compiler-test.js | 12 ++++++------ packages/htmlbars-runtime/lib/render.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index 9f725f3c..debc7efa 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -131,7 +131,7 @@ test("Simple elements can be re-rendered", function() { var oldFirstChild = fragment.firstChild; - result.rerender(); + result.revalidate(); strictEqual(fragment.firstChild, oldFirstChild); equalTokens(fragment, "

hello!

content
"); @@ -470,14 +470,14 @@ test("Simple data binding on fragments - re-rendering", function() { var oldFirstChild = fragment.firstChild; - result.rerender(object); + result.revalidate(object); strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); equalTokens(fragment, '

goodbye

to the world
'); object.title = '

brown cow

to the'; - result.rerender(object); + result.revalidate(object); strictEqual(fragment.firstChild, oldFirstChild, "Static nodes in the fragment should have stable identity"); equalTokens(fragment, '

brown cow

to the world
'); @@ -500,7 +500,7 @@ test("Templates with block helpers - re-rendering", function() { state.lastResult = options.inverse.render(this, env, options); } } else { - state.lastResult.rerender(this); + state.lastResult.revalidate(this); } }); @@ -511,11 +511,11 @@ test("Templates with block helpers - re-rendering", function() { equalTokens(result.fragment, '

hello world

'); object.value = 'goodbye world'; - result.rerender(object); + result.revalidate(object); equalTokens(result.fragment, '

goodbye world

'); object.condition = false; - result.rerender(object); + result.revalidate(object); equalTokens(result.fragment, '

Nothing

'); }); diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index 830c5358..3e19a462 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -21,7 +21,7 @@ export default function render(template, context, env, options, blockArguments) return { root: rootNode, fragment: fragment, - rerender: function(newContext, newEnv, newOptions) { + revalidate: function(newContext, newEnv, newOptions) { template.render(newContext, rootNode, newEnv || env, newOptions || options); } }; From 2fd475f0390c163f8a9f88cd3643aa1c3c5bb28a Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Fri, 6 Feb 2015 13:45:30 -0800 Subject: [PATCH 13/27] Simplify template rendering in block helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, invoking a child template in a block helper looked like this: ```js registerHelper(‘identity’, function(params, hash, options, env) { return options.template.render(this, env, options, blockParams); }); ``` It now looks like this: ```js registerHelper(‘identity’, function(params, hash, options, env) { return options.template.render(this, blockParams); }); ``` --- .../tests/html-compiler-test.js | 80 +++++++++---------- packages/htmlbars-runtime/lib/hooks.js | 37 ++++++--- 2 files changed, 66 insertions(+), 51 deletions(-) diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index debc7efa..d1d9ed43 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -343,8 +343,8 @@ test("The compiler can handle top-level unescaped td inside tr contextualElement }); test("The compiler can handle unescaped tr in top of content", function() { - registerHelper('test', function(params, hash, options, env) { - return options.template.render(this, env, options); + registerHelper('test', function(params, hash, options) { + return options.template.render(this); }); var template = compile('{{#test}}{{{html}}}{{/test}}'); @@ -357,8 +357,8 @@ test("The compiler can handle unescaped tr in top of content", function() { }); test("The compiler can handle unescaped tr inside fragment table", function() { - registerHelper('test', function(params, hash, options, env) { - return options.template.render(this, env, options); + registerHelper('test', function(params, hash, options) { + return options.template.render(this); }); var template = compile('{{#test}}{{{html}}}{{/test}}
'); @@ -485,7 +485,7 @@ test("Simple data binding on fragments - re-rendering", function() { test("Templates with block helpers - re-rendering", function() { // This represents the internals of a higher-level helper API - registerHelper('if', function(params, hash, options, env) { + registerHelper('if', function(params, hash, options) { var renderNode = options.renderNode; var state = renderNode.state; var value = params[0]; @@ -495,9 +495,9 @@ test("Templates with block helpers - re-rendering", function() { state.condition = normalized; if (normalized) { - state.lastResult = options.template.render(this, env, options); + state.lastResult = options.template.render(this); } else { - state.lastResult = options.inverse.render(this, env, options); + state.lastResult = options.inverse.render(this); } } else { state.lastResult.revalidate(this); @@ -557,11 +557,11 @@ test("Morphs are escaped correctly", function() { return params[0]; }); - registerHelper('testing-escaped', function(params, hash, options, env) { + registerHelper('testing-escaped', function(params, hash, options) { equal(options.renderNode.parseTextAsHTML, false); if (options.template) { - return options.template.render({}, env, options); + return options.template.render({}); } return params[0]; @@ -761,8 +761,8 @@ test("Attribute runs can contain helpers", function() { }); */ test("A simple block helper can return the default document fragment", function() { - registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env, options); + registerHelper('testing', function(params, hash, options) { + return options.template.render(this); }); compilesTo('{{#testing}}
123
{{/testing}}', '
123
'); @@ -770,34 +770,34 @@ test("A simple block helper can return the default document fragment", function( // TODO: NEXT test("A simple block helper can return text", function() { - registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env, options); + registerHelper('testing', function(params, hash, options) { + return options.template.render(this); }); compilesTo('{{#testing}}test{{else}}not shown{{/testing}}', 'test'); }); test("A block helper can have an else block", function() { - registerHelper('testing', function(params, hash, options, env) { - return options.inverse.render(this, env, options); + registerHelper('testing', function(params, hash, options) { + return options.inverse.render(this); }); compilesTo('{{#testing}}Nope{{else}}
123
{{/testing}}', '
123
'); }); test("A block helper can pass a context to be used in the child", function() { - registerHelper('testing', function(params, hash, options, env) { + registerHelper('testing', function(params, hash, options) { var context = { title: 'Rails is omakase' }; - return options.template.render(context, env, options); + return options.template.render(context); }); compilesTo('{{#testing}}
{{title}}
{{/testing}}', '
Rails is omakase
'); }); test("Block helpers receive hash arguments", function() { - registerHelper('testing', function(params, hash, options, env) { + registerHelper('testing', function(params, hash, options) { if (hash.truth) { - return options.template.render(this, env, options); + return options.template.render(this); } }); @@ -882,7 +882,7 @@ test('Components - Called as helpers', function () { registerHelper('x-append', function(params, hash, options, env) { var rootNode = options.renderNode; options.renderNode = null; - var result = options.template.render(this, env, options); + var result = options.template.render(this); options.renderNode = rootNode; xAppendComponent.render({ yield: result.fragment, text: hash.text }, env, options); }); @@ -936,22 +936,22 @@ function yieldTemplate(parentTemplate, options, callback) { } test("Block params", function() { - registerHelper('a', function(params, hash, options, env) { + registerHelper('a', function(params, hash, options) { var context = createObject(this); yieldTemplate("A({{yield}})", options, function() { - return options.template.render(context, env, options, ['W', 'X1']); + return options.template.render(context, ['W', 'X1']); }); }); - registerHelper('b', function(params, hash, options, env) { + registerHelper('b', function(params, hash, options) { var context = createObject(this); yieldTemplate("B({{yield}})", options, function() { - return options.template.render(context, env, options, ['X2', 'Y']); + return options.template.render(context, ['X2', 'Y']); }); }); - registerHelper('c', function(params, hash, options, env) { + registerHelper('c', function(params, hash, options) { var context = createObject(this); yieldTemplate("C({{yield}})", options, function() { - return options.template.render(context, env, options, ['Z']); + return options.template.render(context, ['Z']); }); }); var t = '{{#a as |w x|}}{{w}},{{x}} {{#b as |x y|}}{{x}},{{y}}{{/b}} {{w}},{{x}} {{#c as |z|}}{{x}},{{z}}{{/c}}{{/a}}'; @@ -972,10 +972,10 @@ test("Block params - Helper should know how many block params it was called with }); test('Block params in HTML syntax', function () { - registerHelper('x-bar', function(params, hash, options, env) { + registerHelper('x-bar', function(params, hash, options) { var context = this; yieldTemplate("BAR({{yield}})", options, function() { - return options.template.render(context, env, options, ['Xerxes', 'York', 'Zed']); + return options.template.render(context, ['Xerxes', 'York', 'Zed']); }); }); compilesTo('{{zee}},{{y}},{{x}}', 'BAR(Zed,York,Xerxes)', {}); @@ -994,8 +994,8 @@ test('Block params in HTML syntax - Throws exception if given zero parameters', test('Block params in HTML syntax - Works with a single parameter', function () { - registerHelper('x-bar', function(params, hash, options, env) { - return options.template.render({}, env, options, ['Xerxes']); + registerHelper('x-bar', function(params, hash, options) { + return options.template.render({}, ['Xerxes']); }); compilesTo('{{x}}', 'Xerxes', {}); }); @@ -1011,7 +1011,7 @@ test('Block params in HTML syntax - Ignores whitespace', function () { expect(3); registerHelper('x-bar', function(params, hash, options) { - return options.template.render({}, env, options, ['Xerxes', 'York']); + return options.template.render({}, ['Xerxes', 'York']); }); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); @@ -1305,11 +1305,11 @@ test("root svg can take some hydration", function() { test("Block helper allows interior namespace", function() { var isTrue = true; - registerHelper('testing', function(params, hash, options, env) { + registerHelper('testing', function(params, hash, options) { if (isTrue) { - return options.template.render(this, env, options); + return options.template.render(this); } else { - return options.inverse.render(this, env, options); + return options.inverse.render(this); } }); @@ -1331,8 +1331,8 @@ test("Block helper allows interior namespace", function() { }); test("Block helper allows namespace to bleed through", function() { - registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env, options); + registerHelper('testing', function(params, hash, options) { + return options.template.render(this); }); var template = compile('
{{#testing}}{{/testing}}
'); @@ -1346,8 +1346,8 @@ test("Block helper allows namespace to bleed through", function() { }); test("Block helper with root svg allows namespace to bleed through", function() { - registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env, options); + registerHelper('testing', function(params, hash, options) { + return options.template.render(this); }); var template = compile('{{#testing}}{{/testing}}'); @@ -1361,8 +1361,8 @@ test("Block helper with root svg allows namespace to bleed through", function() }); test("Block helper with root foreignObject allows namespace to bleed through", function() { - registerHelper('testing', function(params, hash, options, env) { - return options.template.render(this, env, options); + registerHelper('testing', function(params, hash, options) { + return options.template.render(this); }); var template = compile('{{#testing}}
{{/testing}}
'); diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index cd314dbe..1c97272a 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -12,14 +12,34 @@ export function wrap(template) { }; } -export function block(env, morph, context, path, params, hash, template, inverse) { +export function wrapForHelper(template, options, env) { + if (template === null) { return null; } + + return { + isHTMLBars: true, + blockParams: template.blockParams, + render: function(context, blockArguments) { + return render(template, context, env, options, blockArguments); + } + }; +} + +function optionsFor(morph, env, template, inverse) { var options = { renderNode: morph, contextualElement: morph.contextualElement, - template: wrap(template), - inverse: wrap(inverse), + env: env }; + options.template = wrapForHelper(template, options, env); + options.inverse = wrapForHelper(inverse, options, env); + + return options; +} + +export function block(env, morph, context, path, params, hash, template, inverse) { + var options = optionsFor(morph, env, template, inverse); + var helper = lookupHelper(env, context, path); helper.call(context, params, hash, options, env); } @@ -87,14 +107,8 @@ export function set(env, context, name, value) { export function component(env, morph, context, tagName, attrs, template) { var helper = lookupHelper(env, context, tagName); - template = wrap(template); - if (helper) { - var options = { - renderNode: morph, - template: template - }; - + var options = optionsFor(morph, env, template, null); helper.call(context, [], attrs, options, env); } else { componentFallback(env, morph, context, tagName, attrs, template); @@ -114,7 +128,8 @@ function componentFallback(env, morph, context, tagName, attrs, template) { for (var name in attrs) { element.setAttribute(name, attrs[name]); } - element.appendChild(template.render(context, env, { contextualElement: morph.contextualElement }).fragment); + var fragment = render(template, context, env, { contextualElement: morph.contextualElement }).fragment; + element.appendChild(fragment); morph.setNode(element); } From ff0dfeab335cbf1cc6b0b4fa475c76122b14b068 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Fri, 6 Feb 2015 14:05:52 -0800 Subject: [PATCH 14/27] Propagate ownerNode through the render nodes --- .../tests/html-compiler-test.js | 17 +++++++++++++++++ packages/htmlbars-runtime/lib/render.js | 18 ++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index d1d9ed43..2f7e5cb6 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -379,6 +379,23 @@ test("The compiler can handle simple helpers", function() { compilesTo('
{{testing title}}
', '
hello
', { title: 'hello' }); }); +test("Helpers propagate the owner render node", function() { + registerHelper('id', function(params, hash, options) { + return options.template.render(this); + }); + + var template = compile('
{{#id}}

{{#id}}{{#id}}{{name}}{{/id}}{{/id}}

{{/id}}
'); + var context = { name: "Tom Dale" }; + var result = template.render(context, env); + + equalTokens(result.fragment, '

Tom Dale

'); + + var root = result.root; + strictEqual(root, root.childNodes[0].ownerNode); + strictEqual(root, root.childNodes[0].childNodes[0].ownerNode); + strictEqual(root, root.childNodes[0].childNodes[0].childNodes[0].ownerNode); +}); + test("The compiler can handle sexpr helpers", function() { registerHelper('testing', function(params) { return params[0] + "!"; diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index 3e19a462..59491143 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -1,3 +1,5 @@ +import { forEach } from "../htmlbars-util/array-utils"; + export default function render(template, context, env, options, blockArguments) { var dom = env.dom; var contextualElement = options && options.contextualElement; @@ -7,11 +9,23 @@ export default function render(template, context, env, options, blockArguments) var fragment = getCachedFragment(template, env); var nodes = template.buildRenderNodes(dom, fragment, contextualElement); - var rootNode = (options && options.renderNode) || - dom.createMorph(null, fragment.firstChild, fragment.lastChild, contextualElement); + var rootNode, ownerNode; + + if (options && options.renderNode) { + rootNode = options.renderNode; + ownerNode = rootNode.ownerNode; + } else { + rootNode = dom.createMorph(null, fragment.firstChild, fragment.lastChild, contextualElement); + ownerNode = rootNode; + rootNode.ownerNode = rootNode; + } rootNode.childNodes = nodes; + forEach(nodes, function(node) { + node.ownerNode = ownerNode; + }); + template.render(context, rootNode, env, options, blockArguments); if (options && options.renderNode) { From 61a2fe93e152a0d994c890b6b156967ba975efcb Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Fri, 6 Feb 2015 14:06:04 -0800 Subject: [PATCH 15/27] =?UTF-8?q?Maintain=20consistent=20=E2=80=9Cshape?= =?UTF-8?q?=E2=80=9D=20for=20options=20hash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/htmlbars-runtime/lib/hooks.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 1c97272a..1557526e 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -28,7 +28,9 @@ function optionsFor(morph, env, template, inverse) { var options = { renderNode: morph, contextualElement: morph.contextualElement, - env: env + env: env, + template: null, + inverse: null }; options.template = wrapForHelper(template, options, env); From 7098b6bdcc9743426cdb806d42fb409d8114bce1 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Fri, 6 Feb 2015 16:23:42 -0800 Subject: [PATCH 16/27] `get` helper should get render node for dirtying In the future, frameworks like Ember that implement the `get` hook need some way to mark a render node as dirty. To that effect, we need to provide the `get` helper with a reference to its associated render node, such that if the underlying value changes, it can mark the node as dirty. --- .../lib/hydration-javascript-compiler.js | 6 +- .../lib/hydration-opcode-compiler.js | 32 ++++--- .../tests/hydration-opcode-compiler-test.js | 87 +++++++++---------- packages/htmlbars-runtime/lib/hooks.js | 8 +- packages/htmlbars-runtime/lib/render.js | 2 + 5 files changed, 69 insertions(+), 66 deletions(-) diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index 65e6faa5..96404eab 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -134,17 +134,19 @@ prototype.pushHook = function(name, args) { this.stack.push(name + '(' + args.join(', ') + ')'); }; -prototype.pushGetHook = function(path) { +prototype.pushGetHook = function(path, morphNum) { this.pushHook('get', [ 'env', + 'morphs[' + morphNum + ']', 'context', string(path) ]); }; -prototype.pushSexprHook = function() { +prototype.pushSexprHook = function(morphNum) { this.pushHook('subexpr', [ 'env', + 'morphs[' + morphNum + ']', 'context', this.stack.pop(), // path this.stack.pop(), // params diff --git a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js index 63937c3c..0d9d507e 100644 --- a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js @@ -119,21 +119,24 @@ HydrationOpcodeCompiler.prototype.closeElement = function() { HydrationOpcodeCompiler.prototype.mustache = function(mustache, childIndex, childCount) { this.pushMorphPlaceholderNode(childIndex, childCount); - - var sexpr = mustache.sexpr; - var morphNum = this.morphNum++; - var start = this.currentDOMChildIndex; - var end = this.currentDOMChildIndex; - this.morphs.push([morphNum, this.paths.slice(), start, end, mustache.escaped]); + var sexpr = mustache.sexpr; + var opcode; if (isHelper(sexpr)) { prepareSexpr(this, sexpr); - this.opcode('printInlineHook', morphNum); + opcode = 'printInlineHook'; } else { preparePath(this, sexpr.path); - this.opcode('printContentHook', morphNum); + opcode = 'printContentHook'; } + + var morphNum = this.morphNum++; + var start = this.currentDOMChildIndex; + var end = this.currentDOMChildIndex; + this.morphs.push([morphNum, this.paths.slice(), start, end, mustache.escaped]); + + this.opcode(opcode, morphNum); }; HydrationOpcodeCompiler.prototype.block = function(block, childIndex, childCount) { @@ -205,11 +208,12 @@ HydrationOpcodeCompiler.prototype.attribute = function(attr) { this.opcode('pushLiteral', attr.name); + var attrMorphNum = this.morphNum++; + if (this.element !== null) { shareElement(this); } - var attrMorphNum = this.morphNum++; this.opcode('createAttrMorph', attrMorphNum, this.elementNum, attr.name, escaped, namespace); this.opcode('printAttributeHook', attrMorphNum); }; @@ -222,6 +226,7 @@ HydrationOpcodeCompiler.prototype.elementHelper = function(sexpr) { shareElement(this); } + publishElementMorph(this); this.opcode('printElementHook', this.elementMorphNum); }; @@ -239,11 +244,11 @@ HydrationOpcodeCompiler.prototype.pushMorphPlaceholderNode = function(childIndex HydrationOpcodeCompiler.prototype.SubExpression = function(sexpr) { prepareSexpr(this, sexpr); - this.opcode('pushSexprHook'); + this.opcode('pushSexprHook', this.morphNum); }; HydrationOpcodeCompiler.prototype.PathExpression = function(path) { - this.opcode('pushGetHook', path.original); + this.opcode('pushGetHook', path.original, this.morphNum); }; HydrationOpcodeCompiler.prototype.StringLiteral = function(node) { @@ -293,9 +298,12 @@ function prepareSexpr(compiler, sexpr) { function shareElement(compiler) { compiler.opcode('shareElement', ++compiler.elementNum); + compiler.element = null; // Set element to null so we don't cache it twice +} + +function publishElementMorph(compiler) { var morphNum = compiler.elementMorphNum = compiler.morphNum++; compiler.opcode('createElementMorph', morphNum, compiler.elementNum); - compiler.element = null; // Set element to null so we don't cache it twice } function distributeMorphs(morphs, opcodes) { diff --git a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js index 8a2c0171..2eb7db29 100644 --- a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js +++ b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js @@ -15,13 +15,12 @@ test("simple example", function() { deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], [ "shareElement", [ 0 ] ], - [ "createMorph", [ 1, [ 0 ], 0, 0, true ] ], - [ "createMorph", [ 2, [ 0 ], 2, 2, true ] ], - [ "createElementMorph", [ 0, 0 ] ], + [ "createMorph", [ 0, [ 0 ], 0, 0, true ] ], + [ "createMorph", [ 1, [ 0 ], 2, 2, true ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 1 ] ], + [ "printContentHook", [ 0 ] ], [ "pushLiteral", [ "baz" ] ], - [ "printContentHook", [ 2 ] ], + [ "printContentHook", [ 1 ] ], [ "popParent", [] ] ]); }); @@ -122,19 +121,18 @@ test("back to back mustaches should have a text node inserted between them", fun deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], [ "shareElement", [ 0 ] ], - [ "createMorph", [ 1, [0], 0, 0, true ] ], - [ "createMorph", [ 2, [0], 1, 1, true ] ], - [ "createMorph", [ 3, [0], 2, 2, true ] ], - [ "createMorph", [ 4, [0], 4, 4, true] ], - [ "createElementMorph", [ 0, 0 ] ], + [ "createMorph", [ 0, [0], 0, 0, true ] ], + [ "createMorph", [ 1, [0], 1, 1, true ] ], + [ "createMorph", [ 2, [0], 2, 2, true ] ], + [ "createMorph", [ 3, [0], 4, 4, true] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 1 ] ], + [ "printContentHook", [ 0 ] ], [ "pushLiteral", [ "bar" ] ], - [ "printContentHook", [ 2 ] ], + [ "printContentHook", [ 1 ] ], [ "pushLiteral", [ "baz" ] ], - [ "printContentHook", [ 3 ] ], + [ "printContentHook", [ 2 ] ], [ "pushLiteral", [ "qux" ] ], - [ "printContentHook", [ 4 ] ], + [ "printContentHook", [ 3 ] ], [ "popParent", [] ] ]); }); @@ -147,7 +145,7 @@ test("helper usage", function() { [ "prepareObject", [ 0 ] ], [ "pushLiteral", [ 3.14 ] ], [ "pushLiteral", [ true ] ], - [ "pushGetHook", [ "baz.bat" ] ], + [ "pushGetHook", [ "baz.bat", 0 ] ], [ "pushLiteral", [ "bar" ] ], [ "prepareArray", [ 4 ] ], [ "pushLiteral", [ "foo" ] ], @@ -190,15 +188,14 @@ test("attribute mustache", function() { deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], [ "pushLiteral", [ " after" ] ], - [ "pushGetHook", [ "foo" ] ], + [ "pushGetHook", [ "foo", 0 ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], [ "pushConcatHook", [ ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createElementMorph", [ 0, 0 ] ], - [ "createAttrMorph", [ 1, 0, "class", true, null ] ], - [ "printAttributeHook", [ 1 ] ], + [ "createAttrMorph", [ 0, 0, "class", true, null ] ], + [ "printAttributeHook", [ 0 ] ], [ "popParent", [] ] ]); }); @@ -207,14 +204,13 @@ test("quoted attribute mustache", function() { var opcodes = opcodesFor("
"); deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], - [ "pushGetHook", [ "foo" ] ], + [ "pushGetHook", [ "foo", 0 ] ], [ "prepareArray", [ 1 ] ], [ "pushConcatHook", [ ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createElementMorph", [ 0, 0 ] ], - [ "createAttrMorph", [ 1, 0, "class", true, null ] ], - [ "printAttributeHook", [ 1 ] ], + [ "createAttrMorph", [ 0, 0, "class", true, null ] ], + [ "printAttributeHook", [ 0 ] ], [ "popParent", [] ] ]); }); @@ -223,12 +219,11 @@ test("safe bare attribute mustache", function() { var opcodes = opcodesFor("
"); deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], - [ "pushGetHook", [ "foo" ] ], + [ "pushGetHook", [ "foo", 0 ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createElementMorph", [ 0, 0 ] ], - [ "createAttrMorph", [ 1, 0, "class", true, null ] ], - [ "printAttributeHook", [ 1 ] ], + [ "createAttrMorph", [ 0, 0, "class", true, null ] ], + [ "printAttributeHook", [ 0 ] ], [ "popParent", [] ] ]); }); @@ -237,12 +232,11 @@ test("unsafe bare attribute mustache", function() { var opcodes = opcodesFor("
"); deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], - [ "pushGetHook", [ "foo" ] ], + [ "pushGetHook", [ "foo", 0 ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createElementMorph", [ 0, 0 ] ], - [ "createAttrMorph", [ 1, 0, "class", false, null ] ], - [ "printAttributeHook", [ 1 ] ], + [ "createAttrMorph", [ 0, 0, "class", false, null ] ], + [ "printAttributeHook", [ 0 ] ], [ "popParent", [] ] ]); }); @@ -256,15 +250,14 @@ test("attribute helper", function() { [ "pushLiteral", [ "bar" ] ], [ "prepareArray", [ 1 ] ], [ "pushLiteral", [ "foo" ] ], - [ "pushSexprHook", [ ] ], + [ "pushSexprHook", [ 0 ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], [ "pushConcatHook", [ ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], - [ "createElementMorph", [ 0, 0 ] ], - [ "createAttrMorph", [ 1, 0, "class", true, null ] ], - [ "printAttributeHook", [ 1 ] ], + [ "createAttrMorph", [ 0, 0, "class", true, null ] ], + [ "printAttributeHook", [ 0 ] ], [ "popParent", [] ] ]); }); @@ -274,36 +267,34 @@ test("attribute helpers", function() { deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], [ "shareElement", [ 0 ] ], - [ "createElementMorph", [ 0, 0 ] ], [ "pushLiteral", [ " after" ] ], [ "prepareObject", [ 0 ] ], [ "pushLiteral", [ "bar" ] ], [ "prepareArray", [ 1 ] ], [ "pushLiteral", [ "foo" ] ], - [ "pushSexprHook", [ ] ], + [ "pushSexprHook", [ 0 ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], [ "pushConcatHook", [ ] ], [ "pushLiteral", [ "class" ] ], - [ "createAttrMorph", [ 1, 0, "class", true, null ] ], - [ "printAttributeHook", [ 1 ] ], - [ "pushGetHook", [ 'bare' ] ], + [ "createAttrMorph", [ 0, 0, "class", true, null ] ], + [ "printAttributeHook", [ 0 ] ], + [ "pushGetHook", [ 'bare', 1 ] ], [ "pushLiteral", [ 'id' ] ], - [ "createAttrMorph", [ 2, 0, 'id', true, null ] ], - [ "printAttributeHook", [ 2 ] ], + [ "createAttrMorph", [ 1, 0, 'id', true, null ] ], + [ "printAttributeHook", [ 1 ] ], [ "popParent", [] ], - [ "createMorph", [ 3, [], 0, 1, true ] ], + [ "createMorph", [ 2, [], 1, 1, true ] ], [ "pushLiteral", [ 'morphThing' ] ], - [ "printContentHook", [ 3 ] ], + [ "printContentHook", [ 2 ] ], [ "consumeParent", [ 2 ] ], - [ "pushGetHook", [ 'ohMy' ] ], + [ "pushGetHook", [ 'ohMy', 3 ] ], [ "prepareArray", [ 1 ] ], [ "pushConcatHook", [] ], [ "pushLiteral", [ 'class' ] ], [ "shareElement", [ 1 ] ], - [ "createElementMorph", [ 4, 1 ] ], - [ "createAttrMorph", [ 5, 1, 'class', true, null ] ], - [ "printAttributeHook", [ 5 ] ], + [ "createAttrMorph", [ 3, 1, 'class', true, null ] ], + [ "printAttributeHook", [ 3 ] ], [ "popParent", [] ] ]); }); diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 1557526e..594d6ed5 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -60,7 +60,7 @@ export function content(env, morph, context, path) { if (helper) { value = helper.call(context, [], {}, { renderNode: morph }, env); } else { - value = env.hooks.get(env, context, path); + value = env.hooks.get(env, morph, context, path); } morph.setContent(value); @@ -77,16 +77,16 @@ export function attribute(env, attrMorph, name, value) { attrMorph.setContent(value); } -export function subexpr(env, context, helperName, params, hash) { +export function subexpr(env, morph, context, helperName, params, hash) { var helper = lookupHelper(env, context, helperName); if (helper) { return helper.call(context, params, hash, {}, env); } else { - return env.hooks.get(env, context, helperName); + return env.hooks.get(env, morph, context, helperName); } } -export function get(env, context, path) { +export function get(env, morph, context, path) { if (path === '') { return context; } diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index 59491143..6803a37a 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -20,6 +20,8 @@ export default function render(template, context, env, options, blockArguments) rootNode.ownerNode = rootNode; } + // TODO Invoke disposal hook recursively on old rootNode.childNodes + rootNode.childNodes = nodes; forEach(nodes, function(node) { From 13b8337fb5423cd8e1902a3d1b92f51d32f9504e Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Fri, 6 Feb 2015 18:51:56 -0800 Subject: [PATCH 17/27] Introduce dirtiness to render node invalidation Previously, we would always revalidate all render nodes in a tree when calling `revalidate()` on the root render node. As a performance optimization, we introduce the notion of dirtiness to render nodes. We have updated all of the built-in hooks that modify render nodes to first check to see if the node is dirty. If the node is not dirty, we quickly bail out. When the render node related to a block helper (or component) is not dirty, it simply asks its child nodes to continue dirty checking. Just because a block helper or component is dirty does not necessarily mean that it needs to be replaced. For example, an `#if` helper whose content changes may not flip from the truthy template to the falsy template. For this reason, helpers can choose to return the result of rendering a new template, or communicate that the existing template is still stable. If a template is stable, HTMLBars treats it as if the node was not dirty to begin with, and continues dirty checking its children. To support this effort, all of the built-in hooks (content, block, subexpr, etc) now take a render node. These hooks can quickly check the render node for dirtiness and abandon any expensive work if it is false. The intent of this API is for consumers like Ember.js to narrowly target nodes for dirtying when using observers or streams, or dirty entire subtrees when triggered manually by users. Whenever a node becomes dirty, it would schedule a revalidation of the root render node. This guarantees that each node is checked only once, no matter how many changes occur in the tree during a single run loop. It also would guarantee that parent nodes have a chance to re-render themselves before any changes affecting their old children are processed. --- packages/dom-helper/lib/main.js | 1 + .../lib/hydration-javascript-compiler.js | 10 +- .../lib/hydration-opcode-compiler.js | 16 +- .../htmlbars-compiler/tests/dirtying-test.js | 199 ++++++++++++++++++ .../tests/html-compiler-test.js | 90 +------- .../tests/hydration-opcode-compiler-test.js | 10 +- packages/htmlbars-runtime/lib/hooks.js | 106 +++++++--- packages/htmlbars-runtime/lib/render.js | 2 +- packages/htmlbars-test-helpers/lib/main.js | 56 ++++- packages/morph-attr/lib/main.js | 1 + 10 files changed, 357 insertions(+), 134 deletions(-) create mode 100644 packages/htmlbars-compiler/tests/dirtying-test.js diff --git a/packages/dom-helper/lib/main.js b/packages/dom-helper/lib/main.js index add1f370..d1afb226 100644 --- a/packages/dom-helper/lib/main.js +++ b/packages/dom-helper/lib/main.js @@ -111,6 +111,7 @@ function ElementMorph(element, dom, namespace) { this.namespace = namespace; this.state = {}; + this.isDirty = true; } /* diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index 96404eab..3e20e5c0 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -75,12 +75,13 @@ prototype.compile = function(opcodes, options) { } if (result.hasMorphs) { + indent = indent.substr(2); result.createMorphsProgram = - ' function buildRenderNodes(dom, fragment, contextualElement) {\n' + + indent + 'function buildRenderNodes(dom, fragment, contextualElement) {\n' + result.fragmentProcessingProgram + morphs + - ' return morphs;\n' + - ' }\n'; + indent + ' return morphs;\n' + + indent+'}\n'; } else { result.createMorphsProgram = ' function buildRenderNodes() { return []; }\n'; @@ -154,9 +155,10 @@ prototype.pushSexprHook = function(morphNum) { ]); }; -prototype.pushConcatHook = function() { +prototype.pushConcatHook = function(morphNum) { this.pushHook('concat', [ 'env', + 'morphs[' + morphNum + ']', this.stack.pop() // parts ]); }; diff --git a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js index 0d9d507e..b7d9c522 100644 --- a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js @@ -143,6 +143,7 @@ HydrationOpcodeCompiler.prototype.block = function(block, childIndex, childCount this.pushMorphPlaceholderNode(childIndex, childCount); var sexpr = block.sexpr; + prepareSexpr(this, sexpr); var morphNum = this.morphNum++; var start = this.currentDOMChildIndex; @@ -152,7 +153,6 @@ HydrationOpcodeCompiler.prototype.block = function(block, childIndex, childCount var templateId = this.templateId++; var inverseId = block.inverse === null ? null : this.templateId++; - prepareSexpr(this, sexpr); this.opcode('printBlockHook', morphNum, templateId, inverseId); }; @@ -162,11 +162,6 @@ HydrationOpcodeCompiler.prototype.component = function(component, childIndex, ch var program = component.program || {}; var blockParams = program.blockParams || []; - var morphNum = this.morphNum++; - var start = this.currentDOMChildIndex; - var end = this.currentDOMChildIndex; - this.morphs.push([morphNum, this.paths.slice(), start, end, true]); - var attrs = component.attributes; for (var i = attrs.length - 1; i >= 0; i--) { var name = attrs[i].name; @@ -179,12 +174,17 @@ HydrationOpcodeCompiler.prototype.component = function(component, childIndex, ch this.accept(unwrapMustache(value)); } else if (value.type === 'ConcatStatement') { prepareParams(this, value.parts); - this.opcode('pushConcatHook'); + this.opcode('pushConcatHook', this.morphNum); } this.opcode('pushLiteral', name); } + var morphNum = this.morphNum++; + var start = this.currentDOMChildIndex; + var end = this.currentDOMChildIndex; + this.morphs.push([morphNum, this.paths.slice(), start, end, true]); + this.opcode('prepareObject', attrs.length); this.opcode('pushLiteral', component.tag); this.opcode('printComponentHook', morphNum, this.templateId++, blockParams.length); @@ -203,7 +203,7 @@ HydrationOpcodeCompiler.prototype.attribute = function(attr) { this.accept(unwrapMustache(value)); } else if (value.type === 'ConcatStatement') { prepareParams(this, value.parts); - this.opcode('pushConcatHook'); + this.opcode('pushConcatHook', this.morphNum); } this.opcode('pushLiteral', attr.name); diff --git a/packages/htmlbars-compiler/tests/dirtying-test.js b/packages/htmlbars-compiler/tests/dirtying-test.js new file mode 100644 index 00000000..30003313 --- /dev/null +++ b/packages/htmlbars-compiler/tests/dirtying-test.js @@ -0,0 +1,199 @@ +import { compile } from "../htmlbars-compiler/compiler"; +import defaultHooks from "../htmlbars-runtime/hooks"; +import defaultHelpers from "../htmlbars-runtime/helpers"; +import { merge } from "../htmlbars-util/object-utils"; +import DOMHelper from "../dom-helper"; +import { equalTokens } from "../htmlbars-test-helpers"; + +var hooks, helpers, partials, env; + +function registerHelper(name, callback) { + helpers[name] = callback; +} + +function commonSetup() { + hooks = merge({}, defaultHooks); + helpers = merge({}, defaultHelpers); + partials = {}; + + env = { + dom: new DOMHelper(), + hooks: hooks, + helpers: helpers, + partials: partials, + useFragmentCache: true + }; +} + +QUnit.module("HTML-based compiler (dirtying)", { + beforeEach: commonSetup +}); + +test("a simple implementation of a dirtying rerender", function() { + var makeNodeDirty; + + // This represents the internals of a higher-level helper API + registerHelper('if', function(params, hash, options) { + var renderNode = options.renderNode; + + makeNodeDirty = function() { + renderNode.isDirty = true; + }; + + var state = renderNode.state; + var value = params[0]; + var normalized = !!value; + + // If the node is unstable + if (state.condition !== normalized) { + state.condition = normalized; + + if (normalized) { + return options.template.render(this); + } else { + return options.inverse.render(this); + } + } + }); + + var object = { condition: true, value: 'hello world' }; + var template = compile('
{{#if condition}}

{{value}}

{{else}}

Nothing

{{/if}}
'); + var result = template.render(object, env); + + equalTokens(result.fragment, '

hello world

'); + + makeNodeDirty(); + result.revalidate(); + + equalTokens(result.fragment, '

hello world

'); + + // Even though the #if was stable, a dirty child node is updated + object.value = 'goodbye world'; + var textRenderNode = result.root.childNodes[0].childNodes[0]; + textRenderNode.isDirty = true; + result.revalidate(); + equalTokens(result.fragment, '

goodbye world

'); + + // Should not update since render node is not marked as dirty + object.condition = false; + result.revalidate(); + equalTokens(result.fragment, '

goodbye world

'); + + makeNodeDirty(); + result.revalidate(); + equalTokens(result.fragment, '

Nothing

'); +}); + +test("clean content doesn't get blown away", function() { + var template = compile("
{{value}}
"); + var object = { value: "hello" }; + var result = template.render(object, env); + + var textNode = result.fragment.firstChild.firstChild; + equal(textNode.textContent, "hello"); + + object.value = "goodbye"; + result.revalidate(); // without setting the node to dirty + + equalTokens(result.fragment, '
hello
'); + + var textRenderNode = result.root.childNodes[0]; + + textRenderNode.setContent = function() { + ok(false, "Should not get called"); + }; + + object.value = "hello"; + textRenderNode.isDirty = true; + result.revalidate(); +}); + +test("helper calls follow the normal dirtying rules", function() { + registerHelper('capitalize', function(params) { + return params[0].toUpperCase(); + }); + + var template = compile("
{{capitalize value}}
"); + var object = { value: "hello" }; + var result = template.render(object, env); + + var textNode = result.fragment.firstChild.firstChild; + equal(textNode.textContent, "HELLO"); + + object.value = "goodbye"; + result.revalidate(); // without setting the node to dirty + + equalTokens(result.fragment, '
HELLO
'); + + var textRenderNode = result.root.childNodes[0]; + + textRenderNode.isDirty = true; + result.revalidate(); + + equalTokens(result.fragment, '
GOODBYE
'); + + textRenderNode.setContent = function() { + ok(false, "Should not get called"); + }; + + // Checks normalized value, not raw value + object.value = "GoOdByE"; + textRenderNode.isDirty = true; + result.revalidate(); +}); + +test("attribute nodes follow the normal dirtying rules", function() { + var template = compile("
hello
"); + var object = { value: "world" }; + var result = template.render(object, env); + + equalTokens(result.fragment, "
hello
"); + + object.value = "universe"; + result.revalidate(); // without setting the node to dirty + + equalTokens(result.fragment, "
hello
"); + + var attrRenderNode = result.root.childNodes[0]; + + attrRenderNode.isDirty = true; + result.revalidate(); + + equalTokens(result.fragment, "
hello
"); + + attrRenderNode.setContent = function() { + ok(false, "Should not get called"); + }; + + object.value = "universe"; + attrRenderNode.isDirty = true; + result.revalidate(); +}); + +test("attribute nodes w/ concat follow the normal dirtying rules", function() { + var template = compile("
hello
"); + var object = { value: "world" }; + var result = template.render(object, env); + + equalTokens(result.fragment, "
hello
"); + + object.value = "universe"; + result.revalidate(); // without setting the node to dirty + + equalTokens(result.fragment, "
hello
"); + + var attrRenderNode = result.root.childNodes[0]; + + attrRenderNode.isDirty = true; + result.revalidate(); + + equalTokens(result.fragment, "
hello
"); + + attrRenderNode.setContent = function() { + ok(false, "Should not get called"); + }; + + object.value = "universe"; + attrRenderNode.isDirty = true; + result.revalidate(); +}); diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index 2f7e5cb6..7a00c3a1 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -1,11 +1,10 @@ import { compile } from "../htmlbars-compiler/compiler"; import { forEach } from "../htmlbars-util/array-utils"; -import { tokenize } from "../simple-html-tokenizer"; import defaultHooks from "../htmlbars-runtime/hooks"; import defaultHelpers from "../htmlbars-runtime/helpers"; import { merge } from "../htmlbars-util/object-utils"; import DOMHelper from "../dom-helper"; -import { createObject, normalizeInnerHTML, getTextContent } from "../htmlbars-test-helpers"; +import { createObject, normalizeInnerHTML, getTextContent, equalTokens } from "../htmlbars-test-helpers"; var xhtmlNamespace = "http://www.w3.org/1999/xhtml", svgNamespace = "http://www.w3.org/2000/svg"; @@ -26,13 +25,6 @@ var innerHTMLHandlesNewlines = (function() { return div.innerHTML.length === 8; })(); -// IE8 removes comments and does other unspeakable things with innerHTML -var ie8GenerateTokensNeeded = (function() { - var div = document.createElement("div"); - div.innerHTML = ""; - return div.innerHTML === ""; -})(); - function registerHelper(name, callback) { helpers[name] = callback; } @@ -48,49 +40,6 @@ function compilesTo(html, expected, context) { return fragment; } -function generateTokens(fragmentOrHtml) { - var div = document.createElement("div"); - if (typeof fragmentOrHtml === 'string') { - div.innerHTML = fragmentOrHtml; - } else { - div.appendChild(fragmentOrHtml.cloneNode(true)); - } - if (ie8GenerateTokensNeeded) { - // IE8 drops comments and does other unspeakable things on `innerHTML`. - // So in that case we do it to both the expected and actual so that they match. - var div2 = document.createElement("div"); - div2.innerHTML = div.innerHTML; - div.innerHTML = div2.innerHTML; - } - return { tokens: tokenize(div.innerHTML), html: div.innerHTML }; -} - -function equalTokens(fragment, html) { - if (fragment.fragment) { fragment = fragment.fragment; } - if (html.fragment) { html = html.fragment; } - - var fragTokens = generateTokens(fragment); - var htmlTokens = generateTokens(html); - - function normalizeTokens(token) { - if (token.type === 'StartTag') { - token.attributes = token.attributes.sort(function(a,b){ - if (a.name > b.name) { - return 1; - } - if (a.name < b.name) { - return -1; - } - return 0; - }); - } - } - - forEach(fragTokens.tokens, normalizeTokens); - forEach(htmlTokens.tokens, normalizeTokens); - - deepEqual(fragTokens.tokens, htmlTokens.tokens, "Expected: " + html + "; Actual: " + fragTokens.html); -} function commonSetup() { hooks = merge({}, defaultHooks); @@ -500,43 +449,6 @@ test("Simple data binding on fragments - re-rendering", function() { equalTokens(fragment, '

brown cow

to the world
'); }); -test("Templates with block helpers - re-rendering", function() { - // This represents the internals of a higher-level helper API - registerHelper('if', function(params, hash, options) { - var renderNode = options.renderNode; - var state = renderNode.state; - var value = params[0]; - var normalized = !!value; - - if (state.condition !== normalized) { - state.condition = normalized; - - if (normalized) { - state.lastResult = options.template.render(this); - } else { - state.lastResult = options.inverse.render(this); - } - } else { - state.lastResult.revalidate(this); - } - }); - - var object = { condition: true, value: 'hello world' }; - var template = compile('
{{#if condition}}

{{value}}

{{else}}

Nothing

{{/if}}
'); - var result = template.render(object, env); - - equalTokens(result.fragment, '

hello world

'); - - object.value = 'goodbye world'; - result.revalidate(object); - equalTokens(result.fragment, '

goodbye world

'); - - object.condition = false; - result.revalidate(object); - - equalTokens(result.fragment, '

Nothing

'); -}); - test("second render respects whitespace", function () { var template = compile('Hello {{ foo }} '); template.render({}, env, { contextualElement: document.createElement('div') }); diff --git a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js index 2eb7db29..69b64ccb 100644 --- a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js +++ b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js @@ -191,7 +191,7 @@ test("attribute mustache", function() { [ "pushGetHook", [ "foo", 0 ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], - [ "pushConcatHook", [ ] ], + [ "pushConcatHook", [ 0 ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], @@ -206,7 +206,7 @@ test("quoted attribute mustache", function() { [ "consumeParent", [ 0 ] ], [ "pushGetHook", [ "foo", 0 ] ], [ "prepareArray", [ 1 ] ], - [ "pushConcatHook", [ ] ], + [ "pushConcatHook", [ 0 ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], @@ -253,7 +253,7 @@ test("attribute helper", function() { [ "pushSexprHook", [ 0 ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], - [ "pushConcatHook", [ ] ], + [ "pushConcatHook", [ 0 ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], @@ -275,7 +275,7 @@ test("attribute helpers", function() { [ "pushSexprHook", [ 0 ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], - [ "pushConcatHook", [ ] ], + [ "pushConcatHook", [ 0 ] ], [ "pushLiteral", [ "class" ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], [ "printAttributeHook", [ 0 ] ], @@ -290,7 +290,7 @@ test("attribute helpers", function() { [ "consumeParent", [ 2 ] ], [ "pushGetHook", [ 'ohMy', 3 ] ], [ "prepareArray", [ 1 ] ], - [ "pushConcatHook", [] ], + [ "pushConcatHook", [ 3 ] ], [ "pushLiteral", [ 'class' ] ], [ "shareElement", [ 1 ] ], [ "createAttrMorph", [ 3, 1, 'class', true, null ] ], diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 594d6ed5..2abd9ba2 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -40,44 +40,90 @@ function optionsFor(morph, env, template, inverse) { } export function block(env, morph, context, path, params, hash, template, inverse) { - var options = optionsFor(morph, env, template, inverse); + var state = morph.state; - var helper = lookupHelper(env, context, path); - helper.call(context, params, hash, options, env); + if (morph.isDirty) { + var options = optionsFor(morph, env, template, inverse); + + var helper = lookupHelper(env, context, path); + var result = helper.call(context, params, hash, options, env); + + if (result === undefined && state.lastResult) { + state.lastResult.revalidate(this); + } else if (result !== undefined) { + state.lastResult = result; + } + } else { + state.lastResult.revalidate(this); + } + + morph.isDirty = false; } export function inline(env, morph, context, path, params, hash) { - var helper = lookupHelper(env, context, path); - var value = helper.call(context, params, hash, { renderNode: morph }, env); + if (morph.isDirty) { + var state = morph.state; + var helper = lookupHelper(env, context, path); + + var value = helper.call(context, params, hash, { renderNode: morph }, env); + + if (state.lastValue !== value) { + morph.setContent(value); + } - morph.setContent(value); + state.lastValue = value; + morph.isDirty = false; + } } export function content(env, morph, context, path) { - var helper = lookupHelper(env, context, path); + if (morph.isDirty) { + var state = morph.state; + var helper = lookupHelper(env, context, path); - var value; - if (helper) { - value = helper.call(context, [], {}, { renderNode: morph }, env); - } else { - value = env.hooks.get(env, morph, context, path); - } + var value; + if (helper) { + value = helper.call(context, [], {}, { renderNode: morph }, env); + } else { + value = env.hooks.get(env, morph, context, path); + } + + if (state.lastValue !== value) { + morph.setContent(value); + } - morph.setContent(value); + state.lastValue = value; + morph.isDirty = false; + } } export function element(env, morph, context, path, params, hash) { - var helper = lookupHelper(env, context, path); - if (helper) { - helper.call(context, params, hash, { element: morph.element }, env); + if (morph.isDirty) { + var helper = lookupHelper(env, context, path); + if (helper) { + helper.call(context, params, hash, { element: morph.element }, env); + } + + morph.isDirty = false; } } -export function attribute(env, attrMorph, name, value) { - attrMorph.setContent(value); +export function attribute(env, morph, name, value) { + if (morph.isDirty) { + var state = morph.state; + + if (state.lastValue !== value) { + morph.setContent(value); + } + + state.lastValue = value; + morph.isDirty = false; + } } export function subexpr(env, morph, context, helperName, params, hash) { + if (!morph.isDirty) { return; } + var helper = lookupHelper(env, context, helperName); if (helper) { return helper.call(context, params, hash, {}, env); @@ -87,6 +133,8 @@ export function subexpr(env, morph, context, helperName, params, hash) { } export function get(env, morph, context, path) { + if (!morph.isDirty) { return; } + if (path === '') { return context; } @@ -108,16 +156,22 @@ export function set(env, context, name, value) { } export function component(env, morph, context, tagName, attrs, template) { - var helper = lookupHelper(env, context, tagName); - if (helper) { - var options = optionsFor(morph, env, template, null); - helper.call(context, [], attrs, options, env); - } else { - componentFallback(env, morph, context, tagName, attrs, template); + if (morph.isDirty) { + var helper = lookupHelper(env, context, tagName); + if (helper) { + var options = optionsFor(morph, env, template, null); + helper.call(context, [], attrs, options, env); + } else { + componentFallback(env, morph, context, tagName, attrs, template); + } + + morph.isDirty = false; } } -export function concat(env, params) { +export function concat(env, morph, params) { + if (!morph.isDirty) { return; } + var value = ""; for (var i = 0, l = params.length; i < l; i++) { value += params[i]; diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index 6803a37a..dfbcdf9c 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -38,7 +38,7 @@ export default function render(template, context, env, options, blockArguments) root: rootNode, fragment: fragment, revalidate: function(newContext, newEnv, newOptions) { - template.render(newContext, rootNode, newEnv || env, newOptions || options); + template.render(newContext || context, rootNode, newEnv || env, newOptions || options); } }; } diff --git a/packages/htmlbars-test-helpers/lib/main.js b/packages/htmlbars-test-helpers/lib/main.js index 7ee301e0..129caa88 100644 --- a/packages/htmlbars-test-helpers/lib/main.js +++ b/packages/htmlbars-test-helpers/lib/main.js @@ -1,3 +1,6 @@ +import { tokenize } from "../simple-html-tokenizer"; +import { forEach } from "../htmlbars-util/array-utils"; + export function equalInnerHTML(fragment, html) { var actualHTML = normalizeInnerHTML(fragment.innerHTML); QUnit.push(actualHTML === html, actualHTML, html); @@ -20,6 +23,57 @@ export function equalHTML(node, html) { equalInnerHTML(div, html); } +// IE8 removes comments and does other unspeakable things with innerHTML +var ie8GenerateTokensNeeded = (function() { + var div = document.createElement("div"); + div.innerHTML = ""; + return div.innerHTML === ""; +})(); + +function generateTokens(fragmentOrHtml) { + var div = document.createElement("div"); + if (typeof fragmentOrHtml === 'string') { + div.innerHTML = fragmentOrHtml; + } else { + div.appendChild(fragmentOrHtml.cloneNode(true)); + } + if (ie8GenerateTokensNeeded) { + // IE8 drops comments and does other unspeakable things on `innerHTML`. + // So in that case we do it to both the expected and actual so that they match. + var div2 = document.createElement("div"); + div2.innerHTML = div.innerHTML; + div.innerHTML = div2.innerHTML; + } + return { tokens: tokenize(div.innerHTML), html: div.innerHTML }; +} + +export function equalTokens(fragment, html) { + if (fragment.fragment) { fragment = fragment.fragment; } + if (html.fragment) { html = html.fragment; } + + var fragTokens = generateTokens(fragment); + var htmlTokens = generateTokens(html); + + function normalizeTokens(token) { + if (token.type === 'StartTag') { + token.attributes = token.attributes.sort(function(a,b){ + if (a.name > b.name) { + return 1; + } + if (a.name < b.name) { + return -1; + } + return 0; + }); + } + } + + forEach(fragTokens.tokens, normalizeTokens); + forEach(htmlTokens.tokens, normalizeTokens); + + deepEqual(fragTokens.tokens, htmlTokens.tokens, "Expected: " + html + "; Actual: " + fragTokens.html); +} + // detect weird IE8 html strings var ie8InnerHTMLTestElement = document.createElement('div'); ie8InnerHTMLTestElement.setAttribute('id', 'womp'); @@ -103,4 +157,4 @@ export function createObject(obj) { Temp.prototype = obj; return new Temp(); } -} \ No newline at end of file +} diff --git a/packages/morph-attr/lib/main.js b/packages/morph-attr/lib/main.js index bd8e92a7..9cc5a6b6 100644 --- a/packages/morph-attr/lib/main.js +++ b/packages/morph-attr/lib/main.js @@ -28,6 +28,7 @@ function AttrMorph(element, attrName, domHelper, namespace) { this.domHelper = domHelper; this.namespace = namespace !== undefined ? namespace : getAttrNamespace(attrName); this.state = {}; + this.isDirty = true; this.escaped = true; var normalizedAttrName = normalizeProperty(this.element, attrName); From 1ce5d4aed22d901cc6008d986bc2c5ea078f5c62 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Sun, 8 Feb 2015 04:38:46 -0800 Subject: [PATCH 18/27] Defer more of rendering to the runtime This commit makes all tests pass with the new visitor structure, significantly shrinking the size of compiled templates and making it possible to do re-renders without having to execute a render function. This also means that, in theory, hooks like `inline` can know the structure of what they're invoking before actually invoking it, allowing implementation to do smarter caching at the "statement" level. This change is honestly mostly a wonky improvement, but it does help us avoid making as many decisions in the compiled template, deferring them to the runtime. There is still a decent amount of cruft left as of this commit, which we will remove momentarily. --- demos/compile-and-run.html | 16 +++- .../lib/fragment-javascript-compiler.js | 2 +- .../lib/hydration-javascript-compiler.js | 85 +++++++++++++++-- .../lib/template-compiler.js | 27 +++--- .../lib/expression-visitor.js | 93 +++++++++++++++++++ packages/htmlbars-runtime/lib/hooks.js | 4 +- packages/htmlbars-runtime/lib/render.js | 26 +++++- 7 files changed, 221 insertions(+), 32 deletions(-) create mode 100644 packages/htmlbars-runtime/lib/expression-visitor.js diff --git a/demos/compile-and-run.html b/demos/compile-and-run.html index abf1642f..dbbd08c3 100644 --- a/demos/compile-and-run.html +++ b/demos/compile-and-run.html @@ -33,10 +33,12 @@ var compiler = requireModule('htmlbars-compiler'), DOMHelper = requireModule('dom-helper').default, hooks = requireModule('htmlbars-runtime').hooks, - helpers = requireModule('htmlbars-runtime').helpers; + helpers = requireModule('htmlbars-runtime').helpers, + render = requireModule('htmlbars-runtime').render; var templateSource = localStorage.getItem('templateSource'); var data = localStorage.getItem('templateData'); + var shouldRender = localStorage.getItem('shouldRender'); if (templateSource) { textarea.value = templateSource; @@ -46,13 +48,19 @@ dataarea.value = data; } + if (shouldRender === "false") { + skipRender.checked = true; + } + button.addEventListener('click', function() { var source = textarea.value, data = dataarea.value, + shouldRender = !skipRender.checked, compileOptions; localStorage.setItem('templateSource', source); localStorage.setItem('templateData', data); + localStorage.setItem('shouldRender', shouldRender); try { data = JSON.parse(data); @@ -70,10 +78,10 @@ var templateSpec = compiler.compileSpec(source, compileOptions); output.innerHTML = '
' + templateSpec + '
'; - if (!skipRender.checked) { + if (shouldRender) { var env = { dom: new DOMHelper(), hooks: hooks, helpers: helpers }; - var template = compiler.compile(source, compileOptions); - var dom = template.render(data, env, { contextualElement: output }).fragment; + var template = compiler.template(templateSpec); + var dom = render(template, data, env, { contextualElement: output }).fragment; output.innerHTML += '
' + JSON.stringify(data) + '

'; output.appendChild(dom); diff --git a/packages/htmlbars-compiler/lib/fragment-javascript-compiler.js b/packages/htmlbars-compiler/lib/fragment-javascript-compiler.js index 195970e4..e51df292 100644 --- a/packages/htmlbars-compiler/lib/fragment-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/fragment-javascript-compiler.js @@ -20,7 +20,7 @@ FragmentJavaScriptCompiler.prototype.compile = function(opcodes, options) { this.namespaceFrameStack = [{namespace: null, depth: null}]; this.domNamespace = null; - this.source.push('function build(dom) {\n'); + this.source.push('function buildFragment(dom) {\n'); processOpcodes(this, opcodes); this.source.push(this.indent+'}'); diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index 3e20e5c0..c9f171ca 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -29,6 +29,9 @@ prototype.compile = function(opcodes, options) { this.hooks = {}; this.hasOpenBoundary = false; this.hasCloseBoundary = false; + this.statements = []; + this.expressionStack = []; + this.augmentContext = []; processOpcodes(this, opcodes); @@ -50,6 +53,8 @@ prototype.compile = function(opcodes, options) { createMorphsProgram: '', hydrateMorphsProgram: '', fragmentProcessingProgram: '', + statements: this.statements, + augmentContext: this.augmentContext, hasMorphs: false }; @@ -58,11 +63,11 @@ prototype.compile = function(opcodes, options) { if (this.morphs.length) { result.hasMorphs = true; morphs = - indent+'var morphs = new Array(' + this.morphs.length + ');\n'; + indent+' var morphs = new Array(' + this.morphs.length + ');\n'; for (i = 0, l = this.morphs.length; i < l; ++i) { var morph = this.morphs[i]; - morphs += indent+'morphs['+i+'] = '+morph+';\n'; + morphs += indent+' morphs['+i+'] = '+morph+';\n'; } } @@ -75,16 +80,15 @@ prototype.compile = function(opcodes, options) { } if (result.hasMorphs) { - indent = indent.substr(2); result.createMorphsProgram = - indent + 'function buildRenderNodes(dom, fragment, contextualElement) {\n' + + 'function buildRenderNodes(dom, fragment, contextualElement) {\n' + result.fragmentProcessingProgram + morphs + indent + ' return morphs;\n' + - indent+'}\n'; + indent+'}'; } else { result.createMorphsProgram = - ' function buildRenderNodes() { return []; }\n'; + 'function buildRenderNodes() { return []; }'; } return result; @@ -92,25 +96,32 @@ prototype.compile = function(opcodes, options) { prototype.prepareArray = function(length) { var values = []; + var expressionValues = []; for (var i = 0; i < length; i++) { values.push(this.stack.pop()); + expressionValues.push(this.expressionStack.pop()); } + this.expressionStack.push(expressionValues); this.stack.push('[' + values.join(', ') + ']'); }; prototype.prepareObject = function(size) { var pairs = []; + var expressionPairs = []; for (var i = 0; i < size; i++) { pairs.push(this.stack.pop() + ': ' + this.stack.pop()); + expressionPairs.push(this.expressionStack.pop(), this.expressionStack.pop()); } + this.expressionStack.push(expressionPairs); this.stack.push('{' + pairs.join(', ') + '}'); }; prototype.pushRaw = function(value) { + this.expressionStack.push(value); this.stack.push(value); }; @@ -123,6 +134,8 @@ prototype.closeBoundary = function() { }; prototype.pushLiteral = function(value) { + this.expressionStack.push(value); + if (typeof value === 'string') { this.stack.push(string(value)); } else { @@ -136,6 +149,8 @@ prototype.pushHook = function(name, args) { }; prototype.pushGetHook = function(path, morphNum) { + this.expressionStack.push([ 'get', path ]); + this.pushHook('get', [ 'env', 'morphs[' + morphNum + ']', @@ -145,6 +160,13 @@ prototype.pushGetHook = function(path, morphNum) { }; prototype.pushSexprHook = function(morphNum) { + this.expressionStack.push([ + 'subexpr', + this.expressionStack.pop(), + this.expressionStack.pop(), + this.expressionStack.pop() + ]); + this.pushHook('subexpr', [ 'env', 'morphs[' + morphNum + ']', @@ -156,6 +178,8 @@ prototype.pushSexprHook = function(morphNum) { }; prototype.pushConcatHook = function(morphNum) { + this.expressionStack.push([ 'concat', this.expressionStack.pop() ]); + this.pushHook('concat', [ 'env', 'morphs[' + morphNum + ']', @@ -169,6 +193,8 @@ prototype.printHook = function(name, args) { }; prototype.printSetHook = function(name, index) { + this.augmentContext.push(name); + this.printHook('set', [ 'env', 'context', @@ -178,6 +204,15 @@ prototype.printSetHook = function(name, index) { }; prototype.printBlockHook = function(morphNum, templateId, inverseId) { + this.statements.push([ + 'block', + this.expressionStack.pop(), // path + this.expressionStack.pop(), // params + this.expressionStack.pop(), // hash + templateId, + inverseId + ]); + this.printHook('block', [ 'env', 'morphs[' + morphNum + ']', @@ -191,17 +226,29 @@ prototype.printBlockHook = function(morphNum, templateId, inverseId) { }; prototype.printInlineHook = function(morphNum) { + var path = this.stack.pop(); + var params = this.stack.pop(); + var hash = this.stack.pop(); + + var exprPath = this.expressionStack.pop(); + var exprParams = this.expressionStack.pop(); + var exprHash = this.expressionStack.pop(); + + this.statements.push([ 'inline', exprPath, exprParams, exprHash ]); + this.printHook('inline', [ 'env', 'morphs[' + morphNum + ']', 'context', - this.stack.pop(), // path - this.stack.pop(), // params - this.stack.pop() // hash + path, + params, + hash ]); }; prototype.printContentHook = function(morphNum) { + this.statements.push([ 'content', this.expressionStack.pop() ]); + this.printHook('content', [ 'env', 'morphs[' + morphNum + ']', @@ -211,6 +258,13 @@ prototype.printContentHook = function(morphNum) { }; prototype.printComponentHook = function(morphNum, templateId) { + this.statements.push([ + 'component', + this.expressionStack.pop(), // path + this.expressionStack.pop(), // attrs + templateId + ]); + this.printHook('component', [ 'env', 'morphs[' + morphNum + ']', @@ -222,6 +276,12 @@ prototype.printComponentHook = function(morphNum, templateId) { }; prototype.printAttributeHook = function(attrMorphNum) { + this.statements.push([ + 'attribute', + this.expressionStack.pop(), // name + this.expressionStack.pop() // value; + ]); + this.printHook('attribute', [ 'env', 'morphs[' + attrMorphNum + ']', @@ -231,6 +291,13 @@ prototype.printAttributeHook = function(attrMorphNum) { }; prototype.printElementHook = function(morphNum) { + this.statements.push([ + 'element', + this.expressionStack.pop(), // path + this.expressionStack.pop(), // params + this.expressionStack.pop() // hash + ]); + this.printHook('element', [ 'env', 'morphs[' + morphNum + ']', diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index 7653d695..e581a322 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -88,6 +88,16 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { templateSignature += ', blockArguments'; } + var statements = hydrationPrograms.statements.map(function(s) { + return indent+' '+JSON.stringify(s); + }).join(",\n"); + + var augmentContext = JSON.stringify(hydrationPrograms.augmentContext); + + var templates = this.childTemplates.map(function(_, index) { + return 'child' + index; + }).join(', '); + var template = '(function() {\n' + this.getChildTemplateVars(indent + ' ') + @@ -96,18 +106,13 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' blockParams: ' + blockParams.length + ',\n' + indent+' cachedFragment: null,\n' + indent+' hasRendered: false,\n' + - indent+' build: ' + fragmentProgram + ',\n' + - indent+' buildRenderNodes: buildRenderNodes,\n' + - indent+' render: function render(' + templateSignature + ') {\n' + - indent+' var dom = env.dom;\n' + - this.getHydrationHooks(indent + ' ', this.hydrationCompiler.hooks) + - indent+' var contextualElement = options && options.contextualElement;\n' + - indent+' dom.detectNamespace(contextualElement);\n' + - indent+' var morphs = rootNode.childNodes;\n' + - hydrationPrograms.hydrateMorphsProgram + - indent+' }\n' + + indent+' buildFragment: ' + fragmentProgram + ',\n' + + indent+' buildRenderNodes: ' + hydrationPrograms.createMorphsProgram + ',\n' + + indent+' statements: [\n' + statements + '\n' + + indent+' ],\n' + + indent+' augmentContext: ' + augmentContext + ',\n' + + indent+' templates: [' + templates + ']\n' + indent+' };\n' + - hydrationPrograms.createMorphsProgram + indent+'}())'; this.templates.push(template); diff --git a/packages/htmlbars-runtime/lib/expression-visitor.js b/packages/htmlbars-runtime/lib/expression-visitor.js new file mode 100644 index 00000000..395e107c --- /dev/null +++ b/packages/htmlbars-runtime/lib/expression-visitor.js @@ -0,0 +1,93 @@ +export default { + accept: function(node, morph, context, env, template) { + // Primitive literals are unambiguously non-array representations of + // themselves. + if (typeof node !== 'object') { + return node; + } + + var type = node[0]; + return this[type](node, morph, context, env, template); + }, + + acceptArray: function(nodes, morph, context, env, template) { + return nodes.map(function(node) { + return this.accept(node, morph, context, env, template); + }, this); + }, + + acceptObject: function(pairs, morph, context, env, template) { + var object = {}; + + for (var i=0, l=pairs.length; i Date: Sun, 8 Feb 2015 06:42:53 -0800 Subject: [PATCH 19/27] Remove unnecessary inlined render function Now that the template is largely just a data structure that describes its statements and expressions, the `render` function is no longer necessary. Instead, the runtime `render` function uses the data structure to decide what to do. At the moment, it simply delegates to the hooks that were already there, but the plan is to allow runtimes to completely swap out the expression visitor to do more exotic things like building and caching streams. --- .../lib/hydration-javascript-compiler.js | 150 +++--------------- .../lib/hydration-opcode-compiler.js | 17 +- .../tests/hydration-opcode-compiler-test.js | 72 ++++----- 3 files changed, 63 insertions(+), 176 deletions(-) diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index c9f171ca..85ea7fee 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -1,5 +1,5 @@ import { processOpcodes } from "./utils"; -import { string, array } from "../htmlbars-util/quoting"; +import { array } from "../htmlbars-util/quoting"; function HydrationJavaScriptCompiler() { this.stack = []; @@ -96,33 +96,22 @@ prototype.compile = function(opcodes, options) { prototype.prepareArray = function(length) { var values = []; - var expressionValues = []; for (var i = 0; i < length; i++) { - values.push(this.stack.pop()); - expressionValues.push(this.expressionStack.pop()); + values.push(this.expressionStack.pop()); } - this.expressionStack.push(expressionValues); - this.stack.push('[' + values.join(', ') + ']'); + this.expressionStack.push(values); }; prototype.prepareObject = function(size) { var pairs = []; - var expressionPairs = []; for (var i = 0; i < size; i++) { - pairs.push(this.stack.pop() + ': ' + this.stack.pop()); - expressionPairs.push(this.expressionStack.pop(), this.expressionStack.pop()); + pairs.push(this.expressionStack.pop(), this.expressionStack.pop()); } - this.expressionStack.push(expressionPairs); - this.stack.push('{' + pairs.join(', ') + '}'); -}; - -prototype.pushRaw = function(value) { - this.expressionStack.push(value); - this.stack.push(value); + this.expressionStack.push(pairs); }; prototype.openBoundary = function() { @@ -135,75 +124,30 @@ prototype.closeBoundary = function() { prototype.pushLiteral = function(value) { this.expressionStack.push(value); - - if (typeof value === 'string') { - this.stack.push(string(value)); - } else { - this.stack.push(value.toString()); - } -}; - -prototype.pushHook = function(name, args) { - this.hooks[name] = true; - this.stack.push(name + '(' + args.join(', ') + ')'); }; -prototype.pushGetHook = function(path, morphNum) { +prototype.pushGetHook = function(path) { this.expressionStack.push([ 'get', path ]); - - this.pushHook('get', [ - 'env', - 'morphs[' + morphNum + ']', - 'context', - string(path) - ]); }; -prototype.pushSexprHook = function(morphNum) { +prototype.pushSexprHook = function() { this.expressionStack.push([ 'subexpr', this.expressionStack.pop(), this.expressionStack.pop(), this.expressionStack.pop() ]); - - this.pushHook('subexpr', [ - 'env', - 'morphs[' + morphNum + ']', - 'context', - this.stack.pop(), // path - this.stack.pop(), // params - this.stack.pop() // hash - ]); }; -prototype.pushConcatHook = function(morphNum) { +prototype.pushConcatHook = function() { this.expressionStack.push([ 'concat', this.expressionStack.pop() ]); - - this.pushHook('concat', [ - 'env', - 'morphs[' + morphNum + ']', - this.stack.pop() // parts - ]); -}; - -prototype.printHook = function(name, args) { - this.hooks[name] = true; - this.source.push(this.indent + ' ' + name + '(' + args.join(', ') + ');\n'); }; -prototype.printSetHook = function(name, index) { +prototype.printSetHook = function(name) { this.augmentContext.push(name); - - this.printHook('set', [ - 'env', - 'context', - string(name), - 'blockArguments[' + index + ']' - ]); }; -prototype.printBlockHook = function(morphNum, templateId, inverseId) { +prototype.printBlockHook = function(templateId, inverseId) { this.statements.push([ 'block', this.expressionStack.pop(), // path @@ -212,100 +156,44 @@ prototype.printBlockHook = function(morphNum, templateId, inverseId) { templateId, inverseId ]); - - this.printHook('block', [ - 'env', - 'morphs[' + morphNum + ']', - 'context', - this.stack.pop(), // path - this.stack.pop(), // params - this.stack.pop(), // hash - templateId === null ? 'null' : 'child' + templateId, - inverseId === null ? 'null' : 'child' + inverseId - ]); }; -prototype.printInlineHook = function(morphNum) { - var path = this.stack.pop(); - var params = this.stack.pop(); - var hash = this.stack.pop(); +prototype.printInlineHook = function() { + var path = this.expressionStack.pop(); + var params = this.expressionStack.pop(); + var hash = this.expressionStack.pop(); - var exprPath = this.expressionStack.pop(); - var exprParams = this.expressionStack.pop(); - var exprHash = this.expressionStack.pop(); - - this.statements.push([ 'inline', exprPath, exprParams, exprHash ]); - - this.printHook('inline', [ - 'env', - 'morphs[' + morphNum + ']', - 'context', - path, - params, - hash - ]); + this.statements.push([ 'inline', path, params, hash ]); }; -prototype.printContentHook = function(morphNum) { +prototype.printContentHook = function() { this.statements.push([ 'content', this.expressionStack.pop() ]); - - this.printHook('content', [ - 'env', - 'morphs[' + morphNum + ']', - 'context', - this.stack.pop() // path - ]); }; -prototype.printComponentHook = function(morphNum, templateId) { +prototype.printComponentHook = function(templateId) { this.statements.push([ 'component', this.expressionStack.pop(), // path this.expressionStack.pop(), // attrs templateId ]); - - this.printHook('component', [ - 'env', - 'morphs[' + morphNum + ']', - 'context', - this.stack.pop(), // path - this.stack.pop(), // attrs - templateId === null ? 'null' : 'child' + templateId - ]); }; -prototype.printAttributeHook = function(attrMorphNum) { +prototype.printAttributeHook = function() { this.statements.push([ 'attribute', this.expressionStack.pop(), // name this.expressionStack.pop() // value; ]); - - this.printHook('attribute', [ - 'env', - 'morphs[' + attrMorphNum + ']', - this.stack.pop(), // name - this.stack.pop() // value - ]); }; -prototype.printElementHook = function(morphNum) { +prototype.printElementHook = function() { this.statements.push([ 'element', this.expressionStack.pop(), // path this.expressionStack.pop(), // params this.expressionStack.pop() // hash ]); - - this.printHook('element', [ - 'env', - 'morphs[' + morphNum + ']', - 'context', - this.stack.pop(), // path - this.stack.pop(), // params - this.stack.pop() // hash - ]); }; prototype.createMorph = function(morphNum, parentPath, startIndex, endIndex, escaped) { diff --git a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js index b7d9c522..8aea3f3f 100644 --- a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js @@ -28,7 +28,6 @@ function HydrationOpcodeCompiler() { this.currentDOMChildIndex = 0; this.morphs = []; this.morphNum = 0; - this.elementMorphNum = null; this.element = null; this.elementNum = -1; } @@ -136,7 +135,7 @@ HydrationOpcodeCompiler.prototype.mustache = function(mustache, childIndex, chil var end = this.currentDOMChildIndex; this.morphs.push([morphNum, this.paths.slice(), start, end, mustache.escaped]); - this.opcode(opcode, morphNum); + this.opcode(opcode); }; HydrationOpcodeCompiler.prototype.block = function(block, childIndex, childCount) { @@ -153,7 +152,7 @@ HydrationOpcodeCompiler.prototype.block = function(block, childIndex, childCount var templateId = this.templateId++; var inverseId = block.inverse === null ? null : this.templateId++; - this.opcode('printBlockHook', morphNum, templateId, inverseId); + this.opcode('printBlockHook', templateId, inverseId); }; HydrationOpcodeCompiler.prototype.component = function(component, childIndex, childCount) { @@ -187,7 +186,7 @@ HydrationOpcodeCompiler.prototype.component = function(component, childIndex, ch this.opcode('prepareObject', attrs.length); this.opcode('pushLiteral', component.tag); - this.opcode('printComponentHook', morphNum, this.templateId++, blockParams.length); + this.opcode('printComponentHook', this.templateId++, blockParams.length); }; HydrationOpcodeCompiler.prototype.attribute = function(attr) { @@ -215,7 +214,7 @@ HydrationOpcodeCompiler.prototype.attribute = function(attr) { } this.opcode('createAttrMorph', attrMorphNum, this.elementNum, attr.name, escaped, namespace); - this.opcode('printAttributeHook', attrMorphNum); + this.opcode('printAttributeHook'); }; HydrationOpcodeCompiler.prototype.elementHelper = function(sexpr) { @@ -227,7 +226,7 @@ HydrationOpcodeCompiler.prototype.elementHelper = function(sexpr) { } publishElementMorph(this); - this.opcode('printElementHook', this.elementMorphNum); + this.opcode('printElementHook'); }; HydrationOpcodeCompiler.prototype.pushMorphPlaceholderNode = function(childIndex, childCount) { @@ -244,11 +243,11 @@ HydrationOpcodeCompiler.prototype.pushMorphPlaceholderNode = function(childIndex HydrationOpcodeCompiler.prototype.SubExpression = function(sexpr) { prepareSexpr(this, sexpr); - this.opcode('pushSexprHook', this.morphNum); + this.opcode('pushSexprHook'); }; HydrationOpcodeCompiler.prototype.PathExpression = function(path) { - this.opcode('pushGetHook', path.original, this.morphNum); + this.opcode('pushGetHook', path.original); }; HydrationOpcodeCompiler.prototype.StringLiteral = function(node) { @@ -302,7 +301,7 @@ function shareElement(compiler) { } function publishElementMorph(compiler) { - var morphNum = compiler.elementMorphNum = compiler.morphNum++; + var morphNum = compiler.morphNum++; compiler.opcode('createElementMorph', morphNum, compiler.elementNum); } diff --git a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js index 69b64ccb..0087fe67 100644 --- a/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js +++ b/packages/htmlbars-compiler/tests/hydration-opcode-compiler-test.js @@ -18,9 +18,9 @@ test("simple example", function() { [ "createMorph", [ 0, [ 0 ], 0, 0, true ] ], [ "createMorph", [ 1, [ 0 ], 2, 2, true ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], + [ "printContentHook", [ ] ], [ "pushLiteral", [ "baz" ] ], - [ "printContentHook", [ 1 ] ], + [ "printContentHook", [ ] ], [ "popParent", [] ] ]); }); @@ -33,7 +33,7 @@ test("simple block", function() { [ "prepareObject", [ 0 ] ], [ "prepareArray", [ 0 ] ], [ "pushLiteral", [ "foo" ] ], - [ "printBlockHook", [ 0, 0, null ] ], + [ "printBlockHook", [ 0, null ] ], [ "popParent", [] ] ]); }); @@ -46,7 +46,7 @@ test("simple block with block params", function() { [ "prepareObject", [ 0 ] ], [ "prepareArray", [ 0 ] ], [ "pushLiteral", [ "foo" ] ], - [ "printBlockHook", [ 0, 0, null ] ], + [ "printBlockHook", [ 0, null ] ], [ "popParent", [] ] ]); }); @@ -57,7 +57,7 @@ test("element with a sole mustache child", function() { [ "consumeParent", [ 0 ] ], [ "createMorph", [ 0, [ 0 ], 0, 0, true ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], + [ "printContentHook", [ ] ], [ "popParent", [] ] ]); }); @@ -68,7 +68,7 @@ test("element with a mustache between two text nodes", function() { [ "consumeParent", [ 0 ] ], [ "createMorph", [ 0, [ 0 ], 1, 1, true ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], + [ "printContentHook", [ ] ], [ "popParent", [] ] ]); }); @@ -80,7 +80,7 @@ test("mustache two elements deep", function() { [ "consumeParent", [ 0 ] ], [ "createMorph", [ 0, [ 0, 0 ], 0, 0, true ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], + [ "printContentHook", [ ] ], [ "popParent", [] ], [ "popParent", [] ] ]); @@ -92,12 +92,12 @@ test("two sibling elements with mustaches", function() { [ "consumeParent", [ 0 ] ], [ "createMorph", [ 0, [ 0 ], 0, 0, true ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], + [ "printContentHook", [ ] ], [ "popParent", [] ], [ "consumeParent", [ 1 ] ], [ "createMorph", [ 1, [ 1 ], 0, 0, true ] ], [ "pushLiteral", [ "bar" ] ], - [ "printContentHook", [ 1 ] ], + [ "printContentHook", [ ] ], [ "popParent", [] ] ]); }); @@ -109,10 +109,10 @@ test("mustaches at the root", function() { [ "createMorph", [ 1, [ ], 2, 2, true ] ], [ "openBoundary", [ ] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], + [ "printContentHook", [ ] ], [ "closeBoundary", [ ] ], [ "pushLiteral", [ "bar" ] ], - [ "printContentHook", [ 1 ] ] + [ "printContentHook", [ ] ] ]); }); @@ -126,13 +126,13 @@ test("back to back mustaches should have a text node inserted between them", fun [ "createMorph", [ 2, [0], 2, 2, true ] ], [ "createMorph", [ 3, [0], 4, 4, true] ], [ "pushLiteral", [ "foo" ] ], - [ "printContentHook", [ 0 ] ], + [ "printContentHook", [ ] ], [ "pushLiteral", [ "bar" ] ], - [ "printContentHook", [ 1 ] ], + [ "printContentHook", [ ] ], [ "pushLiteral", [ "baz" ] ], - [ "printContentHook", [ 2 ] ], + [ "printContentHook", [ ] ], [ "pushLiteral", [ "qux" ] ], - [ "printContentHook", [ 3 ] ], + [ "printContentHook", [ ] ], [ "popParent", [] ] ]); }); @@ -145,11 +145,11 @@ test("helper usage", function() { [ "prepareObject", [ 0 ] ], [ "pushLiteral", [ 3.14 ] ], [ "pushLiteral", [ true ] ], - [ "pushGetHook", [ "baz.bat", 0 ] ], + [ "pushGetHook", [ "baz.bat" ] ], [ "pushLiteral", [ "bar" ] ], [ "prepareArray", [ 4 ] ], [ "pushLiteral", [ "foo" ] ], - [ "printInlineHook", [ 0 ] ], + [ "printInlineHook", [ ] ], [ "popParent", [] ] ]); }); @@ -163,7 +163,7 @@ test("node mustache", function() { [ "pushLiteral", [ "foo" ] ], [ "shareElement", [ 0 ] ], [ "createElementMorph", [ 0, 0 ] ], - [ "printElementHook", [ 0 ] ], + [ "printElementHook", [ ] ], [ "popParent", [] ] ]); }); @@ -178,7 +178,7 @@ test("node helper", function() { [ "pushLiteral", [ "foo" ] ], [ "shareElement", [ 0 ] ], [ "createElementMorph", [ 0, 0 ] ], - [ "printElementHook", [ 0 ] ], + [ "printElementHook", [ ] ], [ "popParent", [] ] ]); }); @@ -188,14 +188,14 @@ test("attribute mustache", function() { deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], [ "pushLiteral", [ " after" ] ], - [ "pushGetHook", [ "foo", 0 ] ], + [ "pushGetHook", [ "foo" ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], [ "pushConcatHook", [ 0 ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0 ] ], + [ "printAttributeHook", [ ] ], [ "popParent", [] ] ]); }); @@ -204,13 +204,13 @@ test("quoted attribute mustache", function() { var opcodes = opcodesFor("
"); deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], - [ "pushGetHook", [ "foo", 0 ] ], + [ "pushGetHook", [ "foo" ] ], [ "prepareArray", [ 1 ] ], [ "pushConcatHook", [ 0 ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0 ] ], + [ "printAttributeHook", [ ] ], [ "popParent", [] ] ]); }); @@ -219,11 +219,11 @@ test("safe bare attribute mustache", function() { var opcodes = opcodesFor("
"); deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], - [ "pushGetHook", [ "foo", 0 ] ], + [ "pushGetHook", [ "foo" ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0 ] ], + [ "printAttributeHook", [ ] ], [ "popParent", [] ] ]); }); @@ -232,11 +232,11 @@ test("unsafe bare attribute mustache", function() { var opcodes = opcodesFor("
"); deepEqual(opcodes, [ [ "consumeParent", [ 0 ] ], - [ "pushGetHook", [ "foo", 0 ] ], + [ "pushGetHook", [ "foo" ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], [ "createAttrMorph", [ 0, 0, "class", false, null ] ], - [ "printAttributeHook", [ 0 ] ], + [ "printAttributeHook", [ ] ], [ "popParent", [] ] ]); }); @@ -250,14 +250,14 @@ test("attribute helper", function() { [ "pushLiteral", [ "bar" ] ], [ "prepareArray", [ 1 ] ], [ "pushLiteral", [ "foo" ] ], - [ "pushSexprHook", [ 0 ] ], + [ "pushSexprHook", [ ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], [ "pushConcatHook", [ 0 ] ], [ "pushLiteral", [ "class" ] ], [ "shareElement", [ 0 ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0 ] ], + [ "printAttributeHook", [ ] ], [ "popParent", [] ] ]); }); @@ -272,29 +272,29 @@ test("attribute helpers", function() { [ "pushLiteral", [ "bar" ] ], [ "prepareArray", [ 1 ] ], [ "pushLiteral", [ "foo" ] ], - [ "pushSexprHook", [ 0 ] ], + [ "pushSexprHook", [ ] ], [ "pushLiteral", [ "before " ] ], [ "prepareArray", [ 3 ] ], [ "pushConcatHook", [ 0 ] ], [ "pushLiteral", [ "class" ] ], [ "createAttrMorph", [ 0, 0, "class", true, null ] ], - [ "printAttributeHook", [ 0 ] ], - [ "pushGetHook", [ 'bare', 1 ] ], + [ "printAttributeHook", [ ] ], + [ "pushGetHook", [ 'bare' ] ], [ "pushLiteral", [ 'id' ] ], [ "createAttrMorph", [ 1, 0, 'id', true, null ] ], - [ "printAttributeHook", [ 1 ] ], + [ "printAttributeHook", [ ] ], [ "popParent", [] ], [ "createMorph", [ 2, [], 1, 1, true ] ], [ "pushLiteral", [ 'morphThing' ] ], - [ "printContentHook", [ 2 ] ], + [ "printContentHook", [ ] ], [ "consumeParent", [ 2 ] ], - [ "pushGetHook", [ 'ohMy', 3 ] ], + [ "pushGetHook", [ 'ohMy' ] ], [ "prepareArray", [ 1 ] ], [ "pushConcatHook", [ 3 ] ], [ "pushLiteral", [ 'class' ] ], [ "shareElement", [ 1 ] ], [ "createAttrMorph", [ 3, 1, 'class', true, null ] ], - [ "printAttributeHook", [ 3 ] ], + [ "printAttributeHook", [ ] ], [ "popParent", [] ] ]); }); From cf27996e3104524146c56a25a523f9be04364491 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Mon, 9 Feb 2015 12:07:03 -0800 Subject: [PATCH 20/27] Use extracted morph-range library --- packages/dom-helper/lib/main.js | 2 +- packages/htmlbars-runtime/lib/render.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/dom-helper/lib/main.js b/packages/dom-helper/lib/main.js index d1afb226..3f31d4d0 100644 --- a/packages/dom-helper/lib/main.js +++ b/packages/dom-helper/lib/main.js @@ -1,4 +1,4 @@ -import Morph from "./morph-range"; +import Morph from "../morph-range"; import AttrMorph from "./morph-attr"; import { buildHTMLDOM, diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index 021815dd..17b7c51e 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -18,7 +18,7 @@ export default function render(template, context, env, options, blockArguments) } else { rootNode = dom.createMorph(null, fragment.firstChild, fragment.lastChild, contextualElement); ownerNode = rootNode; - rootNode.ownerNode = rootNode; + initializeNode(rootNode, ownerNode); } // TODO Invoke disposal hook recursively on old rootNode.childNodes @@ -26,7 +26,7 @@ export default function render(template, context, env, options, blockArguments) rootNode.childNodes = nodes; forEach(nodes, function(node) { - node.ownerNode = ownerNode; + initializeNode(node, ownerNode); }); var statements = template.statements; @@ -59,6 +59,12 @@ export default function render(template, context, env, options, blockArguments) } } +function initializeNode(node, owner) { + node.ownerNode = owner; + node.state = {}; + node.isDirty = true; +} + export function getCachedFragment(template, env) { var dom = env.dom, fragment; if (env.useFragmentCache && dom.canClone) { From 5a223d1e7c1e2b28ff3e163ddd038bcc0f55906c Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Tue, 10 Feb 2015 14:28:39 -0800 Subject: [PATCH 21/27] Ensure templates always have stable boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, if a template’s first (or last) child was a dynamic node (like a mustache or block), the associated render node’s firstChild or lastChild would change. ```hbs {{#if condition}}{{value}}{{/if}} ``` In this case, the render node associated with the `#if` helper cannot have stable `firstNode` and `lastNode`, because when the nodes associated with `value` change, the nodes associated with the `if` helper must change as well. The solution was to require that templates create fragments with stable start and end nodes. If the first node is dynamic, we insert a boundary empty text node to ensure that the template’s boundaries will not change. --- .../lib/hydration-javascript-compiler.js | 24 +++++++++++--- .../lib/hydration-opcode-compiler.js | 4 +++ .../lib/template-compiler.js | 32 +++++++++++++++++- .../htmlbars-compiler/tests/dirtying-test.js | 33 +++++++++++++++++++ .../tests/html-compiler-test.js | 1 + 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index 85ea7fee..1c34f462 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -32,6 +32,8 @@ prototype.compile = function(opcodes, options) { this.statements = []; this.expressionStack = []; this.augmentContext = []; + this.hasOpenBoundary = false; + this.hasCloseBoundary = false; processOpcodes(this, opcodes); @@ -74,23 +76,35 @@ prototype.compile = function(opcodes, options) { if (this.fragmentProcessing.length) { var processing = ""; for (i = 0, l = this.fragmentProcessing.length; i < l; ++i) { - processing += this.indent+this.fragmentProcessing[i]+'\n'; + processing += this.indent+' '+this.fragmentProcessing[i]+'\n'; } result.fragmentProcessingProgram = processing; } + var createMorphsProgram; if (result.hasMorphs) { - result.createMorphsProgram = + createMorphsProgram = 'function buildRenderNodes(dom, fragment, contextualElement) {\n' + - result.fragmentProcessingProgram + - morphs + + result.fragmentProcessingProgram + morphs; + + if (this.hasOpenBoundary) { + createMorphsProgram += indent+" dom.insertBoundary(fragment, 0);\n"; + } + + if (this.hasCloseBoundary) { + createMorphsProgram += indent+" dom.insertBoundary(fragment, null);\n"; + } + + createMorphsProgram += indent + ' return morphs;\n' + indent+'}'; } else { - result.createMorphsProgram = + createMorphsProgram = 'function buildRenderNodes() { return []; }'; } + result.createMorphsProgram = createMorphsProgram; + return result; }; diff --git a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js index 8aea3f3f..e5677b2a 100644 --- a/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-opcode-compiler.js @@ -71,6 +71,10 @@ HydrationOpcodeCompiler.prototype.startProgram = function(program, c, blankChild } }; +HydrationOpcodeCompiler.prototype.insertBoundary = function(first) { + this.opcode(first ? 'openBoundary' : 'closeBoundary'); +}; + HydrationOpcodeCompiler.prototype.endProgram = function() { distributeMorphs(this.morphs, this.opcodes); }; diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index e581a322..8c6fe77a 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -18,11 +18,37 @@ function TemplateCompiler(options) { export default TemplateCompiler; +var dynamicNodes = { + mustache: true, + block: true, + component: true +}; + TemplateCompiler.prototype.compile = function(ast) { var templateVisitor = new TemplateVisitor(); templateVisitor.visit(ast); - processOpcodes(this, templateVisitor.actions); + var normalizedActions = []; + var actions = templateVisitor.actions; + + for (var i=0, l=actions.length - 1; i

Nothing

'); }); +test("block helpers whose template has a morph at the edge", function() { + registerHelper('id', function(params, hash, options) { + return options.template.render(this); + }); + + var template = compile("{{#id}}{{value}}{{/id}}"); + var object = { value: "hello world" }; + var result = template.render(object, env); + + equalTokens(result.fragment, 'hello world'); + var firstNode = result.root.firstNode; + equal(firstNode.nodeType, 3, "first node of the parent template"); + equal(firstNode.textContent, "", "its content should be empty"); + + var secondNode = firstNode.nextSibling; + equal(secondNode.nodeType, 3, "first node of the helper template should be a text node"); + equal(secondNode.textContent, "", "its content should be empty"); + + var textContent = secondNode.nextSibling; + equal(textContent.nodeType, 3, "second node of the helper should be a text node"); + equal(textContent.textContent, "hello world", "its content should be hello world"); + + var fourthNode = textContent.nextSibling; + equal(fourthNode.nodeType, 3, "last node of the helper should be a text node"); + equal(fourthNode.textContent, "", "its content should be empty"); + + var lastNode = fourthNode.nextSibling; + equal(lastNode.nodeType, 3, "last node of the parent template should be a text node"); + equal(lastNode.textContent, "", "its content should be empty"); + + strictEqual(lastNode.nextSibling, null, "there should only be five nodes"); +}); + test("clean content doesn't get blown away", function() { var template = compile("
{{value}}
"); var object = { value: "hello" }; diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index 7a00c3a1..f295459d 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -1187,6 +1187,7 @@ test("svg can live with hydration", function() { var template = compile('{{name}}'); var fragment = template.render({ name: 'Milly' }, env, { contextualElement: document.body }).fragment; + equal( fragment.childNodes[0].namespaceURI, svgNamespace, "svg namespace inside a block is present" ); From 40823796e0fb821a702cf9660c58949490339036 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Tue, 10 Feb 2015 15:57:11 -0800 Subject: [PATCH 22/27] Clean up scope handling This commit refactors the internal scope handling of templates so that the concept of a scope, including block parameters, is rigorous. It also stops leaking the caller's scope into a helper call. This commit has two primary changes: * The internal "scope" is now broken up into `self` and `locals`. Block helpers now receive `options.template.yield`, which is used like `options.template.yield([ optional, block, params ])`. If block parameters are supplied, HTMLBars will ask the host to create a new scope frame, and the `locals` hash will be populated with the yielded parameters. It is still possible to shift the `self` by using `options.template.render(newSelf, [ optional, block, params ])`, which will always create a new scope frame. Since self-shifting helpers are intended to be rare, `yield` is the new path, and encapsulates all of the scoping details. * Helpers and block helpers no longer receive the `self` as `this`, because they are intended to be analogous to functions. Functions do not have access to the scope of the caller O_o. Note that hooks, which are used by the host to implement the scoping semantics, of course still have access to the scope. --- .../lib/hydration-javascript-compiler.js | 6 +- .../lib/template-compiler.js | 4 +- .../htmlbars-compiler/tests/dirtying-test.js | 20 +-- .../tests/html-compiler-test.js | 92 ++++++------- packages/htmlbars-runtime/lib/hooks.js | 122 ++++++++++++------ packages/htmlbars-runtime/lib/render.js | 28 ++-- packages/htmlbars-runtime/tests/main-test.js | 3 +- 7 files changed, 158 insertions(+), 117 deletions(-) diff --git a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js index 1c34f462..21e03828 100644 --- a/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js +++ b/packages/htmlbars-compiler/lib/hydration-javascript-compiler.js @@ -31,7 +31,7 @@ prototype.compile = function(opcodes, options) { this.hasCloseBoundary = false; this.statements = []; this.expressionStack = []; - this.augmentContext = []; + this.locals = []; this.hasOpenBoundary = false; this.hasCloseBoundary = false; @@ -56,7 +56,7 @@ prototype.compile = function(opcodes, options) { hydrateMorphsProgram: '', fragmentProcessingProgram: '', statements: this.statements, - augmentContext: this.augmentContext, + locals: this.locals, hasMorphs: false }; @@ -158,7 +158,7 @@ prototype.pushConcatHook = function() { }; prototype.printSetHook = function(name) { - this.augmentContext.push(name); + this.locals.push(name); }; prototype.printBlockHook = function(templateId, inverseId) { diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index 8c6fe77a..1f9c9aed 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -122,7 +122,7 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { return indent+' '+JSON.stringify(s); }).join(",\n"); - var augmentContext = JSON.stringify(hydrationPrograms.augmentContext); + var locals = JSON.stringify(hydrationPrograms.locals); var templates = this.childTemplates.map(function(_, index) { return 'child' + index; @@ -140,7 +140,7 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' buildRenderNodes: ' + hydrationPrograms.createMorphsProgram + ',\n' + indent+' statements: [\n' + statements + '\n' + indent+' ],\n' + - indent+' augmentContext: ' + augmentContext + ',\n' + + indent+' locals: ' + locals + ',\n' + indent+' templates: [' + templates + ']\n' + indent+' };\n' + indent+'}())'; diff --git a/packages/htmlbars-compiler/tests/dirtying-test.js b/packages/htmlbars-compiler/tests/dirtying-test.js index 833dc36f..d45374a3 100644 --- a/packages/htmlbars-compiler/tests/dirtying-test.js +++ b/packages/htmlbars-compiler/tests/dirtying-test.js @@ -49,9 +49,9 @@ test("a simple implementation of a dirtying rerender", function() { state.condition = normalized; if (normalized) { - return options.template.render(this); + return options.template.yield(); } else { - return options.inverse.render(this); + return options.inverse.yield(); } } }); @@ -86,7 +86,7 @@ test("a simple implementation of a dirtying rerender", function() { test("block helpers whose template has a morph at the edge", function() { registerHelper('id', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); var template = compile("{{#id}}{{value}}{{/id}}"); @@ -137,7 +137,7 @@ test("clean content doesn't get blown away", function() { }; object.value = "hello"; - textRenderNode.isDirty = true; + result.dirty(); result.revalidate(); }); @@ -160,7 +160,7 @@ test("helper calls follow the normal dirtying rules", function() { var textRenderNode = result.root.childNodes[0]; - textRenderNode.isDirty = true; + result.dirty(); result.revalidate(); equalTokens(result.fragment, '
GOODBYE
'); @@ -171,7 +171,7 @@ test("helper calls follow the normal dirtying rules", function() { // Checks normalized value, not raw value object.value = "GoOdByE"; - textRenderNode.isDirty = true; + result.dirty(); result.revalidate(); }); @@ -189,7 +189,7 @@ test("attribute nodes follow the normal dirtying rules", function() { var attrRenderNode = result.root.childNodes[0]; - attrRenderNode.isDirty = true; + result.dirty(); result.revalidate(); equalTokens(result.fragment, "
hello
"); @@ -199,7 +199,7 @@ test("attribute nodes follow the normal dirtying rules", function() { }; object.value = "universe"; - attrRenderNode.isDirty = true; + result.dirty(); result.revalidate(); }); @@ -217,7 +217,7 @@ test("attribute nodes w/ concat follow the normal dirtying rules", function() { var attrRenderNode = result.root.childNodes[0]; - attrRenderNode.isDirty = true; + result.dirty(); result.revalidate(); equalTokens(result.fragment, "
hello
"); @@ -227,6 +227,6 @@ test("attribute nodes w/ concat follow the normal dirtying rules", function() { }; object.value = "universe"; - attrRenderNode.isDirty = true; + result.dirty(); result.revalidate(); }); diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index f295459d..54dc437d 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -4,7 +4,7 @@ import defaultHooks from "../htmlbars-runtime/hooks"; import defaultHelpers from "../htmlbars-runtime/helpers"; import { merge } from "../htmlbars-util/object-utils"; import DOMHelper from "../dom-helper"; -import { createObject, normalizeInnerHTML, getTextContent, equalTokens } from "../htmlbars-test-helpers"; +import { normalizeInnerHTML, getTextContent, equalTokens } from "../htmlbars-test-helpers"; var xhtmlNamespace = "http://www.w3.org/1999/xhtml", svgNamespace = "http://www.w3.org/2000/svg"; @@ -293,7 +293,7 @@ test("The compiler can handle top-level unescaped td inside tr contextualElement test("The compiler can handle unescaped tr in top of content", function() { registerHelper('test', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); var template = compile('{{#test}}{{{html}}}{{/test}}'); @@ -307,7 +307,7 @@ test("The compiler can handle unescaped tr in top of content", function() { test("The compiler can handle unescaped tr inside fragment table", function() { registerHelper('test', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); var template = compile('{{#test}}{{{html}}}{{/test}}
'); @@ -330,7 +330,7 @@ test("The compiler can handle simple helpers", function() { test("Helpers propagate the owner render node", function() { registerHelper('id', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); var template = compile('
{{#id}}

{{#id}}{{#id}}{{name}}{{/id}}{{/id}}

{{/id}}
'); @@ -374,7 +374,7 @@ test("Simple data binding using text nodes", function() { hooks.content = function(env, morph, context, path) { callback = function() { - morph.setContent(context[path]); + morph.setContent(context.self[path]); }; callback(); }; @@ -399,7 +399,7 @@ test("Simple data binding on fragments", function() { hooks.content = function(env, morph, context, path) { morph.parseTextAsHTML = true; callback = function() { - morph.setContent(context[path]); + morph.setContent(context.self[path]); }; callback(); }; @@ -421,7 +421,7 @@ test("Simple data binding on fragments", function() { test("Simple data binding on fragments - re-rendering", function() { hooks.content = function(env, morph, context, path) { morph.parseTextAsHTML = true; - morph.setContent(context[path]); + morph.setContent(context.self[path]); }; var object = { title: '

hello

to the' }; @@ -691,7 +691,7 @@ test("Attribute runs can contain helpers", function() { */ test("A simple block helper can return the default document fragment", function() { registerHelper('testing', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); compilesTo('{{#testing}}
123
{{/testing}}', '
123
'); @@ -700,7 +700,7 @@ test("A simple block helper can return the default document fragment", function( // TODO: NEXT test("A simple block helper can return text", function() { registerHelper('testing', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); compilesTo('{{#testing}}test{{else}}not shown{{/testing}}', 'test'); @@ -708,7 +708,7 @@ test("A simple block helper can return text", function() { test("A block helper can have an else block", function() { registerHelper('testing', function(params, hash, options) { - return options.inverse.render(this); + return options.inverse.yield(); }); compilesTo('{{#testing}}Nope{{else}}
123
{{/testing}}', '
123
'); @@ -726,7 +726,7 @@ test("A block helper can pass a context to be used in the child", function() { test("Block helpers receive hash arguments", function() { registerHelper('testing', function(params, hash, options) { if (hash.truth) { - return options.template.render(this); + return options.template.yield(); } }); @@ -780,28 +780,24 @@ test("Node helpers can modify the node after many nodes returned from top-level }); test("Node helpers can be used for attribute bindings", function() { - var callback; - registerHelper('testing', function(params, hash, options) { - var path = hash.href, + var value = hash.href, element = options.element; - var context = this; - - callback = function() { - var value = context[path]; - element.setAttribute('href', value); - }; - callback(); + element.setAttribute('href', value); }); var object = { url: 'linky.html' }; - var fragment = compilesTo('linky', 'linky', object); + var template = compile('linky'); + var result = template.render(object, env); + equalTokens(result.fragment, 'linky'); object.url = 'zippy.html'; - callback(); - equalTokens(fragment, 'linky'); + result.dirty(); + result.revalidate(); + + equalTokens(result.fragment, 'linky'); }); @@ -811,7 +807,7 @@ test('Components - Called as helpers', function () { registerHelper('x-append', function(params, hash, options, env) { var rootNode = options.renderNode; options.renderNode = null; - var result = options.template.render(this); + var result = options.template.yield(); options.renderNode = rootNode; xAppendComponent.render({ yield: result.fragment, text: hash.text }, env, options); }); @@ -866,21 +862,18 @@ function yieldTemplate(parentTemplate, options, callback) { test("Block params", function() { registerHelper('a', function(params, hash, options) { - var context = createObject(this); yieldTemplate("A({{yield}})", options, function() { - return options.template.render(context, ['W', 'X1']); + return options.template.yield(['W', 'X1']); }); }); registerHelper('b', function(params, hash, options) { - var context = createObject(this); yieldTemplate("B({{yield}})", options, function() { - return options.template.render(context, ['X2', 'Y']); + return options.template.yield(['X2', 'Y']); }); }); registerHelper('c', function(params, hash, options) { - var context = createObject(this); yieldTemplate("C({{yield}})", options, function() { - return options.template.render(context, ['Z']); + return options.template.yield(['Z']); }); }); var t = '{{#a as |w x|}}{{w}},{{x}} {{#b as |x y|}}{{x}},{{y}}{{/b}} {{w}},{{x}} {{#c as |z|}}{{x}},{{z}}{{/c}}{{/a}}'; @@ -891,20 +884,19 @@ test("Block params - Helper should know how many block params it was called with expect(4); registerHelper('count-block-params', function(params, hash, options) { - equal(options.template.blockParams, this.count, 'Helpers should receive the correct number of block params in options.template.blockParams.'); + equal(options.template.blockParams, hash.count, 'Helpers should receive the correct number of block params in options.template.blockParams.'); }); - compile('{{#count-block-params}}{{/count-block-params}}').render({ count: 0 }, env, { contextualElement: document.body }); - compile('{{#count-block-params as |x|}}{{/count-block-params}}').render({ count: 1 }, env, { contextualElement: document.body }); - compile('{{#count-block-params as |x y|}}{{/count-block-params}}').render({ count: 2 }, env, { contextualElement: document.body }); - compile('{{#count-block-params as |x y z|}}{{/count-block-params}}').render({ count: 3 }, env, { contextualElement: document.body }); + compile('{{#count-block-params count=0}}{{/count-block-params}}').render({}, env, { contextualElement: document.body }); + compile('{{#count-block-params count=1 as |x|}}{{/count-block-params}}').render({}, env, { contextualElement: document.body }); + compile('{{#count-block-params count=2 as |x y|}}{{/count-block-params}}').render({}, env, { contextualElement: document.body }); + compile('{{#count-block-params count=3 as |x y z|}}{{/count-block-params}}').render({}, env, { contextualElement: document.body }); }); test('Block params in HTML syntax', function () { registerHelper('x-bar', function(params, hash, options) { - var context = this; yieldTemplate("BAR({{yield}})", options, function() { - return options.template.render(context, ['Xerxes', 'York', 'Zed']); + return options.template.yield(['Xerxes', 'York', 'Zed']); }); }); compilesTo('{{zee}},{{y}},{{x}}', 'BAR(Zed,York,Xerxes)', {}); @@ -924,7 +916,7 @@ test('Block params in HTML syntax - Throws exception if given zero parameters', test('Block params in HTML syntax - Works with a single parameter', function () { registerHelper('x-bar', function(params, hash, options) { - return options.template.render({}, ['Xerxes']); + return options.template.yield(['Xerxes']); }); compilesTo('{{x}}', 'Xerxes', {}); }); @@ -940,7 +932,7 @@ test('Block params in HTML syntax - Ignores whitespace', function () { expect(3); registerHelper('x-bar', function(params, hash, options) { - return options.template.render({}, ['Xerxes', 'York']); + return options.template.yield(['Xerxes', 'York']); }); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); @@ -951,13 +943,13 @@ test('Block params in HTML syntax - Helper should know how many block params it expect(4); registerHelper('count-block-params', function(params, hash, options) { - equal(options.template.blockParams, this.count, 'Helpers should receive the correct number of block params in options.template.blockParams.'); + equal(options.template.blockParams, parseInt(hash.count, 10), 'Helpers should receive the correct number of block params in options.template.blockParams.'); }); - compile('').render({ count: 0 }, env, { contextualElement: document.body }); - compile('').render({ count: 1 }, env, { contextualElement: document.body }); - compile('').render({ count: 2 }, env, { contextualElement: document.body }); - compile('').render({ count: 3 }, env, { contextualElement: document.body }); + compile('').render({ count: 0 }, env, { contextualElement: document.body }); + compile('').render({ count: 1 }, env, { contextualElement: document.body }); + compile('').render({ count: 2 }, env, { contextualElement: document.body }); + compile('').render({ count: 3 }, env, { contextualElement: document.body }); }); test("Block params in HTML syntax - Throws an error on invalid block params syntax", function() { @@ -1237,9 +1229,9 @@ test("Block helper allows interior namespace", function() { registerHelper('testing', function(params, hash, options) { if (isTrue) { - return options.template.render(this); + return options.template.yield(); } else { - return options.inverse.render(this); + return options.inverse.yield(); } }); @@ -1262,7 +1254,7 @@ test("Block helper allows interior namespace", function() { test("Block helper allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); var template = compile('
{{#testing}}{{/testing}}
'); @@ -1277,7 +1269,7 @@ test("Block helper allows namespace to bleed through", function() { test("Block helper with root svg allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); var template = compile('{{#testing}}{{/testing}}'); @@ -1292,7 +1284,7 @@ test("Block helper with root svg allows namespace to bleed through", function() test("Block helper with root foreignObject allows namespace to bleed through", function() { registerHelper('testing', function(params, hash, options) { - return options.template.render(this); + return options.template.yield(); }); var template = compile('{{#testing}}
{{/testing}}
'); diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 9184d822..8b3c272b 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -6,25 +6,44 @@ export function wrap(template) { return { isHTMLBars: true, blockParams: template.blockParams, - render: function(context, env, options, blockArguments) { - return render(template, context, env, options, blockArguments); + render: function(self, env, options, blockArguments) { + var scope = env.hooks.createScope(null, template.blockParams); + scope.self = self; + return render(template, scope, env, options, blockArguments); } }; } -export function wrapForHelper(template, options, env) { +export function wrapForHelper(template, originalScope, options, env) { if (template === null) { return null; } return { isHTMLBars: true, blockParams: template.blockParams, - render: function(context, blockArguments) { - return render(template, context, env, options, blockArguments); + + yield: function(blockArguments) { + var scope = originalScope; + + if (blockArguments !== undefined) { + scope = env.hooks.createScope(originalScope, template.blockParams); + } + + return render(template, scope, env, options, blockArguments); + }, + + render: function(newSelf, blockArguments) { + var scope = originalScope; + if (newSelf !== originalScope.self || blockArguments !== undefined) { + scope = env.hooks.createScope(originalScope, template.blockParams); + scope.self = newSelf; + } + + return render(template, scope, env, options, blockArguments); } }; } -function optionsFor(morph, env, template, inverse) { +function optionsFor(morph, scope, env, template, inverse) { var options = { renderNode: morph, contextualElement: morph.contextualElement, @@ -33,39 +52,56 @@ function optionsFor(morph, env, template, inverse) { inverse: null }; - options.template = wrapForHelper(template, options, env); - options.inverse = wrapForHelper(inverse, options, env); + options.template = wrapForHelper(template, scope, options, env); + options.inverse = wrapForHelper(inverse, scope, options, env); return options; } -export function block(env, morph, context, path, params, hash, template, inverse) { +export function createScope(parentScope, localVariables) { + var scope; + + if (parentScope) { + scope = Object.create(parentScope); + scope.locals = Object.create(parentScope.locals); + } else { + scope = { self: null, locals: {} }; + } + + for (var i=0, l=localVariables.length; i Date: Tue, 10 Feb 2015 16:16:29 -0800 Subject: [PATCH 23/27] Make ordering of scope/env consistent --- packages/htmlbars-runtime/lib/hooks.js | 32 +++++++++++++++++-------- packages/htmlbars-runtime/lib/render.js | 2 +- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 8b3c272b..6234a7a3 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -62,8 +62,8 @@ export function createScope(parentScope, localVariables) { var scope; if (parentScope) { - scope = Object.create(parentScope); - scope.locals = Object.create(parentScope.locals); + scope = createObject(parentScope); + scope.locals = createObject(parentScope.locals); } else { scope = { self: null, locals: {} }; } @@ -81,7 +81,7 @@ export function block(env, morph, scope, path, params, hash, template, inverse) if (morph.isDirty) { var options = optionsFor(morph, scope, env, template, inverse); - var helper = lookupHelper(env, scope, path); + var helper = lookupHelper(scope, env, path); var result = helper(params, hash, options, env); if (result === undefined && state.lastResult) { @@ -99,7 +99,7 @@ export function block(env, morph, scope, path, params, hash, template, inverse) export function inline(env, morph, scope, path, params, hash) { if (morph.isDirty) { var state = morph.state; - var helper = lookupHelper(env, scope, path); + var helper = lookupHelper(scope, env, path); var value = helper(params, hash, { renderNode: morph }, env); @@ -115,7 +115,7 @@ export function inline(env, morph, scope, path, params, hash) { export function content(env, morph, scope, path) { if (morph.isDirty) { var state = morph.state; - var helper = lookupHelper(env, scope, path); + var helper = lookupHelper(scope, env, path); var value; if (helper) { @@ -135,7 +135,7 @@ export function content(env, morph, scope, path) { export function element(env, morph, scope, path, params, hash) { if (morph.isDirty) { - var helper = lookupHelper(env, scope, path); + var helper = lookupHelper(scope, env, path); if (helper) { helper(params, hash, { element: morph.element }, env); } @@ -160,7 +160,7 @@ export function attribute(env, morph, name, value) { export function subexpr(env, morph, scope, helperName, params, hash) { if (!morph.isDirty) { return; } - var helper = lookupHelper(env, scope, helperName); + var helper = lookupHelper(scope, env, helperName); if (helper) { return helper(params, hash, {}, env); } else { @@ -188,13 +188,13 @@ export function get(env, morph, scope, path) { return value; } -export function bindLocal(env, scope, name, value) { +export function bindLocal(scope, env, name, value) { scope.locals[name] = value; } export function component(env, morph, scope, tagName, attrs, template) { if (morph.isDirty) { - var helper = lookupHelper(env, scope, tagName); + var helper = lookupHelper(scope, env, tagName); if (helper) { var options = optionsFor(morph, scope, env, template, null); helper([], attrs, options, env); @@ -226,10 +226,22 @@ function componentFallback(env, morph, scope, tagName, attrs, template) { morph.setNode(element); } -function lookupHelper(env, scope, helperName) { +function lookupHelper(scope, env, helperName) { return env.helpers[helperName]; } +// IE8 does not have Object.create, so use a polyfill if needed. +// Polyfill based on Mozilla's (MDN) +export function createObject(obj) { + if (typeof Object.create === 'function') { + return Object.create(obj); + } else { + var Temp = function() {}; + Temp.prototype = obj; + return new Temp(); + } +} + export default { createScope: createScope, content: content, diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index b0886ee1..352024c6 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -60,7 +60,7 @@ export default function render(template, scope, env, options, blockArguments) { var i, l; for (i=0, l=locals.length; i Date: Thu, 12 Feb 2015 14:44:17 -0800 Subject: [PATCH 24/27] Tighten up argument order This commit ensures that when any of `morph`, `env`, `scope`, `options`, or `blockArguments` exist in a function signature, that they appear in a consistent order (namely the one we used above). This commit also ends the practice of passing `env` to helpers, because helpers should be seen as functions operating within the scope system of HTMLBars, not arbitrary escape valves that can contort and abuse the template scope. Instead, environments implement the scoping system (and any keywords) by customizing the hooks invoked by template rendering. As of this commit, `partial` is a hook-defined keyword. In Ember `yield`, which also needs access to ambient scope information, will also be a hook-defined keyword. This finishes a separation between the hooks, which are used by environments to define the "programming language" of HTMLBars, and helpes, which are user-provided functions that operate within the scoping rules of HTMLBars. --- .../htmlbars-compiler/tests/dirtying-test.js | 3 +- .../tests/html-compiler-test.js | 19 ++--- .../lib/expression-visitor.js | 70 +++++++-------- packages/htmlbars-runtime/lib/helpers.js | 8 -- packages/htmlbars-runtime/lib/hooks.js | 85 +++++++++++-------- packages/htmlbars-runtime/lib/main.js | 2 - packages/htmlbars-runtime/lib/render.js | 6 +- packages/htmlbars-runtime/tests/main-test.js | 1 + 8 files changed, 97 insertions(+), 97 deletions(-) delete mode 100644 packages/htmlbars-runtime/lib/helpers.js diff --git a/packages/htmlbars-compiler/tests/dirtying-test.js b/packages/htmlbars-compiler/tests/dirtying-test.js index d45374a3..c96c2460 100644 --- a/packages/htmlbars-compiler/tests/dirtying-test.js +++ b/packages/htmlbars-compiler/tests/dirtying-test.js @@ -1,6 +1,5 @@ import { compile } from "../htmlbars-compiler/compiler"; import defaultHooks from "../htmlbars-runtime/hooks"; -import defaultHelpers from "../htmlbars-runtime/helpers"; import { merge } from "../htmlbars-util/object-utils"; import DOMHelper from "../dom-helper"; import { equalTokens } from "../htmlbars-test-helpers"; @@ -13,7 +12,7 @@ function registerHelper(name, callback) { function commonSetup() { hooks = merge({}, defaultHooks); - helpers = merge({}, defaultHelpers); + helpers = {}; partials = {}; env = { diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index 54dc437d..55ceb900 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -1,7 +1,6 @@ import { compile } from "../htmlbars-compiler/compiler"; import { forEach } from "../htmlbars-util/array-utils"; import defaultHooks from "../htmlbars-runtime/hooks"; -import defaultHelpers from "../htmlbars-runtime/helpers"; import { merge } from "../htmlbars-util/object-utils"; import DOMHelper from "../dom-helper"; import { normalizeInnerHTML, getTextContent, equalTokens } from "../htmlbars-test-helpers"; @@ -43,7 +42,7 @@ function compilesTo(html, expected, context) { function commonSetup() { hooks = merge({}, defaultHooks); - helpers = merge({}, defaultHelpers); + helpers = {}; partials = {}; env = { @@ -372,9 +371,9 @@ test("The compiler passes along the hash arguments", function() { test("Simple data binding using text nodes", function() { var callback; - hooks.content = function(env, morph, context, path) { + hooks.content = function(morph, env, scope, path) { callback = function() { - morph.setContent(context.self[path]); + morph.setContent(scope.self[path]); }; callback(); }; @@ -396,10 +395,10 @@ test("Simple data binding using text nodes", function() { test("Simple data binding on fragments", function() { var callback; - hooks.content = function(env, morph, context, path) { + hooks.content = function(morph, env, scope, path) { morph.parseTextAsHTML = true; callback = function() { - morph.setContent(context.self[path]); + morph.setContent(scope.self[path]); }; callback(); }; @@ -419,9 +418,9 @@ test("Simple data binding on fragments", function() { }); test("Simple data binding on fragments - re-rendering", function() { - hooks.content = function(env, morph, context, path) { + hooks.content = function(morph, env, scope, path) { morph.parseTextAsHTML = true; - morph.setContent(context.self[path]); + morph.setContent(scope.self[path]); }; var object = { title: '

hello

to the' }; @@ -461,7 +460,7 @@ test("second render respects whitespace", function () { test("morph receives escaping information", function() { expect(3); - hooks.content = function(env, morph, context, path) { + hooks.content = function(morph, env, scope, path) { if (path === 'escaped') { equal(morph.parseTextAsHTML, false); } else if (path === 'unescaped') { @@ -804,7 +803,7 @@ test("Node helpers can be used for attribute bindings", function() { test('Components - Called as helpers', function () { var xAppendComponent = compile('{{yield}}{{text}}'); - registerHelper('x-append', function(params, hash, options, env) { + registerHelper('x-append', function(params, hash, options) { var rootNode = options.renderNode; options.renderNode = null; var result = options.template.yield(); diff --git a/packages/htmlbars-runtime/lib/expression-visitor.js b/packages/htmlbars-runtime/lib/expression-visitor.js index 395e107c..6c3bbf46 100644 --- a/packages/htmlbars-runtime/lib/expression-visitor.js +++ b/packages/htmlbars-runtime/lib/expression-visitor.js @@ -1,5 +1,5 @@ export default { - accept: function(node, morph, context, env, template) { + accept: function(node, morph, env, scope, template) { // Primitive literals are unambiguously non-array representations of // themselves. if (typeof node !== 'object') { @@ -7,87 +7,87 @@ export default { } var type = node[0]; - return this[type](node, morph, context, env, template); + return this[type](node, morph, env, scope, template); }, - acceptArray: function(nodes, morph, context, env, template) { + acceptArray: function(nodes, morph, env, scope, template) { return nodes.map(function(node) { - return this.accept(node, morph, context, env, template); + return this.accept(node, morph, env, scope, template); }, this); }, - acceptObject: function(pairs, morph, context, env, template) { + acceptObject: function(pairs, morph, env, scope, template) { var object = {}; for (var i=0, l=pairs.length; i Date: Thu, 12 Feb 2015 15:41:12 -0800 Subject: [PATCH 25/27] Provide `this.yield` sugar in block helpers ```js registerHelper('random', function() { if (Math.random() > 0.5) { return this.yield(); } }); ``` This allows you to write a helper that takes a block and easily invokes it. The `yield` method can take block parameters: ```hbs {{#count as |i|}}

Rendered {{i}} times

{{/count}} ``` ```js var count = 0; registerHelper('count', function() { this.yield([ ++count ]); }); ``` Behind the scenes, this method passes along the entire current scope as well as the render node being filled in and the contextual element. --- .../tests/html-compiler-test.js | 69 +++++++++---------- packages/htmlbars-runtime/lib/hooks.js | 8 ++- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index 55ceb900..541440be 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -291,8 +291,8 @@ test("The compiler can handle top-level unescaped td inside tr contextualElement }); test("The compiler can handle unescaped tr in top of content", function() { - registerHelper('test', function(params, hash, options) { - return options.template.yield(); + registerHelper('test', function() { + return this.yield(); }); var template = compile('{{#test}}{{{html}}}{{/test}}'); @@ -305,8 +305,8 @@ test("The compiler can handle unescaped tr in top of content", function() { }); test("The compiler can handle unescaped tr inside fragment table", function() { - registerHelper('test', function(params, hash, options) { - return options.template.yield(); + registerHelper('test', function() { + return this.yield(); }); var template = compile('{{#test}}{{{html}}}{{/test}}
'); @@ -328,8 +328,8 @@ test("The compiler can handle simple helpers", function() { }); test("Helpers propagate the owner render node", function() { - registerHelper('id', function(params, hash, options) { - return options.template.yield(); + registerHelper('id', function() { + return this.yield(); }); var template = compile('
{{#id}}

{{#id}}{{#id}}{{name}}{{/id}}{{/id}}

{{/id}}
'); @@ -689,18 +689,14 @@ test("Attribute runs can contain helpers", function() { }); */ test("A simple block helper can return the default document fragment", function() { - registerHelper('testing', function(params, hash, options) { - return options.template.yield(); - }); + registerHelper('testing', function() { return this.yield(); }); compilesTo('{{#testing}}
123
{{/testing}}', '
123
'); }); // TODO: NEXT test("A simple block helper can return text", function() { - registerHelper('testing', function(params, hash, options) { - return options.template.yield(); - }); + registerHelper('testing', function() { return this.yield(); }); compilesTo('{{#testing}}test{{else}}not shown{{/testing}}', 'test'); }); @@ -723,9 +719,9 @@ test("A block helper can pass a context to be used in the child", function() { }); test("Block helpers receive hash arguments", function() { - registerHelper('testing', function(params, hash, options) { + registerHelper('testing', function(params, hash) { if (hash.truth) { - return options.template.yield(); + return this.yield(); } }); @@ -806,7 +802,7 @@ test('Components - Called as helpers', function () { registerHelper('x-append', function(params, hash, options) { var rootNode = options.renderNode; options.renderNode = null; - var result = options.template.yield(); + var result = this.yield(); options.renderNode = rootNode; xAppendComponent.render({ yield: result.fragment, text: hash.text }, env, options); }); @@ -851,29 +847,30 @@ test("Simple elements can have dashed attributes", function() { equalTokens(fragment, '
content
'); }); -function yieldTemplate(parentTemplate, options, callback) { +function yieldTemplate(parentTemplate, options, callback, bind) { var node = options.renderNode; options.renderNode = null; - var child = callback(); + var child = callback.call(bind); options.renderNode = node; + compile(parentTemplate).render({ yield: child.fragment }, env, options); } test("Block params", function() { registerHelper('a', function(params, hash, options) { yieldTemplate("A({{yield}})", options, function() { - return options.template.yield(['W', 'X1']); - }); + return this.yield(['W', 'X1']); + }, this); }); registerHelper('b', function(params, hash, options) { yieldTemplate("B({{yield}})", options, function() { - return options.template.yield(['X2', 'Y']); - }); + return this.yield(['X2', 'Y']); + }, this); }); registerHelper('c', function(params, hash, options) { yieldTemplate("C({{yield}})", options, function() { - return options.template.yield(['Z']); - }); + return this.yield(['Z']); + }, this); }); var t = '{{#a as |w x|}}{{w}},{{x}} {{#b as |x y|}}{{x}},{{y}}{{/b}} {{w}},{{x}} {{#c as |z|}}{{x}},{{z}}{{/c}}{{/a}}'; compilesTo(t, 'A(W,X1 B(X2,Y) W,X1 C(X1,Z))', {}); @@ -895,8 +892,8 @@ test("Block params - Helper should know how many block params it was called with test('Block params in HTML syntax', function () { registerHelper('x-bar', function(params, hash, options) { yieldTemplate("BAR({{yield}})", options, function() { - return options.template.yield(['Xerxes', 'York', 'Zed']); - }); + return this.yield(['Xerxes', 'York', 'Zed']); + }, this); }); compilesTo('{{zee}},{{y}},{{x}}', 'BAR(Zed,York,Xerxes)', {}); }); @@ -914,8 +911,8 @@ test('Block params in HTML syntax - Throws exception if given zero parameters', test('Block params in HTML syntax - Works with a single parameter', function () { - registerHelper('x-bar', function(params, hash, options) { - return options.template.yield(['Xerxes']); + registerHelper('x-bar', function() { + return this.yield(['Xerxes']); }); compilesTo('{{x}}', 'Xerxes', {}); }); @@ -930,8 +927,8 @@ test('Block params in HTML syntax - Works with other attributes', function () { test('Block params in HTML syntax - Ignores whitespace', function () { expect(3); - registerHelper('x-bar', function(params, hash, options) { - return options.template.yield(['Xerxes', 'York']); + registerHelper('x-bar', function() { + return this.yield(['Xerxes', 'York']); }); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); compilesTo('{{x}},{{y}}', 'Xerxes,York', {}); @@ -1228,7 +1225,7 @@ test("Block helper allows interior namespace", function() { registerHelper('testing', function(params, hash, options) { if (isTrue) { - return options.template.yield(); + return this.yield(); } else { return options.inverse.yield(); } @@ -1252,8 +1249,8 @@ test("Block helper allows interior namespace", function() { }); test("Block helper allows namespace to bleed through", function() { - registerHelper('testing', function(params, hash, options) { - return options.template.yield(); + registerHelper('testing', function() { + return this.yield(); }); var template = compile('
{{#testing}}{{/testing}}
'); @@ -1267,8 +1264,8 @@ test("Block helper allows namespace to bleed through", function() { }); test("Block helper with root svg allows namespace to bleed through", function() { - registerHelper('testing', function(params, hash, options) { - return options.template.yield(); + registerHelper('testing', function() { + return this.yield(); }); var template = compile('{{#testing}}{{/testing}}'); @@ -1282,8 +1279,8 @@ test("Block helper with root svg allows namespace to bleed through", function() }); test("Block helper with root foreignObject allows namespace to bleed through", function() { - registerHelper('testing', function(params, hash, options) { - return options.template.yield(); + registerHelper('testing', function() { + return this.yield(); }); var template = compile('{{#testing}}
{{/testing}}
'); diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 397bc8be..1e6b4ff4 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -58,6 +58,10 @@ function optionsFor(morph, env, scope, template, inverse) { return options; } +function thisFor(options) { + return { yield: options.template.yield }; +} + export function createScope(parentScope, localVariables) { var scope; @@ -82,7 +86,7 @@ export function block(morph, env, scope, path, params, hash, template, inverse) var options = optionsFor(morph, env, scope, template, inverse); var helper = lookupHelper(env, scope, path); - var result = helper(params, hash, options); + var result = helper.call(thisFor(options), params, hash, options); if (result === undefined && state.lastResult) { state.lastResult.revalidate(scope.self); @@ -207,7 +211,7 @@ export function component(morph, env, scope, tagName, attrs, template) { var helper = lookupHelper(env, scope, tagName); if (helper) { var options = optionsFor(morph, env, scope, template, null); - helper([], attrs, options); + helper.call(thisFor(options), [], attrs, options); } else { componentFallback(morph, env, scope, tagName, attrs, template); } From 4ee6315f42934c92b2ac46763b0ad1f54051c423 Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Thu, 12 Feb 2015 18:02:06 -0800 Subject: [PATCH 26/27] Hooks don't need to understand contextualElement The RenderNode already has a reference to its contextual element, so passing extracting it from the render node is unnecessary. --- packages/htmlbars-runtime/lib/hooks.js | 5 ++--- packages/htmlbars-runtime/lib/render.js | 8 +++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 1e6b4ff4..812bd867 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -46,7 +46,6 @@ export function wrapForHelper(template, env, originalScope, options) { function optionsFor(morph, env, scope, template, inverse) { var options = { renderNode: morph, - contextualElement: morph.contextualElement, env: env, template: null, inverse: null @@ -123,7 +122,7 @@ export function inline(morph, env, scope, path, params, hash) { export function partial(renderNode, env, scope, path) { var template = env.partials[path]; - return template.render(scope.self, env, { contextualElement: renderNode.contextualElement }).fragment; + return template.render(scope.self, env, {}).fragment; } export function content(morph, env, scope, path) { @@ -235,7 +234,7 @@ function componentFallback(morph, env, scope, tagName, attrs, template) { for (var name in attrs) { element.setAttribute(name, attrs[name]); } - var fragment = render(template, env, scope, { contextualElement: morph.contextualElement }).fragment; + var fragment = render(template, env, scope, {}).fragment; element.appendChild(fragment); morph.setNode(element); } diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index ae9a0de4..dd805053 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -3,7 +3,13 @@ import ExpressionVisitor from "./expression-visitor"; export default function render(template, env, scope, options, blockArguments) { var dom = env.dom; - var contextualElement = options && options.contextualElement; + var contextualElement; + + if (options && options.renderNode) { + contextualElement = options.renderNode.contextualElement; + } else if (options && options.contextualElement) { + contextualElement = options.contextualElement; + } dom.detectNamespace(contextualElement); From e836007c8cc4da4f5d95e5a537a5f740716d222c Mon Sep 17 00:00:00 2001 From: Tom Dale and Yehuda Katz Date: Thu, 12 Feb 2015 18:02:41 -0800 Subject: [PATCH 27/27] Start writing host hook documentation --- packages/htmlbars-runtime/lib/hooks.js | 307 +++++++++++++++++++++++++ 1 file changed, 307 insertions(+) diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 812bd867..1a476847 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -1,5 +1,77 @@ import render from "./render"; +/** + HTMLBars delegates the runtime behavior of a template to + hooks provided by the host environment. These hooks explain + the lexical environment of a Handlebars template, the internal + representation of references, and the interaction between an + HTMLBars template and the DOM it is managing. + + While HTMLBars host hooks have access to all of this internal + machinery, templates and helpers have access to the abstraction + provided by the host hooks. + + ## The Lexical Environment + + The default lexical environment of an HTMLBars template includes: + + * Any local variables, provided by *block arguments* + * The current value of `self` + + ## Simple Nesting + + Let's look at a simple template with a nested block: + + ```hbs +

{{title}}

+ + {{#if author}} + + {{/if}} + ``` + + In this case, the lexical environment at the top-level of the + template does not change inside of the `if` block. This is + achieved via an implementation of `if` that looks like this: + + ```js + registerHelper('if', function(params) { + if (!!params[0]) { + return this.yield(); + } + }); + ``` + + A call to `this.yield` invokes the child template using the + current lexical environment. + + ## Block Arguments + + It is possible for nested blocks to introduce new local + variables: + + ```hbs + {{#count-calls as |i|}} +

{{title}}

+

Called {{i}} times

+ {{/count}} + ``` + + In this example, the child block inherits its surrounding + lexical environment, but augments it with a single new + variable binding. + + The implementation of `count-calls` supplies the value of + `i`, but does not otherwise alter the environment: + + ```js + var count = 0; + registerHelper('count-calls', function() { + return this.yield([ ++count ]); + }); + ``` +*/ + export function wrap(template) { if (template === null) { return null; } @@ -61,6 +133,32 @@ function thisFor(options) { return { yield: options.template.yield }; } +/** + Host Hook: createScope + + @param {Scope?} parentScope + @param {Array} localVariables + @return Scope + + Corresponds to entering a new HTMLBars block. + + This hook is invoked when a block is entered with + a new `self` or additional local variables. + + When invoked for a top-level template, the + `parentScope` is `null`, and this hook should return + a fresh Scope. + + When invoked for a child template, the `parentScope` + is the scope for the parent environment, and + `localVariables` is an array of names of new variable + bindings that should be created for this scope. + + Note that the `Scope` is an opaque value that is + passed to other host hooks. For example, the `get` + hook uses the scope to retrieve a value for a given + scope and variable name. +*/ export function createScope(parentScope, localVariables) { var scope; @@ -78,6 +176,54 @@ export function createScope(parentScope, localVariables) { return scope; } +/** + Host Hook: block + + @param {RenderNode} renderNode + @param {Environment} env + @param {Scope} scope + @param {String} path + @param {Array} params + @param {Object} hash + @param {Block} block + @param {Block} elseBlock + + Corresponds to: + + ```hbs + {{#helper param1 param2 key1=val1 key2=val2}} + {{!-- child template --}} + {{/helper}} + ``` + + This host hook is a workhorse of the system. It is invoked + whenever a block is encountered, and is responsible for + resolving the helper to call, and then invoke it. + + The helper should be invoked with: + + - `{Array} params`: the parameters passed to the helper + in the template. + - `{Object} hash`: an object containing the keys and values passed + in the hash position in the template. + + The values in `params` and `hash` will already be resolved + through a previous call to the `get` host hook. + + The helper should be invoked with a `this` value that is + an object with one field: + + `{Function} yield`: when invoked, this function executes the + block with the current scope. It takes an optional array of + block parameters. If block parameters are supplied, HTMLBars + will invoke the `bindLocal` host hook to bind the supplied + values to the block arguments provided by the template. + + In general, the default implementation of `block` should work + for most host environments. It delegates to other host hooks + where appropriate, and properly invokes the helper with the + appropriate arguments. +*/ export function block(morph, env, scope, path, params, hash, template, inverse) { var state = morph.state; @@ -99,6 +245,44 @@ export function block(morph, env, scope, path, params, hash, template, inverse) morph.isDirty = false; } +/** + Host Hook: inline + + @param {RenderNode} renderNode + @param {Environment} env + @param {Scope} scope + @param {String} path + @param {Array} params + @param {Hash} hash + + Corresponds to: + + ```hbs + {{helper param1 param2 key1=val1 key2=val2}} + ``` + + This host hook is similar to the `block` host hook, but it + invokes helpers that do not supply an attached block. + + Like the `block` hook, the helper should be invoked with: + + - `{Array} params`: the parameters passed to the helper + in the template. + - `{Object} hash`: an object containing the keys and values passed + in the hash position in the template. + + The values in `params` and `hash` will already be resolved + through a previous call to the `get` host hook. + + In general, the default implementation of `inline` should work + for most host environments. It delegates to other host hooks + where appropriate, and properly invokes the helper with the + appropriate arguments. + + The default implementation of `inline` also makes `partial` + a keyword. Instead of invoking a helper named `partial`, + it invokes the `partial` host hook. +*/ export function inline(morph, env, scope, path, params, hash) { if (morph.isDirty) { var state = morph.state; @@ -120,11 +304,59 @@ export function inline(morph, env, scope, path, params, hash) { } } +/** + Host Hook: partial + + @param {RenderNode} renderNode + @param {Environment} env + @param {Scope} scope + @param {String} path + + Corresponds to: + + ```hbs + {{partial "location"}} + ``` + + This host hook is invoked by the default implementation of + the `inline` hook. This makes `partial` a keyword in an + HTMLBars environment using the default `inline` host hook. + + It is implemented as a host hook so that it can retrieve + the named partial out of the `Environment`. Helpers, in + contrast, only have access to the values passed in to them, + and not to the ambient lexical environment. + + The host hook should invoke the referenced partial with + the ambient `self`. +*/ export function partial(renderNode, env, scope, path) { var template = env.partials[path]; return template.render(scope.self, env, {}).fragment; } +/** + Host hook: content + + @param {RenderNode} renderNode + @param {Environment} env + @param {Scope} scope + @param {String} path + + Corresponds to: + + ```hbs + {{content}} + ``` + + This hook is responsible for updating a render node + that represents an area of text with a value. + + Ideally, this hook would be refactored so it did not + combine both the responsibility for identifying whether + the path represented a helper as well as updating the + render node. +*/ export function content(morph, env, scope, path) { if (morph.isDirty) { var state = morph.state; @@ -146,6 +378,33 @@ export function content(morph, env, scope, path) { } } +/** + Host hook: element + + @param {RenderNode} renderNode + @param {Environment} env + @param {Scope} scope + @param {String} path + @param {Array} params + @param {Hash} hash + + Corresponds to: + + ```hbs +
+ ``` + + This hook is responsible for invoking a helper that + modifies an element. + + Its purpose is largely legacy support for awkward + idioms that became common when using the string-based + Handlebars engine. + + Most of the uses of the `element` hook are expected + to be superseded by component syntax and the + `attribute` hook. +*/ export function element(morph, env, scope, path, params, hash) { if (morph.isDirty) { var helper = lookupHelper(env, scope, path); @@ -157,6 +416,27 @@ export function element(morph, env, scope, path, params, hash) { } } +/** + Host hook: attribute + + @param {RenderNode} renderNode + @param {Environment} env + @param {String} name + @param {any} value + + Corresponds to: + + ```hbs +
+ ``` + + This hook is responsible for updating a render node + that represents an element's attribute with a value. + + It receives the name of the attribute as well as an + already-resolved value, and should update the render + node with the value if appropriate. +*/ export function attribute(morph, env, name, value) { if (morph.isDirty) { var state = morph.state; @@ -181,6 +461,33 @@ export function subexpr(morph, env, scope, helperName, params, hash) { } } +/** + Host Hook: get + + @param {RenderNode} renderNode + @param {Environment} env + @param {Scope} scope + @param {String} path + + Corresponds to: + + ```hbs + {{foo.bar}} + ^ + + {{helper foo.bar key=value}} + ^ ^ + ``` + + This hook is the "leaf" hook of the system. It is used to + resolve a path relative to the current scope. + + NOTE: This should be refactored into three hooks: splitting + the path into parts, looking up the first part on the scope, + and resolving the remainder a piece at a time. It would also + be useful to have a "classification" hook that handles + classifying a name as either a helper or value. +*/ export function get(morph, env, scope, path) { if (!morph.isDirty) { return; }