Skip to content

Commit

Permalink
hydration
Browse files Browse the repository at this point in the history
  • Loading branch information
ryansolid committed Sep 27, 2019
1 parent 2943eef commit e85fef3
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 21 deletions.
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ You must provide symbols to create the final js file and the output path. We do
module.exports = {
output: 'path-to-output/filename.js',
variables: {
imports: [ `import S from 's-js'` ],
declarations: {
wrap: 'S',
}
imports: [ `import wrap from 's-js'` ]
}
}
```
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions 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.11.4",
"version": "0.12.0",
"author": "Ryan Carniato",
"license": "MIT",
"repository": {
Expand All @@ -24,7 +24,7 @@
"devDependencies": {
"@babel/core": "7.6.0",
"@babel/preset-env": "^7.6.0",
"babel-plugin-jsx-dom-expressions": "0.11.6",
"babel-plugin-jsx-dom-expressions": "0.12.0",
"coveralls": "3.0.6",
"jest": "24.9.0",
"s-js": "~0.4.9"
Expand Down
5 changes: 5 additions & 0 deletions runtime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ declare module "dom-expressions-runtime" {
export function spread(node: HTMLElement, accessor: any, isSVG: Boolean): void;
export function classList(node: HTMLElement, value: { [k: string]: boolean; }): void;
export function currentContext(): any;
export function isSSR(): boolean;
export function startSSR(): void;
export function hydration(fn: () => unknown, node: HTMLElement): void;
export function getNextElement(template: HTMLTemplateElement): Node;
export function getNextMarker(start: Node): [Node, Array<Node>];
}
7 changes: 6 additions & 1 deletion template/runtime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ export function delegateEvents(eventNames: string[]): void;
export function clearDelegatedEvents(): void;
export function spread(node: HTMLElement, accessor: any, isSVG: Boolean): void;
export function classList(node: HTMLElement, value: { [k: string]: boolean; }): void;
export function currentContext(): any;
export function currentContext(): any;
export function isSSR(): boolean;
export function startSSR(): void;
export function hydration(fn: () => unknown, node: HTMLElement): void;
export function getNextElement(template: HTMLTemplateElement): Node;
export function getNextMarker(start: Node): [Node, Array<Node>];
67 changes: 61 additions & 6 deletions template/runtime.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,61 @@ export function insert(parent, accessor, marker, initial) {
}
}

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

export function isSSR() { return SSR; }
export function startSSR() {
hydrateKey = 0;
SSR = true;
}

export function hydration(code, root) {
hydrateRegistry = new Map();
hydrateKey = 0;
SSR = false;
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);

code();
hydrateRegistry = null;
}

export function getNextElement(template) {
if (!hydrateRegistry) {
const el = template.content.firstChild.cloneNode(true);
if (SSR) el.setAttribute('_hk', `${hydrateKey++}`);
return el;
}
return hydrateRegistry.get(`${hydrateKey++}`);
}

export function getNextMarker(start) {
let end = start,
count = 0,
current = [];
if (hydrateRegistry) {
while (end) {
if (end.nodeType === 8) {
const v = end.nodeValue;
if (v === "#") count++;
else if (v === "/") {
if (count === 0) return [end, current];
count--;
}
}
current.push(end);
end = end.nextSibling;
}
}
return [end, current];
}

// Internal Functions
function dynamicProp(props, key) {
const src = props[key];
Expand Down Expand Up @@ -342,12 +397,12 @@ function reconcileArrays(parent, ns, us) {
}

// Positions for reusing nodes from current DOM state
const P = new Array(umax - umin + 1);
for(let i = umin; i <= umax; i++) P[i] = NOMATCH;

// Index to resolve position from current to new
const I = new Map();
for(let i = umin; i <= umax; i++) I.set(us[i], i);
const P = new Array(umax - umin + 1),
I = new Map();
for(let i = umin; i <= umax; i++) {
P[i] = NOMATCH;
I.set(us[i], i);
}

let reusingNodes = umin + us.length - 1 - umax,
toRemove = []
Expand Down
163 changes: 163 additions & 0 deletions test/hydrate.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import * as r from "./runtime";
import S from "s-js";

describe("r.hydration", () => {
const container = document.createElement("div"),
_tmpl$ = r.template(`<span><!--#--><!--/--> John</span>`),
_tmpl$2 = r.template(`<div>First</div>`),
_tmpl$3 = r.template(`<div>Last</div>`);

it("hydrates simple text", () => {
S.root(() => {
r.startSSR();
const leadingExpr = (function() {
const _el$ = r.getNextElement(_tmpl$),
_el$2 = _el$.firstChild,
_el$3 = _el$2.nextSibling;
r.insert(_el$, "Hi", _el$3);
return _el$;
})();
r.insert(container, leadingExpr);
});
expect(container.innerHTML).toBe(`<span _hk="0"><!--#-->Hi<!--/--> John</span>`);
// gather refs
const el1 = container.firstChild,
el2 = el1.firstChild,
el3 = el2.nextSibling,
el4 = el3.nextSibling;

S.root(() => {
r.hydration(() => {
const leadingExpr = (function() {
const _el$ = r.getNextElement(_tmpl$),
_el$2 = _el$.firstChild,
[_el$3, _co$] = r.getNextMarker(_el$2.nextSibling);
r.hydration(() => r.insert(_el$, "Hi", _el$3, _co$), _el$);
return _el$;
})();
r.insert(container, leadingExpr, undefined, [...container.childNodes]);
}, container);
});
expect(container.innerHTML).toBe(`<span _hk="0"><!--#-->Hi<!--/--> John</span>`);
expect(container.firstChild).toBe(el1);
expect(el1.firstChild).toBe(el2);
expect(el2.nextSibling).toBe(el3);
expect(el3.nextSibling).toBe(el4);
});

