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

Commit 63171f0

Browse files
author
Godhuda
committed
WIP rehydration
1 parent a4665bd commit 63171f0

File tree

3 files changed

+269
-10
lines changed

3 files changed

+269
-10
lines changed

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

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { compile } from "../htmlbars-compiler/compiler";
2-
import { manualElement } from "../htmlbars-runtime/render";
2+
import { RenderResult, LastYielded, manualElement } from "../htmlbars-runtime/render";
33
import { hostBlock } from "../htmlbars-runtime/hooks";
44
import render from "../htmlbars-runtime/render";
55
import { blockFor } from "../htmlbars-util/template-utils";
66
import defaultHooks from "../htmlbars-runtime/hooks";
77
import { merge } from "../htmlbars-util/object-utils";
88
import DOMHelper from "../dom-helper";
99
import { equalTokens } from "../htmlbars-test-helpers";
10+
import { rehydrateNode, serializeNode } from "../htmlbars-runtime/render";
1011

1112
var hooks, helpers, partials, env;
1213

@@ -730,3 +731,114 @@ test("The invoke helper hook can instruct the runtime to link the result", funct
730731
equalTokens(result.fragment, "24");
731732
equal(invokeCount, 1);
732733
});
734+
735+
test("it is possible to synthesize a simple fragment and render node and pass it to the rendering process", function() {
736+
let template = compile("<p>{{name}}</p>");
737+
738+
let p = env.dom.createElement('p');
739+
env.dom.appendText(p, 'Godfrey');
740+
741+
let rootMorph = env.dom.createMorph(null, p, p);
742+
let childMorph = env.dom.createMorphAt(p, 0, 0);
743+
744+
rootMorph.childNodes = [childMorph];
745+
746+
let scope = env.hooks.createFreshScope();
747+
let obj = { name: "Yehuda" };
748+
let result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: rootMorph, self: obj });
749+
750+
let original = p.firstChild;
751+
result.render();
752+
753+
strictEqual(p.firstChild, original, "The text node remained stable");
754+
equalTokens(p, "<p>Yehuda</p>");
755+
});
756+
757+
test("it is possible to rehydrate a template with blocks", function() {
758+
let template = compile("<p>{{#if bool}}<span>{{name}}</span>{{/if}}</p>");
759+
760+
let p = env.dom.createElement('p');
761+
let span = env.dom.appendChild(p, env.dom.createElement('span'));
762+
env.dom.appendText(span, 'Godfrey');
763+
764+
let rootMorph = env.dom.createMorph(null, p, p);
765+
let childMorph1 = env.dom.createMorphAt(p, 0, 0);
766+
let childMorph2 = env.dom.createMorphAt(span, 0, 0);
767+
768+
let obj = { bool: true, name: "Yehuda" };
769+
childMorph1.lastYielded = new LastYielded(obj, template.raw.templates[0], null);
770+
771+
rootMorph.childNodes = [childMorph1];
772+
childMorph1.childNodes = [childMorph2];
773+
774+
let scope = env.hooks.createFreshScope();
775+
let result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: rootMorph, self: obj });
776+
777+
let childResult = RenderResult.rehydrate(env, scope, template.raw.templates[0], { renderNode: childMorph1, self: obj });
778+
childMorph1.lastResult = childResult;
779+
780+
let original = p.firstChild;
781+
result.render();
782+
783+
strictEqual(p.firstChild, original, "The text node remained stable");
784+
equalTokens(p, "<p><span>Yehuda</span></p>");
785+
});
786+
787+
test("it is possible to serialize a render node tree", function() {
788+
let template = compile('<p title="{{title}}">{{name}}</p>');
789+
let obj = { title: 'chancancode', name: 'Godfrey' };
790+
let result = template.render(obj, env);
791+
792+
equalTokens(result.fragment, '<p title="chancancode">Godfrey</p>');
793+
794+
let original = result.fragment.firstChild;
795+
let newRoot = env.dom.createMorph(null, original, original);
796+
newRoot.ownerNode = newRoot;
797+
let node = rehydrateNode(serializeNode(env, result.root), newRoot);
798+
799+
let scope = env.hooks.createFreshScope();
800+
801+
result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: node, self: obj });
802+
803+
result.render();
804+
805+
strictEqual(result.root.firstNode, original);
806+
equalTokens(result.root.firstNode, '<p title="chancancode">Godfrey</p>');
807+
});
808+
809+
test("it is possible to serialize a render node tree with recursive templates", function() {
810+
env.hooks.rehydrateLastYielded = function(env, morph) {
811+
morph.lastYielded.template = morph.lastYielded.templateId;
812+
};
813+
814+
env.hooks.serializeLastYielded = function(env, morph) {
815+
return morph.lastYielded.template;
816+
};
817+
818+
let template = compile('<p title="{{title}}">{{#if bool}}<span>{{name}}</span>{{/if}}</p>');
819+
let obj = { title: 'chancancode', name: 'Godfrey', bool: true };
820+
let result = template.render(obj, env);
821+
822+
equalTokens(result.fragment, '<p title="chancancode"><span>Godfrey</span></p>');
823+
824+
let original = result.fragment.firstChild;
825+
let span = original.firstChild;
826+
let godfrey = span.firstChild;
827+
828+
let newRoot = env.dom.createMorph(null, original, original);
829+
newRoot.ownerNode = newRoot;
830+
let node = rehydrateNode(serializeNode(env, result.root), newRoot);
831+
832+
let scope = env.hooks.createFreshScope();
833+
834+
result = RenderResult.rehydrate(env, scope, template.raw, { renderNode: node, self: obj });
835+
836+
result.render();
837+
838+
let newNode = result.root.firstNode;
839+
strictEqual(newNode, original, "the <p> is the same");
840+
strictEqual(newNode.firstChild, span, "the <span> is the same");
841+
strictEqual(newNode.firstChild.firstChild, godfrey, "the text node is the same");
842+
843+
equalTokens(newNode, '<p title="chancancode"><span>Godfrey</span></p>');
844+
});

