Skip to content

Commit e85fef3

Browse files
committed
hydration
1 parent 2943eef commit e85fef3

8 files changed

+246
-21
lines changed

README.md

+1-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@ You must provide symbols to create the final js file and the output path. We do
3333
module.exports = {
3434
output: 'path-to-output/filename.js',
3535
variables: {
36-
imports: [ `import S from 's-js'` ],
37-
declarations: {
38-
wrap: 'S',
39-
}
36+
imports: [ `import wrap from 's-js'` ]
4037
}
4138
}
4239
```

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "dom-expressions",
33
"description": "A Fine-Grained Runtime for Performant DOM Rendering",
4-
"version": "0.11.4",
4+
"version": "0.12.0",
55
"author": "Ryan Carniato",
66
"license": "MIT",
77
"repository": {
@@ -24,7 +24,7 @@
2424
"devDependencies": {
2525
"@babel/core": "7.6.0",
2626
"@babel/preset-env": "^7.6.0",
27-
"babel-plugin-jsx-dom-expressions": "0.11.6",
27+
"babel-plugin-jsx-dom-expressions": "0.12.0",
2828
"coveralls": "3.0.6",
2929
"jest": "24.9.0",
3030
"s-js": "~0.4.9"

runtime.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ declare module "dom-expressions-runtime" {
88
export function spread(node: HTMLElement, accessor: any, isSVG: Boolean): void;
99
export function classList(node: HTMLElement, value: { [k: string]: boolean; }): void;
1010
export function currentContext(): any;
11+
export function isSSR(): boolean;
12+
export function startSSR(): void;
13+
export function hydration(fn: () => unknown, node: HTMLElement): void;
14+
export function getNextElement(template: HTMLTemplateElement): Node;
15+
export function getNextMarker(start: Node): [Node, Array<Node>];
1116
}

template/runtime.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ export function delegateEvents(eventNames: string[]): void;
66
export function clearDelegatedEvents(): void;
77
export function spread(node: HTMLElement, accessor: any, isSVG: Boolean): void;
88
export function classList(node: HTMLElement, value: { [k: string]: boolean; }): void;
9-
export function currentContext(): any;
9+
export function currentContext(): any;
10+
export function isSSR(): boolean;
11+
export function startSSR(): void;
12+
export function hydration(fn: () => unknown, node: HTMLElement): void;
13+
export function getNextElement(template: HTMLTemplateElement): Node;
14+
export function getNextMarker(start: Node): [Node, Array<Node>];

template/runtime.ejs

+61-6
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,61 @@ export function insert(parent, accessor, marker, initial) {
7575
}
7676
}
7777

78+
// SSR
79+
let hydrateRegistry = null,
80+
hydrateKey = 0,
81+
SSR = false;
82+
83+
export function isSSR() { return SSR; }
84+
export function startSSR() {
85+
hydrateKey = 0;
86+
SSR = true;
87+
}
88+
89+
export function hydration(code, root) {
90+
hydrateRegistry = new Map();
91+
hydrateKey = 0;
92+
SSR = false;
93+
const iterator = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
94+
acceptNode: node => node.hasAttribute('_hk') && NodeFilter.FILTER_ACCEPT
95+
});
96+
let node;
97+
while (node = iterator.nextNode()) hydrateRegistry.set(node.getAttribute('_hk'), node);
98+
99+
code();
100+
hydrateRegistry = null;
101+
}
102+
103+
export function getNextElement(template) {
104+
if (!hydrateRegistry) {
105+
const el = template.content.firstChild.cloneNode(true);
106+
if (SSR) el.setAttribute('_hk', `${hydrateKey++}`);
107+
return el;
108+
}
109+
return hydrateRegistry.get(`${hydrateKey++}`);
110+
}
111+
112+
export function getNextMarker(start) {
113+
let end = start,
114+
count = 0,
115+
current = [];
116+
if (hydrateRegistry) {
117+
while (end) {
118+
if (end.nodeType === 8) {
119+
const v = end.nodeValue;
120+
if (v === "#") count++;
121+
else if (v === "/") {
122+
if (count === 0) return [end, current];
123+
count--;
124+
}
125+
}
126+
current.push(end);
127+
end = end.nextSibling;
128+
}
129+
}
130+
return [end, current];
131+
}
132+
78133
// Internal Functions
79134
function dynamicProp(props, key) {
80135
const src = props[key];
@@ -342,12 +397,12 @@ function reconcileArrays(parent, ns, us) {
342397
}
343398

344399
// Positions for reusing nodes from current DOM state
345-
const P = new Array(umax - umin + 1);
346-
for(let i = umin; i <= umax; i++) P[i] = NOMATCH;
347-
348-
// Index to resolve position from current to new
349-
const I = new Map();
350-
for(let i = umin; i <= umax; i++) I.set(us[i], i);
400+
const P = new Array(umax - umin + 1),
401+
I = new Map();
402+
for(let i = umin; i <= umax; i++) {
403+
P[i] = NOMATCH;
404+
I.set(us[i], i);
405+
}
351406

352407
let reusingNodes = umin + us.length - 1 - umax,
353408
toRemove = []

test/hydrate.spec.js

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import * as r from "./runtime";
2+
import S from "s-js";
3+
4+
describe("r.hydration", () => {
5+
const container = document.createElement("div"),
6+
_tmpl$ = r.template(`<span><!--#--><!--/--> John</span>`),
7+
_tmpl$2 = r.template(`<div>First</div>`),
8+
_tmpl$3 = r.template(`<div>Last</div>`);
9+
10+
it("hydrates simple text", () => {
11+
S.root(() => {
12+
r.startSSR();
13+
const leadingExpr = (function() {
14+
const _el$ = r.getNextElement(_tmpl$),
15+
_el$2 = _el$.firstChild,
16+
_el$3 = _el$2.nextSibling;
17+
r.insert(_el$, "Hi", _el$3);
18+
return _el$;
19+
})();
20+
r.insert(container, leadingExpr);
21+
});
22+
expect(container.innerHTML).toBe(`<span _hk="0"><!--#-->Hi<!--/--> John</span>`);
23+
// gather refs
24+
const el1 = container.firstChild,
25+
el2 = el1.firstChild,
26+
el3 = el2.nextSibling,
27+
el4 = el3.nextSibling;
28+
29+
S.root(() => {
30+
r.hydration(() => {
31+
const leadingExpr = (function() {
32+
const _el$ = r.getNextElement(_tmpl$),
33+
_el$2 = _el$.firstChild,
34+
[_el$3, _co$] = r.getNextMarker(_el$2.nextSibling);
35+
r.hydration(() => r.insert(_el$, "Hi", _el$3, _co$), _el$);
36+
return _el$;
37+
})();
38+
r.insert(container, leadingExpr, undefined, [...container.childNodes]);
39+
}, container);
40+
});
41+
expect(container.innerHTML).toBe(`<span _hk="0"><!--#-->Hi<!--/--> John</span>`);
42+
expect(container.firstChild).toBe(el1);
43+
expect(el1.firstChild).toBe(el2);
44+
expect(el2.nextSibling).toBe(el3);
45+
expect(el3.nextSibling).toBe(el4);
46+
});
47+
48+
it("hydrates with an updated timestamp", () => {
49+
container.removeChild(container.firstChild);
50+
const time = Date.now();
51+
S.root(() => {
52+
r.startSSR();
53+
const leadingExpr = (function() {
54+
const _el$ = r.getNextElement(_tmpl$),
55+
_el$2 = _el$.firstChild,
56+
_el$3 = _el$2.nextSibling;
57+
r.insert(_el$, time, _el$3);
58+
return _el$;
59+
})();
60+
r.insert(container, leadingExpr);
61+
});
62+
expect(container.innerHTML).toBe(`<span _hk="0"><!--#-->${time}<!--/--> John</span>`);
63+
// gather refs
64+
const el1 = container.firstChild,
65+
el2 = el1.firstChild,
66+
el3 = el2.nextSibling,
67+
el4 = el3.nextSibling;
68+
69+
const updatedTime = Date.now();
70+
S.root(() => {
71+
r.hydration(() => {
72+
const leadingExpr = (function() {
73+
const _el$ = r.getNextElement(_tmpl$),
74+
_el$2 = _el$.firstChild,
75+
[_el$3, _co$] = r.getNextMarker(_el$2.nextSibling);
76+
r.hydration(() => r.insert(_el$, updatedTime, _el$3, _co$), _el$);
77+
return _el$;
78+
})();
79+
r.insert(container, leadingExpr, undefined, [...container.childNodes]);
80+
}, container);
81+
});
82+
expect(container.innerHTML).toBe(`<span _hk="0"><!--#-->${updatedTime}<!--/--> John</span>`);
83+
expect(container.firstChild).toBe(el1);
84+
expect(el1.firstChild).toBe(el2);
85+
expect(el2.nextSibling).toBe(el3);
86+
expect(el3.nextSibling).toBe(el4);
87+
});
88+
89+
it("hydrates fragments", () => {
90+
container.removeChild(container.firstChild);
91+
r.startSSR();
92+
S.root(() => {
93+
const multiExpression = [r.getNextElement(_tmpl$2), 'middle', r.getNextElement(_tmpl$3)];
94+
r.insert(container, multiExpression);
95+
});
96+
expect(container.innerHTML).toBe(`<div _hk="0">First</div>middle<div _hk="1">Last</div>`);
97+
// gather refs
98+
const el1 = container.firstChild,
99+
el2 = el1.nextSibling,
100+
el3 = el2.nextSibling;
101+
102+
S.root(() => {
103+
r.hydration(() => {
104+
const multiExpression = [r.getNextElement(_tmpl$2), 'middle', r.getNextElement(_tmpl$3)];
105+
r.insert(container, multiExpression, undefined, [...container.childNodes]);
106+
}, container);
107+
});
108+
expect(container.innerHTML).toBe(`<div _hk="0">First</div>middle<div _hk="1">Last</div>`);
109+
expect(container.firstChild).toBe(el1);
110+
expect(el1.nextSibling).toEqual(el2);
111+
expect(el1.nextSibling.nextSibling).toBe(el3);
112+
});
113+
114+
it("hydrates fragments with dynamic", () => {
115+
while (container.firstChild) container.removeChild(container.firstChild);
116+
r.startSSR();
117+
S.root(() => {
118+
const multiExpression = [r.getNextElement(_tmpl$2), () => 'middle', r.getNextElement(_tmpl$3)];
119+
r.insert(container, multiExpression);
120+
});
121+
expect(container.innerHTML).toBe(`<div _hk="0">First</div>middle<div _hk="1">Last</div>`);
122+
// gather refs
123+
const el1 = container.firstChild,
124+
el2 = el1.nextSibling,
125+
el3 = el2.nextSibling;
126+
127+
S.root(() => {
128+
r.hydration(() => {
129+
const multiExpression = [r.getNextElement(_tmpl$2), () => 'middle', r.getNextElement(_tmpl$3)];
130+
r.insert(container, multiExpression, undefined, [...container.childNodes]);
131+
}, container);
132+
});
133+
expect(container.innerHTML).toBe(`<div _hk="0">First</div>middle<div _hk="1">Last</div>`);
134+
expect(container.firstChild).toBe(el1);
135+
expect(el1.nextSibling).toEqual(el2);
136+
expect(el1.nextSibling.nextSibling).toBe(el3);
137+
});
138+
139+
it("hydrates fragments with dynamic template", () => {
140+
while (container.firstChild) container.removeChild(container.firstChild);
141+
r.startSSR();
142+
S.root(() => {
143+
const multiExpression = [r.getNextElement(_tmpl$2), () => r.getNextElement(_tmpl$2), r.getNextElement(_tmpl$3)];
144+
r.insert(container, multiExpression);
145+
});
146+
expect(container.innerHTML).toBe(`<div _hk="0">First</div><div _hk="2">First</div><div _hk="1">Last</div>`);
147+
// gather refs
148+
const el1 = container.firstChild,
149+
el2 = el1.nextSibling,
150+
el3 = el2.nextSibling;
151+
152+
S.root(() => {
153+
r.hydration(() => {
154+
const multiExpression = [r.getNextElement(_tmpl$2), () => r.getNextElement(_tmpl$2), r.getNextElement(_tmpl$3)];
155+
r.insert(container, multiExpression, undefined, [...container.childNodes]);
156+
}, container);
157+
});
158+
expect(container.innerHTML).toBe(`<div _hk="0">First</div><div _hk="2">First</div><div _hk="1">Last</div>`);
159+
expect(container.firstChild).toBe(el1);
160+
expect(el1.nextSibling).toBe(el2);
161+
expect(el1.nextSibling.nextSibling).toBe(el3);
162+
});
163+
});

test/insert.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as r from './runtime'
1+
import * as r from './runtime';
22

33
describe("r.insert", () => {
44
// <div><!-- insert --></div>

0 commit comments

Comments
 (0)