it("hydrates with an updated timestamp", () => {
container.removeChild(container.firstChild);
const time = Date.now();
S.root(() => {
r.startSSR();
const leadingExpr = (function() {
const _el$ = r.getNextElement(_tmpl$),
_el$2 = _el$.firstChild,
_el$3 = _el$2.nextSibling;
r.insert(_el$, time, _el$3);
return _el$;
})();
r.insert(container, leadingExpr);
});
expect(container.innerHTML).toBe(`<span _hk="0"><!--#-->${time}<!--/--> John</span>`);
// gather refs
const el1 = container.firstChild,
el2 = el1.firstChild,
el3 = el2.nextSibling,
el4 = el3.nextSibling;

const updatedTime = Date.now();
S.root(() => {
r.hydration(() => {
const leadingExpr = (function() {
const _el$ = r.getNextElement(_tmpl$),
_el$2 = _el$.firstChild,
[_el$3, _co$] = r.getNextMarker(_el$2.nextSibling);
r.hydration(() => r.insert(_el$, updatedTime, _el$3, _co$), _el$);
return _el$;
})();
r.insert(container, leadingExpr, undefined, [...container.childNodes]);
}, container);
});
expect(container.innerHTML).toBe(`<span _hk="0"><!--#-->${updatedTime}<!--/--> John</span>`);
expect(container.firstChild).toBe(el1);
expect(el1.firstChild).toBe(el2);
expect(el2.nextSibling).toBe(el3);
expect(el3.nextSibling).toBe(el4);
});

it("hydrates fragments", () => {
container.removeChild(container.firstChild);
r.startSSR();
S.root(() => {
const multiExpression = [r.getNextElement(_tmpl$2), 'middle', r.getNextElement(_tmpl$3)];
r.insert(container, multiExpression);
});
expect(container.innerHTML).toBe(`<div _hk="0">First</div>middle<div _hk="1">Last</div>`);
// gather refs
const el1 = container.firstChild,
el2 = el1.nextSibling,
el3 = el2.nextSibling;

S.root(() => {
r.hydration(() => {
const multiExpression = [r.getNextElement(_tmpl$2), 'middle', r.getNextElement(_tmpl$3)];
r.insert(container, multiExpression, undefined, [...container.childNodes]);
}, container);
});
expect(container.innerHTML).toBe(`<div _hk="0">First</div>middle<div _hk="1">Last</div>`);
expect(container.firstChild).toBe(el1);
expect(el1.nextSibling).toEqual(el2);
expect(el1.nextSibling.nextSibling).toBe(el3);
});

it("hydrates fragments with dynamic", () => {
while (container.firstChild) container.removeChild(container.firstChild);
r.startSSR();
S.root(() => {
const multiExpression = [r.getNextElement(_tmpl$2), () => 'middle', r.getNextElement(_tmpl$3)];
r.insert(container, multiExpression);
});
expect(container.innerHTML).toBe(`<div _hk="0">First</div>middle<div _hk="1">Last</div>`);
// gather refs
const el1 = container.firstChild,
el2 = el1.nextSibling,
el3 = el2.nextSibling;

S.root(() => {
r.hydration(() => {
const multiExpression = [r.getNextElement(_tmpl$2), () => 'middle', r.getNextElement(_tmpl$3)];
r.insert(container, multiExpression, undefined, [...container.childNodes]);
}, container);
});
expect(container.innerHTML).toBe(`<div _hk="0">First</div>middle<div _hk="1">Last</div>`);
expect(container.firstChild).toBe(el1);
expect(el1.nextSibling).toEqual(el2);
expect(el1.nextSibling.nextSibling).toBe(el3);
});

it("hydrates fragments with dynamic template", () => {
while (container.firstChild) container.removeChild(container.firstChild);
r.startSSR();
S.root(() => {
const multiExpression = [r.getNextElement(_tmpl$2), () => r.getNextElement(_tmpl$2), r.getNextElement(_tmpl$3)];
r.insert(container, multiExpression);
});
expect(container.innerHTML).toBe(`<div _hk="0">First</div><div _hk="2">First</div><div _hk="1">Last</div>`);
// gather refs
const el1 = container.firstChild,
el2 = el1.nextSibling,
el3 = el2.nextSibling;

S.root(() => {
r.hydration(() => {
const multiExpression = [r.getNextElement(_tmpl$2), () => r.getNextElement(_tmpl$2), r.getNextElement(_tmpl$3)];
r.insert(container, multiExpression, undefined, [...container.childNodes]);
}, container);
});
expect(container.innerHTML).toBe(`<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);
expect(el1.nextSibling.nextSibling).toBe(el3);
});
});
2 changes: 1 addition & 1 deletion test/insert.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as r from './runtime'
import * as r from './runtime';

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

0 comments on commit e85fef3

Please sign in to comment.