From 63171f0e17b241793aa360132a21faa918007bbd Mon Sep 17 00:00:00 2001 From: Godhuda Date: Mon, 17 Aug 2015 17:19:01 -0700 Subject: [PATCH 1/6] WIP rehydration --- .../htmlbars-compiler/tests/dirtying-test.js | 114 +++++++++++++- packages/htmlbars-runtime/lib/hooks.js | 25 +++- packages/htmlbars-runtime/lib/render.js | 140 +++++++++++++++++- 3 files changed, 269 insertions(+), 10 deletions(-) diff --git a/packages/htmlbars-compiler/tests/dirtying-test.js b/packages/htmlbars-compiler/tests/dirtying-test.js index b1550603..b9d91600 100644 --- a/packages/htmlbars-compiler/tests/dirtying-test.js +++ b/packages/htmlbars-compiler/tests/dirtying-test.js @@ -1,5 +1,5 @@ import { compile } from "../htmlbars-compiler/compiler"; -import { manualElement } from "../htmlbars-runtime/render"; +import { RenderResult, LastYielded, manualElement } from "../htmlbars-runtime/render"; import { hostBlock } from "../htmlbars-runtime/hooks"; import render from "../htmlbars-runtime/render"; import { blockFor } from "../htmlbars-util/template-utils"; @@ -7,6 +7,7 @@ import defaultHooks from "../htmlbars-runtime/hooks"; import { merge } from "../htmlbars-util/object-utils"; import DOMHelper from "../dom-helper"; import { equalTokens } from "../htmlbars-test-helpers"; +import { rehydrateNode, serializeNode } from "../htmlbars-runtime/render"; var hooks, helpers, partials, env; @@ -730,3 +731,114 @@ test("The invoke helper hook can instruct the runtime to link the result", funct equalTokens(result.fragment, "24"); equal(invokeCount, 1); }); + +test("it is possible to synthesize a simple fragment and render node and pass it to the rendering process", function() { + let template = compile("

{{name}}

"); + + let p = env.dom.createElement('p'); + env.dom.appendText(p, 'Godfrey'); + + let rootMorph = env.dom.createMorph(null, p, p); + let childMorph = env.dom.createMorphAt(p, 0, 0); + + rootMorph.childNodes = [childMorph]; + + let scope = env.hooks.createFreshScope(); + let obj = { name: "Yehuda" }; + let result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: rootMorph, self: obj }); + + let original = p.firstChild; + result.render(); + + strictEqual(p.firstChild, original, "The text node remained stable"); + equalTokens(p, "

Yehuda

"); +}); + +test("it is possible to rehydrate a template with blocks", function() { + let template = compile("

{{#if bool}}{{name}}{{/if}}

"); + + let p = env.dom.createElement('p'); + let span = env.dom.appendChild(p, env.dom.createElement('span')); + env.dom.appendText(span, 'Godfrey'); + + let rootMorph = env.dom.createMorph(null, p, p); + let childMorph1 = env.dom.createMorphAt(p, 0, 0); + let childMorph2 = env.dom.createMorphAt(span, 0, 0); + + let obj = { bool: true, name: "Yehuda" }; + childMorph1.lastYielded = new LastYielded(obj, template.raw.templates[0], null); + + rootMorph.childNodes = [childMorph1]; + childMorph1.childNodes = [childMorph2]; + + let scope = env.hooks.createFreshScope(); + let result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: rootMorph, self: obj }); + + let childResult = RenderResult.rehydrate(env, scope, template.raw.templates[0], { renderNode: childMorph1, self: obj }); + childMorph1.lastResult = childResult; + + let original = p.firstChild; + result.render(); + + strictEqual(p.firstChild, original, "The text node remained stable"); + equalTokens(p, "

Yehuda

"); +}); + +test("it is possible to serialize a render node tree", function() { + let template = compile('

{{name}}

'); + let obj = { title: 'chancancode', name: 'Godfrey' }; + let result = template.render(obj, env); + + equalTokens(result.fragment, '

Godfrey

'); + + let original = result.fragment.firstChild; + let newRoot = env.dom.createMorph(null, original, original); + newRoot.ownerNode = newRoot; + let node = rehydrateNode(serializeNode(env, result.root), newRoot); + + let scope = env.hooks.createFreshScope(); + + result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: node, self: obj }); + + result.render(); + + strictEqual(result.root.firstNode, original); + equalTokens(result.root.firstNode, '

Godfrey

'); +}); + +test("it is possible to serialize a render node tree with recursive templates", function() { + env.hooks.rehydrateLastYielded = function(env, morph) { + morph.lastYielded.template = morph.lastYielded.templateId; + }; + + env.hooks.serializeLastYielded = function(env, morph) { + return morph.lastYielded.template; + }; + + let template = compile('

{{#if bool}}{{name}}{{/if}}

'); + let obj = { title: 'chancancode', name: 'Godfrey', bool: true }; + let result = template.render(obj, env); + + equalTokens(result.fragment, '

Godfrey

'); + + let original = result.fragment.firstChild; + let span = original.firstChild; + let godfrey = span.firstChild; + + let newRoot = env.dom.createMorph(null, original, original); + newRoot.ownerNode = newRoot; + let node = rehydrateNode(serializeNode(env, result.root), newRoot); + + let scope = env.hooks.createFreshScope(); + + result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: node, self: obj }); + + result.render(); + + let newNode = result.root.firstNode; + strictEqual(newNode, original, "the

is the same"); + strictEqual(newNode.firstChild, span, "the is the same"); + strictEqual(newNode.firstChild.firstChild, godfrey, "the text node is the same"); + + equalTokens(newNode, '

Godfrey

'); +}); diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index b137607d..62c82203 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -1,6 +1,6 @@ import render from "./render"; import MorphList from "../morph-range/morph-list"; -import { createChildMorph } from "./render"; +import { createChildMorph, RenderResult, LastYielded } from "./render"; import { keyLength, shallowCopy } from "../htmlbars-util/object-utils"; import { validateChildMorphs } from "../htmlbars-util/morph-utils"; import { RenderState, clearMorph, clearMorphList, renderAndCleanup } from "../htmlbars-util/template-utils"; @@ -145,10 +145,11 @@ function yieldTemplate(template, env, parentScope, morph, renderState, visitor) var scope = parentScope; - if (morph.lastYielded && isStableTemplate(template, morph.lastYielded)) { + if (morph.lastYielded && morph.lastYielded.isStableTemplate(template)) { return morph.lastResult.revalidateWith(env, undefined, self, blockArguments, visitor); } + // Check to make sure that we actually **need** a new scope, and can't // share the parent scope. Note that we need to move this check into // a host hook, because the host's notion of scope may require a new @@ -157,7 +158,17 @@ function yieldTemplate(template, env, parentScope, morph, renderState, visitor) scope = env.hooks.createChildScope(parentScope); } - morph.lastYielded = { self: self, template: template, shadowTemplate: null }; + if (morph.lastYielded && morph.lastYielded.templateId) { + env.hooks.rehydrateLastYielded(env, morph); + + if (morph.lastYielded.isStableTemplate(template)) { + let renderResult = RenderResult.rehydrate(env, scope, template, { renderNode: morph, self, blockArguments }); + renderResult.render(); + return; + } + } + + morph.lastYielded = new LastYielded(self, template, null); // Render the template that was selected by the helper render(template, env, scope, { renderNode: morph, self: self, blockArguments: blockArguments }); @@ -275,10 +286,6 @@ function yieldItem(template, env, parentScope, morph, renderState, visitor) { }; } -function isStableTemplate(template, lastYielded) { - return !lastYielded.shadowTemplate && template === lastYielded.template; -} - function yieldInShadowTemplate(template, env, parentScope, morph, renderState, visitor) { var hostYield = hostYieldWithShadowTemplate(template, env, parentScope, morph, renderState, visitor); @@ -300,7 +307,7 @@ export function hostYieldWithShadowTemplate(template, env, parentScope, morph, r blockToYield.arity = template.arity; env.hooks.bindBlock(env, shadowScope, blockToYield); - morph.lastYielded = { self: self, template: template, shadowTemplate: shadowTemplate }; + morph.lastYielded = new LastYielded(self, template, shadowTemplate); // Render the shadow template with the block available render(shadowTemplate.raw, env, shadowScope, { renderNode: morph, self: self, blockArguments: blockArguments }); @@ -1094,6 +1101,8 @@ export default { linkRenderNode: linkRenderNode, partial: partial, subexpr: subexpr, + rehydrateLastYielded: null, + serializeLastYielded: null, // fundamental hooks with good default behavior bindBlock: bindBlock, diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index 798c7998..bacf234d 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -28,7 +28,7 @@ export default function render(template, env, scope, options) { return renderResult; } -function RenderResult(env, scope, options, rootNode, ownerNode, nodes, fragment, template, shouldSetContent) { +export function RenderResult(env, scope, options, rootNode, ownerNode, nodes, fragment, template, shouldSetContent) { this.root = rootNode; this.fragment = fragment; @@ -59,6 +59,7 @@ RenderResult.build = function(env, scope, template, options, contextualElement) ownerNode = rootNode.ownerNode; shouldSetContent = true; } else { + // creating the root render node rootNode = dom.createMorph(null, fragment.firstChild, fragment.lastChild, contextualElement); ownerNode = rootNode; initializeNode(rootNode, ownerNode); @@ -75,6 +76,14 @@ RenderResult.build = function(env, scope, template, options, contextualElement) return new RenderResult(env, scope, options, rootNode, ownerNode, nodes, fragment, template, shouldSetContent); }; +RenderResult.rehydrate = function(env, scope, template, options) { + let rootNode = options.renderNode; + let ownerNode = rootNode.ownerNode; + let shouldSetContent = false; + + return new RenderResult(env, scope, options, rootNode, ownerNode, rootNode.childNodes, null, template, shouldSetContent); +}; + export function manualElement(tagName, attributes, _isEmpty) { var statements = []; @@ -318,3 +327,132 @@ export function getCachedFragment(template, env) { return fragment; } + +export function rehydrateNode(serializedNodes, renderNode) { + let dom = renderNode.domHelper; + let context = renderNode.firstNode.parentNode; + let cache = Object.create(null); + + renderNode.childNodes = serializedNodes.map(childNode => _rehydrateNode(renderNode, childNode, dom, context, cache)); + return renderNode; +} + +function _rehydrateNode(owner, renderNode, dom, context, cache) { + let element, node; + + switch (renderNode.type) { + case 'attr': + element = elementFromId(dom, context, renderNode.element, cache); + node = dom.createAttrMorph(element, renderNode.attrName); + break; + case 'range': + element = elementFromId(dom, context, renderNode.parentNode, cache); + node = dom.createMorphAt(element, renderNode.firstNode, renderNode.lastNode); + node.lastYielded = LastYielded.fromTemplateId(renderNode.templateId); + node.childNodes = renderNode.childNodes && renderNode.childNodes.map(childNode => _rehydrateNode(node, childNode, dom, context, cache)); + break; + } + + initializeNode(node, owner); + return node; +} + +function elementFromId(dom, context, id, cache) { + if (id in cache) { + return cache[id]; + } + + let element = context.querySelector(`[data-hbs-node="${id}"]`); + dom.removeAttribute(element, 'data-hbs-node'); + cache[id] = element; + return element; +} + +export function serializeNode(env, renderNode) { + let serializationContext = { id: 0 }; + + return renderNode.childNodes.map(childNode => _serializeNode(env, childNode, serializationContext)); + + //return [{ + //type: 'attr', + //element: "0", + //attrName: 'title' + //}, { + //type: 'range', + //parentNode: "0", + //firstNode: 0, + //lastNode: 0 + //}]; +} + +function _serializeNode(env, renderNode, serializationContext) { + let dom = env.dom; + if (renderNode instanceof dom.MorphClass) { + let parent = renderNode.firstNode.parentNode; + let { firstNode, lastNode } = parentOffsets(dom, parent, renderNode); + + return { + type: 'range', + childNodes: renderNode.childNodes && renderNode.childNodes.map(childNode => _serializeNode(env, childNode, serializationContext)), + parentNode: idFromElement(dom, parent, serializationContext), + templateId: renderNode.lastYielded && env.hooks.serializeLastYielded(env, renderNode), + firstNode, + lastNode + }; + } else if (renderNode instanceof dom.AttrMorphClass) { + return { + type: 'attr', + element: idFromElement(dom, renderNode.element, serializationContext), + attrName: renderNode.attrName + }; + } +} + +function parentOffsets(dom, parent, renderNode) { + let current = parent.firstChild; + let firstNeedle = renderNode.firstNode; + let lastNeedle = renderNode.lastNode; + let firstNode, lastNode; + + while (current !== firstNeedle) { + current = current.nextSibling; + } + + firstNode = current; + + while (current !== lastNeedle) { + current = current.nextSibling; + } + + lastNode = current; + + return { firstNode, lastNode }; +} + +function idFromElement(dom, element, serializationContext) { + let id = dom.getAttribute(element, 'data-hbs-node'); + + if (id) { + return id; + } + + id = (serializationContext.id++) + ''; + dom.setAttribute(element, 'data-hbs-node', id); + return id; +} + +export function LastYielded(self, template, shadowTemplate, templateId) { + this.self = self; + this.template = template; + this.shadowTemplate = shadowTemplate; + this.templateId = templateId; +} + +LastYielded.fromTemplateId = function(templateId) { + return new LastYielded(null, null, null, templateId); +}; + +LastYielded.prototype.isStableTemplate = function(nextTemplate) { + return !this.shadowTemplate && nextTemplate === this.template; +}; + From 7d4593de98de668e162f66c2ed1faf7f9215424e Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 18 Aug 2015 15:21:34 -0700 Subject: [PATCH 2/6] Give each template a serializable unique id --- .../lib/template-compiler.js | 3 +- packages/htmlbars-compiler/lib/utils.js | 30 +++++++++++++++++++ .../htmlbars-compiler/tests/compile-tests.js | 12 ++++++++ .../htmlbars-compiler/tests/utils-test.js | 13 ++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/htmlbars-compiler/tests/utils-test.js diff --git a/packages/htmlbars-compiler/lib/template-compiler.js b/packages/htmlbars-compiler/lib/template-compiler.js index fa2a3ab7..3001b858 100644 --- a/packages/htmlbars-compiler/lib/template-compiler.js +++ b/packages/htmlbars-compiler/lib/template-compiler.js @@ -3,7 +3,7 @@ import FragmentJavaScriptCompiler from './fragment-javascript-compiler'; import HydrationOpcodeCompiler from './hydration-opcode-compiler'; import HydrationJavaScriptCompiler from './hydration-javascript-compiler'; import TemplateVisitor from "./template-visitor"; -import { processOpcodes } from "./utils"; +import { processOpcodes, generateId } from "./utils"; import { repeat } from "../htmlbars-util/quoting"; import { map } from "../htmlbars-util/array-utils"; @@ -110,6 +110,7 @@ TemplateCompiler.prototype.endProgram = function(program, programDepth) { indent+' return {\n' + this.buildMeta(indent+' ', program) + indent+' isEmpty: ' + (program.body.length ? 'false' : 'true') + ',\n' + + indent+' id: "' + generateId() + '",\n' + indent+' arity: ' + blockParams.length + ',\n' + indent+' cachedFragment: null,\n' + indent+' hasRendered: false,\n' + diff --git a/packages/htmlbars-compiler/lib/utils.js b/packages/htmlbars-compiler/lib/utils.js index 9206f666..b4827b84 100644 --- a/packages/htmlbars-compiler/lib/utils.js +++ b/packages/htmlbars-compiler/lib/utils.js @@ -1,3 +1,6 @@ +/*globals window:false*/ +/*globals Uint8Array:false*/ + export function processOpcodes(compiler, opcodes) { for (var i=0, l=opcodes.length; i i % 64); + return [].slice.call(buf).map(i => lookup.charAt(i)).join(''); + }; +} else { + generateId = function() { + let buf = []; + + for (let i=0; i<12; i++) { + buf.push(lookup.charAt(Math.floor(Math.random() * 64))); + } + + return buf.join(''); + }; +} + +// generateId() returns a unique 12-character string consisting of random +// base64 characters. +export { generateId }; diff --git a/packages/htmlbars-compiler/tests/compile-tests.js b/packages/htmlbars-compiler/tests/compile-tests.js index 5bec9eba..2fe54f44 100644 --- a/packages/htmlbars-compiler/tests/compile-tests.js +++ b/packages/htmlbars-compiler/tests/compile-tests.js @@ -47,3 +47,15 @@ test('options are not required for `compile`', function () { ok(template.meta, 'meta is present in template, even if empty'); }); + +test('templates get unique ids', function() { + var template1 = compile('{{#if foo}}hello{{/if}}'); + + ok(typeof template1.raw.id === 'string', 'the top-level template has an id'); + ok(typeof template1.raw.templates[0].id === 'string', 'nested templates have ids'); + + var template2 = compile('Another template'); + ok(typeof template2.raw.id === 'string', 'the top-level template has an id'); + + notEqual(template1.raw.id, template2.raw.id, 'different templates should have different ids'); +}); diff --git a/packages/htmlbars-compiler/tests/utils-test.js b/packages/htmlbars-compiler/tests/utils-test.js new file mode 100644 index 00000000..7567897c --- /dev/null +++ b/packages/htmlbars-compiler/tests/utils-test.js @@ -0,0 +1,13 @@ +import { generateId } from '../htmlbars-compiler/utils'; + +QUnit.module('generating a template ID'); + +QUnit.test('generates a different ID every time', function() { + let seen = Object.create(null); + + for (let i=0; i<1000; i++) { + seen[generateId()] = true; + } + + equal(Object.keys(seen).length, 1000, '1000 different ids were generated'); +}); From 0305e4a2d0344419f4d866c59cbcc8901e28bc19 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 18 Aug 2015 16:31:31 -0700 Subject: [PATCH 3/6] Remove yieldIn and shadow template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This code path predates the existence of `blockFor`, which is a more general-purpose and flexible mechanism for implementing Ember-style “layouts” that `{{yield}}` to the block provided to a component. --- .../htmlbars-compiler/tests/dirtying-test.js | 118 +----------------- .../htmlbars-compiler/tests/hooks-test.js | 2 - .../tests/html-compiler-test.js | 4 +- packages/htmlbars-runtime/lib/hooks.js | 55 +------- packages/htmlbars-runtime/tests/main-test.js | 2 +- 5 files changed, 5 insertions(+), 176 deletions(-) diff --git a/packages/htmlbars-compiler/tests/dirtying-test.js b/packages/htmlbars-compiler/tests/dirtying-test.js index b9d91600..f5c710d6 100644 --- a/packages/htmlbars-compiler/tests/dirtying-test.js +++ b/packages/htmlbars-compiler/tests/dirtying-test.js @@ -105,122 +105,6 @@ test("a simple implementation of a dirtying rerender without inverse", function( equalTokens(result.fragment, '

hello world

', "If the condition is false, the morph becomes empty"); }); -test("a dirtying rerender using `yieldIn`", function() { - var component = compile("

{{yield}}

"); - var template = compile("
{{title}}
"); - - registerHelper("simple-component", function() { - return this.yieldIn(component); - }); - - var object = { title: "Hello world" }; - var result = template.render(object, env); - - var valueNode = getValueNode(); - equalTokens(result.fragment, '

Hello world

'); - - result.rerender(); - - equalTokens(result.fragment, '

Hello world

'); - strictEqual(getValueNode(), valueNode); - - object.title = "Goodbye world"; - - result.rerender(); - equalTokens(result.fragment, '

Goodbye world

'); - strictEqual(getValueNode(), valueNode); - - function getValueNode() { - return result.fragment.firstChild.firstChild.firstChild; - } -}); - -test("a dirtying rerender using `yieldIn` and self", function() { - var component = compile("

{{attrs.name}}{{yield}}

"); - var template = compile("
{{title}}
"); - - registerHelper("simple-component", function(params, hash) { - return this.yieldIn(component, { attrs: hash }); - }); - - var object = { title: "Hello world" }; - var result = template.render(object, env); - - var nameNode = getNameNode(); - var titleNode = getTitleNode(); - equalTokens(result.fragment, '

Yo! Hello world

'); - - rerender(); - equalTokens(result.fragment, '

Yo! Hello world

'); - assertStableNodes(); - - object.title = "Goodbye world"; - - rerender(); - equalTokens(result.fragment, '

Yo! Goodbye world

'); - assertStableNodes(); - - function rerender() { - result.rerender(); - } - - function assertStableNodes() { - strictEqual(getNameNode(), nameNode); - strictEqual(getTitleNode(), titleNode); - } - - function getNameNode() { - return result.fragment.firstChild.firstChild.firstChild.firstChild; - } - - function getTitleNode() { - return result.fragment.firstChild.firstChild.firstChild.nextSibling; - } -}); - -test("a dirtying rerender using `yieldIn`, self and block args", function() { - var component = compile("

{{yield attrs.name}}

"); - var template = compile("
{{key}}{{title}}
"); - - registerHelper("simple-component", function(params, hash) { - return this.yieldIn(component, { attrs: hash }); - }); - - var object = { title: "Hello world" }; - var result = template.render(object, env); - - var nameNode = getNameNode(); - var titleNode = getTitleNode(); - equalTokens(result.fragment, '

Yo! Hello world

'); - - rerender(); - equalTokens(result.fragment, '

Yo! Hello world

'); - assertStableNodes(); - - object.title = "Goodbye world"; - - rerender(); - equalTokens(result.fragment, '

Yo! Goodbye world

'); - assertStableNodes(); - - function rerender() { - result.rerender(); - } - - function assertStableNodes() { - strictEqual(getNameNode(), nameNode); - strictEqual(getTitleNode(), titleNode); - } - - function getNameNode() { - return result.fragment.firstChild.firstChild.firstChild.firstChild; - } - - function getTitleNode() { - return result.fragment.firstChild.firstChild.firstChild.nextSibling; - } -}); - test("block helpers whose template has a morph at the edge", function() { registerHelper('id', function(params, hash, options) { return options.template.yield(); @@ -645,7 +529,7 @@ QUnit.module("Manual elements", { beforeEach: commonSetup }); -test("Setting up a manual element renders and revalidates", function() { +QUnit.skip("Setting up a manual element renders and revalidates", function() { hooks.keywords['manual-element'] = { render: function(morph, env, scope, params, hash, template, inverse, visitor) { var attributes = { diff --git a/packages/htmlbars-compiler/tests/hooks-test.js b/packages/htmlbars-compiler/tests/hooks-test.js index f18be1c1..234db417 100644 --- a/packages/htmlbars-compiler/tests/hooks-test.js +++ b/packages/htmlbars-compiler/tests/hooks-test.js @@ -37,9 +37,7 @@ test("the invokeHelper hook gets invoked to call helpers", function() { var invoked = false; hooks.invokeHelper = function(morph, env, scope, visitor, params, hash, helper, templates, context) { invoked = true; - deepEqual(params, [{ value: "hello world" }]); - ok(templates.template.yieldIn, "templates are passed"); ok(scope.self, "the scope was passed"); ok(morph.state, "the morph was passed"); diff --git a/packages/htmlbars-compiler/tests/html-compiler-test.js b/packages/htmlbars-compiler/tests/html-compiler-test.js index 6ec3f359..6e6074a5 100644 --- a/packages/htmlbars-compiler/tests/html-compiler-test.js +++ b/packages/htmlbars-compiler/tests/html-compiler-test.js @@ -719,7 +719,7 @@ test("Simple elements can have dashed attributes", function() { equalTokens(fragment, '
content
'); }); -test("Block params", function() { +QUnit.skip("Block params", function() { registerHelper('a', function() { this.yieldIn(compile("A({{yield 'W' 'X1'}})")); }); @@ -746,7 +746,7 @@ test("Block params - Helper should know how many block params it was called with 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 () { +QUnit.skip('Block params in HTML syntax', function () { var layout = compile("BAR({{yield 'Xerxes' 'York' 'Zed'}})"); registerHelper('x-bar', function() { diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 62c82203..4d5084e4 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -98,11 +98,7 @@ export function wrap(template) { } export function wrapForHelper(template, env, scope, morph, renderState, visitor) { - if (!template) { - return { - yieldIn: yieldInShadowTemplate(null, env, scope, morph, renderState, visitor) - }; - } + if (!template) { return {}; } var yieldArgs = yieldTemplate(template, env, scope, morph, renderState, visitor); @@ -111,7 +107,6 @@ export function wrapForHelper(template, env, scope, morph, renderState, visitor) arity: template.arity, yield: yieldArgs, yieldItem: yieldItem(template, env, scope, morph, renderState, visitor), - yieldIn: yieldInShadowTemplate(template, env, scope, morph, renderState, visitor), raw: template, render: function(self, blockArguments) { @@ -286,54 +281,6 @@ function yieldItem(template, env, parentScope, morph, renderState, visitor) { }; } -function yieldInShadowTemplate(template, env, parentScope, morph, renderState, visitor) { - var hostYield = hostYieldWithShadowTemplate(template, env, parentScope, morph, renderState, visitor); - - return function(shadowTemplate, self) { - hostYield(shadowTemplate, env, self, []); - }; -} - -export function hostYieldWithShadowTemplate(template, env, parentScope, morph, renderState, visitor) { - return function(shadowTemplate, env, self, blockArguments) { - renderState.morphToClear = null; - - if (morph.lastYielded && isStableShadowRoot(template, shadowTemplate, morph.lastYielded)) { - return morph.lastResult.revalidateWith(env, undefined, self, blockArguments, visitor); - } - - var shadowScope = env.hooks.createFreshScope(); - env.hooks.bindShadowScope(env, parentScope, shadowScope, renderState.shadowOptions); - blockToYield.arity = template.arity; - env.hooks.bindBlock(env, shadowScope, blockToYield); - - morph.lastYielded = new LastYielded(self, template, shadowTemplate); - - // Render the shadow template with the block available - render(shadowTemplate.raw, env, shadowScope, { renderNode: morph, self: self, blockArguments: blockArguments }); - }; - - function blockToYield(env, blockArguments, self, renderNode, shadowParent, visitor) { - if (renderNode.lastResult) { - renderNode.lastResult.revalidateWith(env, undefined, undefined, blockArguments, visitor); - } else { - var scope = parentScope; - - // Since a yielded template shares a `self` with its original context, - // we only need to create a new scope if the template has block parameters - if (template.arity) { - scope = env.hooks.createChildScope(parentScope); - } - - render(template, env, scope, { renderNode: renderNode, self: self, blockArguments: blockArguments }); - } - } -} - -function isStableShadowRoot(template, shadowTemplate, lastYielded) { - return template === lastYielded.template && shadowTemplate === lastYielded.shadowTemplate; -} - function optionsFor(template, inverse, env, scope, morph, visitor) { // If there was a template yielded last time, set morphToClear so it will be cleared // if no template is yielded on this render. diff --git a/packages/htmlbars-runtime/tests/main-test.js b/packages/htmlbars-runtime/tests/main-test.js index 70dfa6d2..f7cfe304 100644 --- a/packages/htmlbars-runtime/tests/main-test.js +++ b/packages/htmlbars-runtime/tests/main-test.js @@ -20,7 +20,7 @@ QUnit.module("htmlbars-runtime", { } }); -test("manualElement function honors namespaces", function() { +QUnit.skip("manualElement function honors namespaces", function() { hooks.keywords['manual-element'] = { render: function(morph, env, scope, params, hash, template, inverse, visitor) { var attributes = { From db6462fc7a318d2a5dcae19b4949c38bbda2bd8b Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 18 Aug 2015 16:33:47 -0700 Subject: [PATCH 4/6] Use the templateId instead of template identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to use a unified code path between rehydration and normal rendering. We also discovered that we don’t use `lastYielded.self` (it was an accidental holdover from the code that was refactored into lastYielded), so we removed it (along with a vestigial `shadowTemplate`, removed in a previous commit). --- .../htmlbars-compiler/tests/dirtying-test.js | 10 +-------- packages/htmlbars-runtime/lib/hooks.js | 22 +++++++------------ packages/htmlbars-runtime/lib/render.js | 15 ++++--------- 3 files changed, 13 insertions(+), 34 deletions(-) diff --git a/packages/htmlbars-compiler/tests/dirtying-test.js b/packages/htmlbars-compiler/tests/dirtying-test.js index f5c710d6..539c9699 100644 --- a/packages/htmlbars-compiler/tests/dirtying-test.js +++ b/packages/htmlbars-compiler/tests/dirtying-test.js @@ -650,7 +650,7 @@ test("it is possible to rehydrate a template with blocks", function() { let childMorph2 = env.dom.createMorphAt(span, 0, 0); let obj = { bool: true, name: "Yehuda" }; - childMorph1.lastYielded = new LastYielded(obj, template.raw.templates[0], null); + childMorph1.lastYielded = new LastYielded(template.raw.templates[0].id); rootMorph.childNodes = [childMorph1]; childMorph1.childNodes = [childMorph2]; @@ -691,14 +691,6 @@ test("it is possible to serialize a render node tree", function() { }); test("it is possible to serialize a render node tree with recursive templates", function() { - env.hooks.rehydrateLastYielded = function(env, morph) { - morph.lastYielded.template = morph.lastYielded.templateId; - }; - - env.hooks.serializeLastYielded = function(env, morph) { - return morph.lastYielded.template; - }; - let template = compile('

{{#if bool}}{{name}}{{/if}}

'); let obj = { title: 'chancancode', name: 'Godfrey', bool: true }; let result = template.render(obj, env); diff --git a/packages/htmlbars-runtime/lib/hooks.js b/packages/htmlbars-runtime/lib/hooks.js index 4d5084e4..8cc449df 100644 --- a/packages/htmlbars-runtime/lib/hooks.js +++ b/packages/htmlbars-runtime/lib/hooks.js @@ -79,7 +79,7 @@ import { linkParams } from "../htmlbars-util/morph-utils"; */ export function wrap(template) { - if (template === null) { return null; } + if (template === null) { return null; } return { meta: template.meta, @@ -140,11 +140,10 @@ function yieldTemplate(template, env, parentScope, morph, renderState, visitor) var scope = parentScope; - if (morph.lastYielded && morph.lastYielded.isStableTemplate(template)) { + if (morph.lastResult && morph.lastYielded.isStableTemplate(template)) { return morph.lastResult.revalidateWith(env, undefined, self, blockArguments, visitor); } - // Check to make sure that we actually **need** a new scope, and can't // share the parent scope. Note that we need to move this check into // a host hook, because the host's notion of scope may require a new @@ -153,17 +152,14 @@ function yieldTemplate(template, env, parentScope, morph, renderState, visitor) scope = env.hooks.createChildScope(parentScope); } - if (morph.lastYielded && morph.lastYielded.templateId) { - env.hooks.rehydrateLastYielded(env, morph); - - if (morph.lastYielded.isStableTemplate(template)) { - let renderResult = RenderResult.rehydrate(env, scope, template, { renderNode: morph, self, blockArguments }); - renderResult.render(); - return; - } + // rehydration + if (morph.lastYielded && morph.lastYielded.isStableTemplate(template)) { + let renderResult = RenderResult.rehydrate(env, scope, template, { renderNode: morph, self, blockArguments }); + renderResult.render(); + return; } - morph.lastYielded = new LastYielded(self, template, null); + morph.lastYielded = new LastYielded(template.id); // Render the template that was selected by the helper render(template, env, scope, { renderNode: morph, self: self, blockArguments: blockArguments }); @@ -1048,8 +1044,6 @@ export default { linkRenderNode: linkRenderNode, partial: partial, subexpr: subexpr, - rehydrateLastYielded: null, - serializeLastYielded: null, // fundamental hooks with good default behavior bindBlock: bindBlock, diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index bacf234d..90c0d776 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -348,7 +348,7 @@ function _rehydrateNode(owner, renderNode, dom, context, cache) { case 'range': element = elementFromId(dom, context, renderNode.parentNode, cache); node = dom.createMorphAt(element, renderNode.firstNode, renderNode.lastNode); - node.lastYielded = LastYielded.fromTemplateId(renderNode.templateId); + node.lastYielded = new LastYielded(renderNode.templateId); node.childNodes = renderNode.childNodes && renderNode.childNodes.map(childNode => _rehydrateNode(node, childNode, dom, context, cache)); break; } @@ -395,7 +395,7 @@ function _serializeNode(env, renderNode, serializationContext) { type: 'range', childNodes: renderNode.childNodes && renderNode.childNodes.map(childNode => _serializeNode(env, childNode, serializationContext)), parentNode: idFromElement(dom, parent, serializationContext), - templateId: renderNode.lastYielded && env.hooks.serializeLastYielded(env, renderNode), + templateId: renderNode.lastYielded && renderNode.lastYielded.templateId, firstNode, lastNode }; @@ -441,18 +441,11 @@ function idFromElement(dom, element, serializationContext) { return id; } -export function LastYielded(self, template, shadowTemplate, templateId) { - this.self = self; - this.template = template; - this.shadowTemplate = shadowTemplate; +export function LastYielded(templateId) { this.templateId = templateId; } -LastYielded.fromTemplateId = function(templateId) { - return new LastYielded(null, null, null, templateId); -}; - LastYielded.prototype.isStableTemplate = function(nextTemplate) { - return !this.shadowTemplate && nextTemplate === this.template; + return nextTemplate.id === this.templateId; }; From a09bed6e7e1c72952ab0b5fee4976751c88e1ef1 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Tue, 18 Aug 2015 17:40:17 -0700 Subject: [PATCH 5/6] Support adjacent text nodes --- packages/dom-helper/lib/main.js | 26 ++++++++++++++ .../htmlbars-compiler/tests/dirtying-test.js | 35 +++++++++++++++++-- packages/htmlbars-runtime/lib/render.js | 14 +++++--- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/packages/dom-helper/lib/main.js b/packages/dom-helper/lib/main.js index 7f041f90..0705a340 100644 --- a/packages/dom-helper/lib/main.js +++ b/packages/dom-helper/lib/main.js @@ -550,6 +550,32 @@ prototype.parseHTML = function(html, contextualElement) { return fragment; }; +const SHOW_TEXT = 4; + +prototype.preserveAdjacentTextNodes = function(root) { + let iterator = this.document.createNodeIterator(root, SHOW_TEXT, function(node) { + return node.previousSibling && node.previousSibling.nodeType === 3; + }); + + let node; + + /*jshint boss:true*/ + while (node = iterator.nextNode()) { + let element = this.createElement('script'); + this.setAttribute(element, 'data-hbs-split', ''); + node.parentNode.insertBefore(element, node); + } +}; + +prototype.restoreAdjacentTextNodes = function(root) { + let elements = root.querySelectorAll('script[data-hbs-split]'); + + for (let i=0, l=elements.length; iGodfrey

'); }); + +test("it is possible to serialize a template with adjacent text nodes", function() { + let template = compile("

{{salutation}} {{name}}

"); + let obj = { salutation: 'Mr.', name: 'Godfrey Chan' }; + let result = template.render(obj, env); + + equalTokens(result.fragment, '

Mr. Godfrey Chan

'); + + let serializedChildNodes = prepareAndSerializeNode(env, result.root); + let serialized = result.fragment.cloneNode(true).firstChild; + + // TODO: actually serialize and parse this, so it works with SimpleDOM and is more accurate + // at the moment, this is a sanity check that we didn't leave any adjacent text nodes + // around. + serialized.normalize(); + + let newRoot = env.dom.createMorph(null, serialized, serialized); + + let newNode = rehydrateNode(serializedChildNodes, newRoot); + + let scope = env.hooks.createFreshScope(); + + obj.name = "Yehuda Katz"; + result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: newNode, self: obj }); + newRoot.ownerNode = newRoot; + result.render(); + + equalTokens(result.root.firstNode, '

Mr. Yehuda Katz

'); +}); diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index 90c0d776..f9410c1a 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -331,6 +331,7 @@ export function getCachedFragment(template, env) { export function rehydrateNode(serializedNodes, renderNode) { let dom = renderNode.domHelper; let context = renderNode.firstNode.parentNode; + dom.restoreAdjacentTextNodes(context); let cache = Object.create(null); renderNode.childNodes = serializedNodes.map(childNode => _rehydrateNode(renderNode, childNode, dom, context, cache)); @@ -368,10 +369,12 @@ function elementFromId(dom, context, id, cache) { return element; } -export function serializeNode(env, renderNode) { +export function prepareAndSerializeNode(env, renderNode) { let serializationContext = { id: 0 }; - return renderNode.childNodes.map(childNode => _serializeNode(env, childNode, serializationContext)); + let serialized = renderNode.childNodes.map(childNode => _serializeNode(env, childNode, serializationContext)); + env.dom.preserveAdjacentTextNodes(renderNode.firstNode.parentNode); + return serialized; //return [{ //type: 'attr', @@ -413,18 +416,21 @@ function parentOffsets(dom, parent, renderNode) { let firstNeedle = renderNode.firstNode; let lastNeedle = renderNode.lastNode; let firstNode, lastNode; + let offset = 0; while (current !== firstNeedle) { + offset++; current = current.nextSibling; } - firstNode = current; + firstNode = offset; while (current !== lastNeedle) { + offset++; current = current.nextSibling; } - lastNode = current; + lastNode = offset; return { firstNode, lastNode }; } From 5b2eb3a3ce5cf880582668777ce42b119ae10910 Mon Sep 17 00:00:00 2001 From: Godhuda Date: Wed, 19 Aug 2015 13:05:54 -0700 Subject: [PATCH 6/6] Preserve empty text nodes across DOM serialization This is important because we use empty text nodes as boundary nodes. --- packages/dom-helper/lib/main.js | 29 ++++++++++++++----- .../htmlbars-compiler/tests/dirtying-test.js | 26 +++++++++++++++++ packages/htmlbars-runtime/lib/render.js | 4 +-- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/dom-helper/lib/main.js b/packages/dom-helper/lib/main.js index 0705a340..46cac961 100644 --- a/packages/dom-helper/lib/main.js +++ b/packages/dom-helper/lib/main.js @@ -552,9 +552,9 @@ prototype.parseHTML = function(html, contextualElement) { const SHOW_TEXT = 4; -prototype.preserveAdjacentTextNodes = function(root) { +prototype.preserveTextNodes = function(root) { let iterator = this.document.createNodeIterator(root, SHOW_TEXT, function(node) { - return node.previousSibling && node.previousSibling.nodeType === 3; + return node.nodeValue === '' || (node.previousSibling && node.previousSibling.nodeType === 3); }); let node; @@ -562,17 +562,32 @@ prototype.preserveAdjacentTextNodes = function(root) { /*jshint boss:true*/ while (node = iterator.nextNode()) { let element = this.createElement('script'); - this.setAttribute(element, 'data-hbs-split', ''); - node.parentNode.insertBefore(element, node); + + if (node.nodeValue === '') { + this.setAttribute(element, 'data-hbs', 'boundary'); + node.parentNode.replaceChild(element, node); + } else { + this.setAttribute(element, 'data-hbs', 'separator'); + node.parentNode.insertBefore(element, node); + } } }; -prototype.restoreAdjacentTextNodes = function(root) { - let elements = root.querySelectorAll('script[data-hbs-split]'); +prototype.restoreTextNodes = function(root) { + let elements = root.querySelectorAll('script[data-hbs]'); for (let i=0, l=elements.length; iMr. Yehuda Katz

'); }); + +test("it is possible to serialize empty text nodes", function() { + let template = compile("

"); + let obj = {}; + let result = template.render(obj, env); + + equalTokens(result.fragment, '

'); + env.dom.appendText(result.fragment.firstChild, ''); + + let serializedChildNodes = prepareAndSerializeNode(env, result.root); + let serialized = result.fragment.cloneNode(true).firstChild; + + // TODO: actually serialize and parse this, so it works with SimpleDOM and is more accurate + // at the moment, this is a sanity check that we didn't leave any adjacent text nodes + // around. + serialized.normalize(); + + let newRoot = env.dom.createMorph(null, serialized, serialized); + + let newNode = rehydrateNode(serializedChildNodes, newRoot); + + let p = newNode.firstNode; + equal(p.childNodes.length, 1, "There is one child node"); + equal(p.childNodes[0].nodeType, 3, "It's a text node"); + equal(p.childNodes[0].nodeValue, '', "An empty text node"); +}); diff --git a/packages/htmlbars-runtime/lib/render.js b/packages/htmlbars-runtime/lib/render.js index f9410c1a..93dda0ca 100644 --- a/packages/htmlbars-runtime/lib/render.js +++ b/packages/htmlbars-runtime/lib/render.js @@ -331,7 +331,7 @@ export function getCachedFragment(template, env) { export function rehydrateNode(serializedNodes, renderNode) { let dom = renderNode.domHelper; let context = renderNode.firstNode.parentNode; - dom.restoreAdjacentTextNodes(context); + dom.restoreTextNodes(context); let cache = Object.create(null); renderNode.childNodes = serializedNodes.map(childNode => _rehydrateNode(renderNode, childNode, dom, context, cache)); @@ -373,7 +373,7 @@ export function prepareAndSerializeNode(env, renderNode) { let serializationContext = { id: 0 }; let serialized = renderNode.childNodes.map(childNode => _serializeNode(env, childNode, serializationContext)); - env.dom.preserveAdjacentTextNodes(renderNode.firstNode.parentNode); + env.dom.preserveTextNodes(renderNode.firstNode.parentNode); return serialized; //return [{