packages/htmlbars-runtime/lib/hooks.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import render from "./render";
22
import MorphList from "../morph-range/morph-list";
3-
import { createChildMorph } from "./render";
3+
import { createChildMorph, RenderResult, LastYielded } from "./render";
44
import { keyLength, shallowCopy } from "../htmlbars-util/object-utils";
55
import { validateChildMorphs } from "../htmlbars-util/morph-utils";
66
import { RenderState, clearMorph, clearMorphList, renderAndCleanup } from "../htmlbars-util/template-utils";
@@ -145,10 +145,11 @@ function yieldTemplate(template, env, parentScope, morph, renderState, visitor)
145145

146146
var scope = parentScope;
147147

148-
if (morph.lastYielded && isStableTemplate(template, morph.lastYielded)) {
148+
if (morph.lastYielded && morph.lastYielded.isStableTemplate(template)) {
149149
return morph.lastResult.revalidateWith(env, undefined, self, blockArguments, visitor);
150150
}
151151

152+
152153
// Check to make sure that we actually **need** a new scope, and can't
153154
// share the parent scope. Note that we need to move this check into
154155
// 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)
157158
scope = env.hooks.createChildScope(parentScope);
158159
}
159160

160-
morph.lastYielded = { self: self, template: template, shadowTemplate: null };
161+
if (morph.lastYielded && morph.lastYielded.templateId) {
162+
env.hooks.rehydrateLastYielded(env, morph);
163+
164+
if (morph.lastYielded.isStableTemplate(template)) {
165+
let renderResult = RenderResult.rehydrate(env, scope, template, { renderNode: morph, self, blockArguments });
166+
renderResult.render();
167+
return;
168+
}
169+
}
170+
171+
morph.lastYielded = new LastYielded(self, template, null);
161172

