Skip to content

Commit

Permalink
hydration context support
Browse files Browse the repository at this point in the history
  • Loading branch information
ryansolid committed Jan 6, 2020
1 parent 01c12fd commit 49389ba
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 46 deletions.
12 changes: 8 additions & 4 deletions dom-expressions.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
module.exports = {
output: 'test/runtime.js',
output: "test/runtime.js",
variables: {
imports: [ `import wrap, { value, sample as ignore } from 's-js'` ],
imports: [
`import wrap, { value, sample as ignore } from 's-js'`,
`import { sharedConfig } from './hydrate.config'`
],
declarations: {
wrapCondition: `(fn) => {
const s = value(ignore(fn));
Expand All @@ -11,6 +14,7 @@ module.exports = {
},
includeContext: false,
wrapConditionals: true,
classComponents: true
classComponents: true,
sharedConfig: true
}
}
};
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dom-expressions",
"description": "A Fine-Grained Runtime for Performant DOM Rendering",
"version": "0.14.10",
"version": "0.14.11",
"author": "Ryan Carniato",
"license": "MIT",
"repository": {
Expand Down
7 changes: 5 additions & 2 deletions runtime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ declare module "dom-expressions-runtime" {
timeoutMs?: number;
}
): Promise<string>;
export function hydration(
export function hydrate(
fn: () => unknown,
node: Element | Document | ShadowRoot | DocumentFragment
): void;
export function getNextElement(template: HTMLTemplateElement): Node;
export function getNextElement(
template: HTMLTemplateElement,
isSSR: boolean
): Node;
export function getNextMarker(start: Node): [Node, Array<Node>];
}
7 changes: 5 additions & 2 deletions template/runtime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ export function renderToString(
timeoutMs?: number;
}
): Promise<string>;
export function hydration(
export function hydrate(
fn: () => unknown,
node: Element | Document | ShadowRoot | DocumentFragment
): void;
export function getNextElement(template: HTMLTemplateElement): Node;
export function getNextElement(
template: HTMLTemplateElement,
isSSR: boolean
): Node;
export function getNextMarker(start: Node): [Node, Array<Node>];
36 changes: 18 additions & 18 deletions template/runtime.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Attributes, SVGAttributes, NonComposedEvents } from 'dom-expressions';
%>

const eventRegistry = new Set();
const config = <%-locals.sharedConfig ? 'sharedConfig' : '{}' %>;

export { wrap<%-locals.wrapConditionals ? ', wrapCondition' : '' %><%-locals.includeContext ? ', currentContext' : '' %> };

Expand Down Expand Up @@ -82,12 +83,9 @@ export function insert(parent, accessor, marker, initial) {
}

// SSR
let hydrateRegistry = null,
hydrateKey = 0;

export function renderToString(code, options = {}) {
options = { timeoutMs: 10000, ...options }
hydrateKey = 0;
config.hydrate = { id: '', count: 0 };
const container = document.createElement("div");
return new Promise(resolve => {
setTimeout(() => resolve(container.innerHTML), options.timeoutMs);
Expand All @@ -98,33 +96,33 @@ export function renderToString(code, options = {}) {
});
}

export function hydration(code, root) {
hydrateRegistry = new Map();
hydrateKey = 0;
const iterator = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
acceptNode: node => node.hasAttribute('_hk') && NodeFilter.FILTER_ACCEPT
});
let node;
while (node = iterator.nextNode()) hydrateRegistry.set(node.getAttribute('_hk'), node);

export function hydrate(code, root) {
config.hydrate = { id: '', count: 0, registry: new Map() };
const templates = root.querySelectorAll(`*[_hk]`);
for (let i = 0; i < templates.length; i++) {
const node = templates[i];
config.hydrate.registry.set(node.getAttribute('_hk'), node);
}
code();
hydrateRegistry = null;
delete config.hydrate;
}

export function getNextElement(template, isSSR) {
if (!hydrateRegistry) {
const hydrate = config.hydrate;
if (!hydrate || !hydrate.registry) {
const el = template.cloneNode(true);
if (isSSR) el.setAttribute('_hk', `${hydrateKey++}`);
if (isSSR && hydrate)
el.setAttribute('_hk', `${hydrate.id}:${hydrate.count++}`);
return el;
}
return hydrateRegistry.get(`${hydrateKey++}`);
return hydrate.registry.get(`${hydrate.id}:${hydrate.count++}`);
}

export function getNextMarker(start) {
let end = start,
count = 0,
current = [];
if (hydrateRegistry) {
if (config.hydrate && config.hydrate.registry) {
while (end) {
if (end.nodeType === 8) {
const v = end.nodeValue;
Expand Down Expand Up @@ -303,6 +301,7 @@ function insertExpression(parent, value, current, marker) {
<%-locals.wrapNested && ` return () => current;` %>
} else if (Array.isArray(value)) {
const array = normalizeIncomingArray([], value);
if (config.hydrate && config.hydrate.registry) return current;
if (array.length === 0) {
current = cleanChildren(parent, current, marker);
if (multi) return current;
Expand All @@ -319,6 +318,7 @@ function insertExpression(parent, value, current, marker) {
}
current = array;
} else if (value instanceof Node) {
if (config.hydrate && config.hydrate.registry) return current;
if (Array.isArray(current)) {
if (multi) return current = cleanChildren(parent, current, marker, value);
cleanChildren(parent, current, null, value);
Expand Down
13 changes: 13 additions & 0 deletions test/hydrate.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const sharedConfig = {};
export function setHydrateContext(context) {
sharedConfig.hydrate = context;
}
export function nextHydrateContext() {
return sharedConfig.hydrate
? {
id: `${sharedConfig.hydrate.id}.${sharedConfig.hydrate.count++}`,
count: 0,
registry: sharedConfig.hydrate.registry
}
: undefined;
}
98 changes: 80 additions & 18 deletions test/hydrate.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as r from "./runtime";
import { setHydrateContext, nextHydrateContext } from "./hydrate.config";
import S from "s-js";

describe("r.hydration", () => {
describe("r.hydrate", () => {
const container = document.createElement("div"),
_tmpl$ = r.template(`<span><!--#--><!--/--> John</span>`),
_tmpl$2 = r.template(`<div>First</div>`),
Expand All @@ -22,7 +23,7 @@ describe("r.hydration", () => {
});
});
rendered = await result;
expect(rendered).toBe(`<span _hk="0"><!--#-->Hi<!--/--> John</span>`);
expect(rendered).toBe(`<span _hk=":0"><!--#-->Hi<!--/--> John</span>`);
// gather refs
container.innerHTML = rendered;
const el1 = container.firstChild,
Expand All @@ -31,7 +32,7 @@ describe("r.hydration", () => {
el4 = el3.nextSibling;

S.root(() => {
r.hydration(() => {
r.hydrate(() => {
const leadingExpr = (function() {
const _el$ = r.getNextElement(_tmpl$),
_el$2 = _el$.firstChild,
Expand All @@ -43,7 +44,7 @@ describe("r.hydration", () => {
}, container);
});
expect(container.innerHTML).toBe(
`<span _hk="0"><!--#-->Hi<!--/--> John</span>`
`<span _hk=":0"><!--#-->Hi<!--/--> John</span>`
);
expect(container.firstChild).toBe(el1);
expect(el1.firstChild).toBe(el2);
Expand All @@ -66,7 +67,7 @@ describe("r.hydration", () => {
});
});
rendered = await result;
expect(rendered).toBe(`<span _hk="0"><!--#-->${time}<!--/--> John</span>`);
expect(rendered).toBe(`<span _hk=":0"><!--#-->${time}<!--/--> John</span>`);
// gather refs
container.innerHTML = rendered;
const el1 = container.firstChild,
Expand All @@ -76,7 +77,7 @@ describe("r.hydration", () => {

const updatedTime = Date.now();
S.root(() => {
r.hydration(() => {
r.hydrate(() => {
const leadingExpr = (function() {
const _el$ = r.getNextElement(_tmpl$),
_el$2 = _el$.firstChild,
Expand All @@ -88,7 +89,7 @@ describe("r.hydration", () => {
}, container);
});
expect(container.innerHTML).toBe(
`<span _hk="0"><!--#-->${updatedTime}<!--/--> John</span>`
`<span _hk=":0"><!--#-->${updatedTime}<!--/--> John</span>`
);
expect(container.firstChild).toBe(el1);
expect(el1.firstChild).toBe(el2);
Expand All @@ -109,7 +110,7 @@ describe("r.hydration", () => {
});
rendered = await result;
expect(rendered).toBe(
`<div _hk="0">First</div>middle<div _hk="1">Last</div>`
`<div _hk=":0">First</div>middle<div _hk=":1">Last</div>`
);
// gather refs
container.innerHTML = rendered;
Expand All @@ -118,7 +119,7 @@ describe("r.hydration", () => {
el3 = el2.nextSibling;

S.root(() => {
r.hydration(() => {
r.hydrate(() => {
const multiExpression = [
r.getNextElement(_tmpl$2),
"middle",
Expand All @@ -130,7 +131,7 @@ describe("r.hydration", () => {
}, container);
});
expect(container.innerHTML).toBe(
`<div _hk="0">First</div>middle<div _hk="1">Last</div>`
`<div _hk=":0">First</div>middle<div _hk=":1">Last</div>`
);
expect(container.firstChild).toBe(el1);
expect(el1.nextSibling).toEqual(el2);
Expand All @@ -150,7 +151,7 @@ describe("r.hydration", () => {
});
rendered = await result;
expect(rendered).toBe(
`<div _hk="0">First</div>middle<div _hk="1">Last</div>`
`<div _hk=":0">First</div>middle<div _hk=":1">Last</div>`
);
// gather refs
container.innerHTML = rendered;
Expand All @@ -159,7 +160,7 @@ describe("r.hydration", () => {
el3 = el2.nextSibling;

S.root(() => {
r.hydration(() => {
r.hydrate(() => {
const multiExpression = [
r.getNextElement(_tmpl$2),
() => "middle",
Expand All @@ -171,7 +172,7 @@ describe("r.hydration", () => {
}, container);
});
expect(container.innerHTML).toBe(
`<div _hk="0">First</div>middle<div _hk="1">Last</div>`
`<div _hk=":0">First</div>middle<div _hk=":1">Last</div>`
);
expect(container.firstChild).toBe(el1);
expect(el1.nextSibling).toEqual(el2);
Expand All @@ -191,7 +192,7 @@ describe("r.hydration", () => {
});
rendered = await result;
expect(rendered).toBe(
`<div _hk="0">First</div><div _hk="2">First</div><div _hk="1">Last</div>`
`<div _hk=":0">First</div><div _hk=":2">First</div><div _hk=":1">Last</div>`
);
// gather refs
container.innerHTML = rendered;
Expand All @@ -200,7 +201,7 @@ describe("r.hydration", () => {
el3 = el2.nextSibling;

S.root(() => {
r.hydration(() => {
r.hydrate(() => {
const multiExpression = [
r.getNextElement(_tmpl$2),
() => r.getNextElement(_tmpl$2),
Expand All @@ -212,7 +213,7 @@ describe("r.hydration", () => {
}, container);
});
expect(container.innerHTML).toBe(
`<div _hk="0">First</div><div _hk="2">First</div><div _hk="1">Last</div>`
`<div _hk=":0">First</div><div _hk=":2">First</div><div _hk=":1">Last</div>`
);
expect(container.firstChild).toBe(el1);
expect(el1.nextSibling).toBe(el2);
Expand All @@ -237,7 +238,7 @@ describe("r.hydration", () => {
});
rendered = await result;
expect(rendered).toBe(
`<div _hk="0">First</div><div _hk="2">Last</div><div _hk="1">Last</div>`
`<div _hk=":0">First</div><div _hk=":2">Last</div><div _hk=":1">Last</div>`
);
});

Expand All @@ -262,7 +263,68 @@ describe("r.hydration", () => {
});
rendered = await result;
expect(rendered).toBe(
`<div _hk="0">First</div><div _hk="1">Last</div>`
`<div _hk=":0">First</div><div _hk=":1">Last</div>`
);
});

it("renders nested asynchronous context", async () => {
S.root(() => {
function lazy(done) {
const signal = S.data(),
ctx = nextHydrateContext();
setTimeout(() => {
setHydrateContext(ctx);
signal(r.getNextElement(_tmpl$3, true));
done();
}, 20);
return signal;
}
result = r.renderToString(done => {
const multiExpression = [
r.getNextElement(_tmpl$2, true),
lazy(done),
r.getNextElement(_tmpl$3, true)
];
return multiExpression;
});
});
rendered = await result;
expect(rendered).toBe(
`<div _hk=":0">First</div><div _hk=".1:0">Last</div><div _hk=":2">Last</div>`
);
// gather refs
container.innerHTML = rendered;
const el1 = container.firstChild,
el2 = el1.nextSibling,
el3 = el2.nextSibling;

S.root(() => {
function lazy() {
const signal = S.data(),
ctx = nextHydrateContext();
setTimeout(() => {
setHydrateContext(ctx);
signal(r.getNextElement(_tmpl$3, true));
}, 20);
return signal;
}
r.hydrate(() => {
const multiExpression = [
r.getNextElement(_tmpl$2),
lazy(),
r.getNextElement(_tmpl$3)
];
r.insert(container, multiExpression, undefined, [
...container.childNodes
]);
}, container);
});
await new Promise(r => setTimeout(r, 50));
expect(container.innerHTML).toBe(
`<div _hk=":0">First</div><div _hk=".1:0">Last</div><div _hk=":2">Last</div>`
);
expect(container.firstChild).toBe(el1);
expect(el1.nextSibling).toBe(el2);
expect(el1.nextSibling.nextSibling).toBe(el3);
});
});

0 comments on commit 49389ba

Please sign in to comment.