Skip to content
This repository was archived by the owner on Apr 4, 2019. It is now read-only.

Commit 13257d7

Browse files
Tom Dale and Yehuda Katztilde-engineering
Tom Dale and Yehuda Katz
authored andcommitted
Ensure templates always have stable boundaries
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.
1 parent fbfcb65 commit 13257d7

File tree

6 files changed

+109
-13
lines changed

6 files changed

+109
-13
lines changed

packages/dom-helper/lib/main.js

+6
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ prototype.appendMorph = function(element, contextualElement) {
414414
return this.createMorph(element, insertion, insertion, contextualElement);
415415
};
416416

417+
prototype.insertBoundary = function(fragment, index) {
418+
// this will always be null or firstChild
419+
var child = index === null ? null : this.childAtIndex(fragment, index);
420+
this.insertBefore(fragment, this.createTextNode(''), child);
421+
};
422+
417423
prototype.parseHTML = function(html, contextualElement) {
418424
var childNodes;
419425

packages/htmlbars-compiler/lib/hydration-javascript-compiler.js

+27-5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ prototype.compile = function(opcodes, options) {
3030
this.statements = [];
3131
this.expressionStack = [];
3232
this.augmentContext = [];
33+
this.hasOpenBoundary = false;
34+
this.hasCloseBoundary = false;
3335

3436
processOpcodes(this, opcodes);
3537

@@ -64,23 +66,35 @@ prototype.compile = function(opcodes, options) {
6466
if (this.fragmentProcessing.length) {
6567
var processing = "";
6668
for (i = 0, l = this.fragmentProcessing.length; i < l; ++i) {
67-
processing += this.indent+this.fragmentProcessing[i]+'\n';
69+
processing += this.indent+' '+this.fragmentProcessing[i]+'\n';
6870
}
6971
result.fragmentProcessingProgram = processing;
7072
}
7173

74+
var createMorphsProgram;
7275
if (result.hasMorphs) {
73-
result.createMorphsProgram =
76+
createMorphsProgram =
7477
'function buildRenderNodes(dom, fragment, contextualElement) {\n' +
75-
result.fragmentProcessingProgram +
76-
morphs +
78+
result.fragmentProcessingProgram + morphs;
79+
80+
if (this.hasOpenBoundary) {
81+
createMorphsProgram += indent+" dom.insertBoundary(fragment, 0);\n";
82+
}
83+
84+
if (this.hasCloseBoundary) {
85+
createMorphsProgram += indent+" dom.insertBoundary(fragment, null);\n";
86+
}
87+
88+
createMorphsProgram +=
7789
indent + ' return morphs;\n' +
7890
indent+'}';
7991
} else {
80-
result.createMorphsProgram =
92+
createMorphsProgram =
8193
'function buildRenderNodes() { return []; }';
8294
}
8395

96+
result.createMorphsProgram = createMorphsProgram;
97+
8498
return result;
8599
};
86100

@@ -104,6 +118,14 @@ prototype.prepareObject = function(size) {
104118
this.expressionStack.push(pairs);
105119
};
106120

121+
prototype.openBoundary = function() {
122+
this.hasOpenBoundary = true;
123+
};
124+
125+
prototype.closeBoundary = function() {
126+
this.hasCloseBoundary = true;
127+
};
128+
107129
prototype.pushLiteral = function(value) {
108130
this.expressionStack.push(value);
109131
};

packages/htmlbars-compiler/lib/hydration-opcode-compiler.js

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ HydrationOpcodeCompiler.prototype.startProgram = function(program, c, blankChild
7171
}
7272
};
7373

74+
HydrationOpcodeCompiler.prototype.insertBoundary = function(first) {
75+
this.opcode(first ? 'openBoundary' : 'closeBoundary');
76+
};
77+
7478
HydrationOpcodeCompiler.prototype.endProgram = function() {
7579
distributeMorphs(this.morphs, this.opcodes);
7680
};

packages/htmlbars-compiler/lib/template-compiler.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,37 @@ function TemplateCompiler(options) {
1818

1919
export default TemplateCompiler;
2020

21+
var dynamicNodes = {
22+
mustache: true,
23+
block: true,
24+
component: true
25+
};
26+
2127
TemplateCompiler.prototype.compile = function(ast) {
2228
var templateVisitor = new TemplateVisitor();
2329
templateVisitor.visit(ast);
2430

25-
processOpcodes(this, templateVisitor.actions);
31+
var normalizedActions = [];
32+
var actions = templateVisitor.actions;
33+
34+
for (var i=0, l=actions.length - 1; i<l; i++) {
35+
var action = actions[i];
36+
var nextAction = actions[i + 1];
37+
38+
normalizedActions.push(action);
39+
40+
if (action[0] === "startProgram" && nextAction[0] in dynamicNodes) {
41+
normalizedActions.push(['insertBoundary', [true]]);
42+
}
43+
44+
if (nextAction[0] === "endProgram" && action[0] in dynamicNodes) {
45+
normalizedActions.push(['insertBoundary', [false]]);
46+
}
47+
}
48+
49+
normalizedActions.push(actions[actions.length - 1]);
50+
51+
processOpcodes(this, normalizedActions);
2652

2753
return this.templates.pop();
2854
};
@@ -37,6 +63,10 @@ TemplateCompiler.prototype.startProgram = function(program, childTemplateCount,
3763
}
3864
};
3965

66+
TemplateCompiler.prototype.insertBoundary = function(first) {
67+
this.hydrationOpcodeCompiler.insertBoundary(first);
68+
};
69+
4070
TemplateCompiler.prototype.getChildTemplateVars = function(indent) {
4171
var vars = '';
4272
if (this.childTemplates) {

packages/htmlbars-compiler/tests/dirtying-test.js

+33
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,39 @@ test("a simple implementation of a dirtying rerender", function() {
8484
equalTokens(result.fragment, '<div><p>Nothing</p></div>');
8585
});
8686

87+
test("block helpers whose template has a morph at the edge", function() {
88+
registerHelper('id', function(params, hash, options) {
89+
return options.template.render(this);
90+
});
91+
92+
var template = compile("{{#id}}{{value}}{{/id}}");
93+
var object = { value: "hello world" };
94+
var result = template.render(object, env);
95+
96+
equalTokens(result.fragment, 'hello world');
97+
var firstNode = result.root.firstNode;
98+
equal(firstNode.nodeType, 3, "first node of the parent template");
99+
equal(firstNode.textContent, "", "its content should be empty");
100+
101+
var secondNode = firstNode.nextSibling;
102+
equal(secondNode.nodeType, 3, "first node of the helper template should be a text node");
103+
equal(secondNode.textContent, "", "its content should be empty");
104+
105+
var textContent = secondNode.nextSibling;
106+
equal(textContent.nodeType, 3, "second node of the helper should be a text node");
107+
equal(textContent.textContent, "hello world", "its content should be hello world");
108+
109+
var fourthNode = textContent.nextSibling;
110+
equal(fourthNode.nodeType, 3, "last node of the helper should be a text node");
111+
equal(fourthNode.textContent, "", "its content should be empty");
112+
113+
var lastNode = fourthNode.nextSibling;
114+
equal(lastNode.nodeType, 3, "last node of the parent template should be a text node");
115+
equal(lastNode.textContent, "", "its content should be empty");
116+
117+
strictEqual(lastNode.nextSibling, null, "there should only be five nodes");
118+
});
119+
87120
test("clean content doesn't get blown away", function() {
88121
var template = compile("<div>{{value}}</div>");
89122
var object = { value: "hello" };

packages/htmlbars-compiler/tests/html-compiler-test.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ test("The compiler can handle top-level unescaped tr", function() {
277277
var fragment = template.render(context, env, { contextualElement: document.createElement('table') }).fragment;
278278

279279
equal(
280-
fragment.firstChild.tagName, 'TR',
280+
fragment.firstChild.nextSibling.tagName, 'TR',
281281
"root tr is present" );
282282
});
283283

@@ -287,7 +287,7 @@ test("The compiler can handle top-level unescaped td inside tr contextualElement
287287
var fragment = template.render(context, env, { contextualElement: document.createElement('tr') }).fragment;
288288

289289
equal(
290-
fragment.firstChild.tagName, 'TD',
290+
fragment.firstChild.nextSibling.tagName, 'TD',
291291
"root td is returned" );
292292
});
293293

@@ -301,7 +301,7 @@ test("The compiler can handle unescaped tr in top of content", function() {
301301
var fragment = template.render(context, env, { contextualElement: document.createElement('table') }).fragment;
302302

303303
equal(
304-
fragment.firstChild.tagName, 'TR',
304+
fragment.firstChild.nextSibling.nextSibling.tagName, 'TR',
305305
"root tr is present" );
306306
});
307307

@@ -316,7 +316,7 @@ test("The compiler can handle unescaped tr inside fragment table", function() {
316316
var tableNode = fragment.firstChild;
317317

318318
equal(
319-
tableNode.firstChild.tagName, 'TR',
319+
tableNode.firstChild.nextSibling.tagName, 'TR',
320320
"root tr is present" );
321321
});
322322

@@ -1187,6 +1187,7 @@ test("svg can live with hydration", function() {
11871187
var template = compile('<svg></svg>{{name}}');
11881188

11891189
var fragment = template.render({ name: 'Milly' }, env, { contextualElement: document.body }).fragment;
1190+
11901191
equal(
11911192
fragment.childNodes[0].namespaceURI, svgNamespace,
11921193
"svg namespace inside a block is present" );
@@ -1246,16 +1247,16 @@ test("Block helper allows interior namespace", function() {
12461247

12471248
var fragment = template.render({ isTrue: true }, env, { contextualElement: document.body }).fragment;
12481249
equal(
1249-
fragment.firstChild.namespaceURI, svgNamespace,
1250+
fragment.firstChild.nextSibling.namespaceURI, svgNamespace,
12501251
"svg namespace inside a block is present" );
12511252

12521253
isTrue = false;
12531254
fragment = template.render({ isTrue: false }, env, { contextualElement: document.body }).fragment;
12541255
equal(
1255-
fragment.firstChild.namespaceURI, xhtmlNamespace,
1256+
fragment.firstChild.nextSibling.namespaceURI, xhtmlNamespace,
12561257
"inverse block path has a normal namespace");
12571258
equal(
1258-
fragment.firstChild.childNodes[0].namespaceURI, svgNamespace,
1259+
fragment.firstChild.nextSibling.firstChild.namespaceURI, svgNamespace,
12591260
"svg namespace inside an element inside a block is present" );
12601261
});
12611262

0 commit comments

Comments
 (0)