162173
// Render the template that was selected by the helper
163174
render(template, env, scope, { renderNode: morph, self: self, blockArguments: blockArguments });
@@ -275,10 +286,6 @@ function yieldItem(template, env, parentScope, morph, renderState, visitor) {
275286
};
276287
}
277288

278-
function isStableTemplate(template, lastYielded) {
279-
return !lastYielded.shadowTemplate && template === lastYielded.template;
280-
}
281-
282289
function yieldInShadowTemplate(template, env, parentScope, morph, renderState, visitor) {
283290
var hostYield = hostYieldWithShadowTemplate(template, env, parentScope, morph, renderState, visitor);
284291

@@ -300,7 +307,7 @@ export function hostYieldWithShadowTemplate(template, env, parentScope, morph, r
300307
blockToYield.arity = template.arity;
301308
env.hooks.bindBlock(env, shadowScope, blockToYield);
302309

303-
morph.lastYielded = { self: self, template: template, shadowTemplate: shadowTemplate };
310+
morph.lastYielded = new LastYielded(self, template, shadowTemplate);
304311

305312
// Render the shadow template with the block available
306313
render(shadowTemplate.raw, env, shadowScope, { renderNode: morph, self: self, blockArguments: blockArguments });
@@ -1094,6 +1101,8 @@ export default {
10941101
linkRenderNode: linkRenderNode,
10951102
partial: partial,
10961103
subexpr: subexpr,
1104+
rehydrateLastYielded: null,
1105+
serializeLastYielded: null,
10971106

10981107
// fundamental hooks with good default behavior
10991108
bindBlock: bindBlock,

packages/htmlbars-runtime/lib/render.js

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default function render(template, env, scope, options) {
2828
return renderResult;
2929
}
3030

31-
function RenderResult(env, scope, options, rootNode, ownerNode, nodes, fragment, template, shouldSetContent) {
31+
export function RenderResult(env, scope, options, rootNode, ownerNode, nodes, fragment, template, shouldSetContent) {
3232
this.root = rootNode;
3333
this.fragment = fragment;
3434

@@ -59,6 +59,7 @@ RenderResult.build = function(env, scope, template, options, contextualElement)
5959
ownerNode = rootNode.ownerNode;
6060
shouldSetContent = true;
6161
} else {
62+
// creating the root render node
6263
rootNode = dom.createMorph(null, fragment.firstChild, fragment.lastChild, contextualElement);
6364
ownerNode = rootNode;
6465
initializeNode(rootNode, ownerNode);
@@ -75,6 +76,14 @@ RenderResult.build = function(env, scope, template, options, contextualElement)
7576
return new RenderResult(env, scope, options, rootNode, ownerNode, nodes, fragment, template, shouldSetContent);
7677
};
7778

79+
RenderResult.rehydrate = function(env, scope, template, options) {
80+
let rootNode = options.renderNode;
81+
let ownerNode = rootNode.ownerNode;
82+
let shouldSetContent = false;
83+
84+
return new RenderResult(env, scope, options, rootNode, ownerNode, rootNode.childNodes, null, template, shouldSetContent);
85+
};
86+
7887
export function manualElement(tagName, attributes, _isEmpty) {
7988
var statements = [];
8089

@@ -318,3 +327,132 @@ export function getCachedFragment(template, env) {
318327

319328
return fragment;
320329
}
330+
331+
export function rehydrateNode(serializedNodes, renderNode) {
332+
let dom = renderNode.domHelper;
333+
let context = renderNode.firstNode.parentNode;
334+
let cache = Object.create(null);
335+
336+
renderNode.childNodes = serializedNodes.map(childNode => _rehydrateNode(renderNode, childNode, dom, context, cache));
337+
return renderNode;
338+
}
339+
340+
function _rehydrateNode(owner, renderNode, dom, context, cache) {
341+
let element, node;
342+
343+
switch (renderNode.type) {
344+
case 'attr':
345+
element = elementFromId(dom, context, renderNode.element, cache);
346+
node = dom.createAttrMorph(element, renderNode.attrName);
347+
break;
348+
case 'range':
349+
element = elementFromId(dom, context, renderNode.parentNode, cache);
350+
node = dom.createMorphAt(element, renderNode.firstNode, renderNode.lastNode);
351+
node.lastYielded = LastYielded.fromTemplateId(renderNode.templateId);
352+
node.childNodes = renderNode.childNodes && renderNode.childNodes.map(childNode => _rehydrateNode(node, childNode, dom, context, cache));
353+
break;
354+
}
355+
356+
initializeNode(node, owner);
357+
return node;
358+
}
359+
360+
function elementFromId(dom, context, id, cache) {
361+
if (id in cache) {
362+
return cache[id];
363+
}
364+
365+
let element = context.querySelector(`[data-hbs-node="${id}"]`);
366+
dom.removeAttribute(element, 'data-hbs-node');
367+
cache[id] = element;
368+
return element;
369+
}
370+
371+
export function serializeNode(env, renderNode) {
372+
let serializationContext = { id: 0 };
373+
374+
return renderNode.childNodes.map(childNode => _serializeNode(env, childNode, serializationContext));
375+
376+
//return [{
377+
//type: 'attr',
378+
//element: "0",
379+
//attrName: 'title'
380+
//}, {
381+
//type: 'range',
382+
//parentNode: "0",
383+
//firstNode: 0,
384+
//lastNode: 0
385+
//}];
386+
}
387+
388+
function _serializeNode(env, renderNode, serializationContext) {
389+
let dom = env.dom;
390+
if (renderNode instanceof dom.MorphClass) {
391+
let parent = renderNode.firstNode.parentNode;
392+
let { firstNode, lastNode } = parentOffsets(dom, parent, renderNode);
393+
394+
return {
395+
type: 'range',
396+
childNodes: renderNode.childNodes && renderNode.childNodes.map(childNode => _serializeNode(env, childNode, serializationContext)),
397+
parentNode: idFromElement(dom, parent, serializationContext),
398+
templateId: renderNode.lastYielded && env.hooks.serializeLastYielded(env, renderNode),
399+
firstNode,
400+
lastNode
401+
};
402+
} else if (renderNode instanceof dom.AttrMorphClass) {
403+
return {
404+
type: 'attr',
405+
element: idFromElement(dom, renderNode.element, serializationContext),
406+
attrName: renderNode.attrName
407+
};
408+
}
409+
}
410+
411+
function parentOffsets(dom, parent, renderNode) {
412+
let current = parent.firstChild;
413+
let firstNeedle = renderNode.firstNode;
414+
let lastNeedle = renderNode.lastNode;
415+
let firstNode, lastNode;
416+
417+
while (current !== firstNeedle) {
418+
current = current.nextSibling;
419+
}
420+
421+
firstNode = current;
422+
423+
while (current !== lastNeedle) {
424+
current = current.nextSibling;
425+
}
426+
427+
lastNode = current;
428+
429+
return { firstNode, lastNode };
430+
}
431+
432+
function idFromElement(dom, element, serializationContext) {
433+
let id = dom.getAttribute(element, 'data-hbs-node');
434+
435+
if (id) {
436+
return id;
437+
}
438+
439+
id = (serializationContext.id++) + '';
440+
dom.setAttribute(element, 'data-hbs-node', id);
441+
return id;
442+
}
443+
444+
export function LastYielded(self, template, shadowTemplate, templateId) {
445+
this.self = self;
446+
this.template = template;
447+
this.shadowTemplate = shadowTemplate;
448+
this.templateId = templateId;
449+
}
450+
451+
LastYielded.fromTemplateId = function(templateId) {
452+
return new LastYielded(null, null, null, templateId);
453+
};
454+
455+
LastYielded.prototype.isStableTemplate = function(nextTemplate) {
456+
return !this.shadowTemplate && nextTemplate === this.template;
457+
};
458+

0 commit comments

Comments
 (0)