Skip to content

Commit cb1c528

Browse files
committed
feat: support containerless rehydration
1 parent af357eb commit cb1c528

File tree

3 files changed

+70
-68
lines changed

3 files changed

+70
-68
lines changed
Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,13 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`reactFromMarkupContainer E2E tests Should rehydrate a basic component 1`] = `
4-
"
5-
<div data-react-from-html-container=\\"\\">
6-
<span>rehydrated component</span>
7-
</div>"
8-
`;
9-
10-
exports[`reactFromMarkupContainer E2E tests Should rehydrate valid HTML markup 1`] = `
11-
"
12-
<div data-react-from-html-container=\\"\\">
13-
<p>paragraph</p>
14-
</div>"
15-
`;
3+
exports[`reactFromHtml E2E tests Should rehydrate a basic component 1`] = `"<div class=\\"rehydration-root\\"><span>rehydrated component</span></div>"`;
164
17-
exports[`reactFromMarkupContainer E2E tests Should work for nested markup containers 1`] = `
5+
exports[`reactFromHtml E2E tests Should work for nested rehydratables 1`] = `
186
"
19-
<div data-react-from-html-container=\\"\\">
20-
<span>rehydrated component</span>
21-
<div data-react-from-html-container=\\"\\">
22-
<span>rehydrated component</span>
23-
<span>rehydrated component</span>
24-
</div>
25-
</div>"
7+
<div class=\\"rehydration-root\\"><span>
8+
<div class=\\"rehydration-root\\"><span>
9+
Hello, World!
10+
</span></div>
11+
</span></div>
12+
"
2613
`;

src/__tests__/tests.ts

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* eslint-env jest */
22
import * as React from "react";
3-
import reactFromMarkupContainer from "..";
3+
import reactFromHtml from "..";
44

5-
describe("reactFromMarkupContainer E2E tests", async () => {
5+
describe("reactFromHtml E2E tests", async () => {
66
it("Should rehydrate a basic component", async () => {
77
const componentName: string = "myComponent";
88

@@ -13,63 +13,46 @@ describe("reactFromMarkupContainer E2E tests", async () => {
1313
const rehydrators = { [componentName]: rehydrator };
1414
const documentElement = document.createElement("div");
1515

16-
documentElement.innerHTML = `
17-
<div data-react-from-html-container>
18-
<div data-rehydratable="${componentName}"></div>
19-
</div>`;
16+
documentElement.innerHTML = `<div data-rehydratable="${componentName}"></div>`;
2017

21-
await reactFromMarkupContainer(documentElement, rehydrators, {
18+
await reactFromHtml(documentElement, rehydrators, {
2219
extra: {},
2320
});
2421

2522
expect(documentElement.innerHTML).toMatchSnapshot();
2623
});
2724

28-
it("Should rehydrate valid HTML markup", async () => {
29-
const documentElement = document.createElement("div");
30-
31-
documentElement.innerHTML = `
32-
<div data-react-from-html-container>
33-
<p>paragraph</p>
34-
</div>`;
35-
36-
await reactFromMarkupContainer(documentElement, {}, { extra: {} });
37-
38-
expect(documentElement.innerHTML).toMatchSnapshot();
39-
});
40-
41-
it("Should work for nested markup containers", async () => {
25+
it("Should work for nested rehydratables", async () => {
4226
const componentName: string = "mycomponentName";
4327

4428
const mockCall = jest.fn();
4529
const rehydrators = {
46-
[componentName]: async () => {
30+
[componentName]: async (node: HTMLElement) => {
4731
mockCall();
4832

49-
return React.createElement("span", {}, "rehydrated component");
33+
await reactFromHtml(node, rehydrators, { extra: {} });
34+
35+
return React.createElement("span", {
36+
dangerouslySetInnerHTML: { __html: node.innerHTML },
37+
});
5038
},
5139
};
5240

5341
const documentElement = document.createElement("div");
5442

5543
documentElement.innerHTML = `
56-
<div data-react-from-html-container>
57-
<div data-rehydratable="${componentName}"></div>
58-
<div data-react-from-html-container>
59-
<div data-rehydratable="${componentName}">
60-
<div data-react-from-html-container>
61-
<div data-rehydratable="${componentName}"></div>
62-
</div>
63-
</div>
64-
<div data-rehydratable="${componentName}"></div>
65-
</div>
66-
</div>`;
67-
68-
await reactFromMarkupContainer(documentElement, rehydrators, {
44+
<div data-rehydratable="${componentName}">
45+
<div data-rehydratable="${componentName}">
46+
Hello, World!
47+
</div>
48+
</div>
49+
`;
50+
51+
await reactFromHtml(documentElement, rehydrators, {
6952
extra: {},
7053
});
7154

7255
expect(documentElement.innerHTML).toMatchSnapshot();
73-
expect(mockCall).toBeCalledTimes(3);
56+
expect(mockCall).toBeCalledTimes(2);
7457
});
7558
});

src/rehydrator.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const rehydratableToReactElement = async (
2323

2424
return rehydrator(
2525
el,
26-
children => rehydrateChildren(children, rehydrators, options),
26+
async children =>
27+
(await rehydrateChildren(children, rehydrators, options)).rehydrated,
2728
options.extra
2829
);
2930
};
@@ -44,11 +45,34 @@ const createCustomHandler = (
4445
return false;
4546
};
4647

47-
const rehydrateChildren = (
48+
const createReactRoot = (el: Node) => {
49+
const container = document.createElement("div");
50+
51+
if (el.parentNode) {
52+
el.parentNode.replaceChild(container, el);
53+
}
54+
55+
container.appendChild(el);
56+
container.classList.add("rehydration-root");
57+
58+
return container;
59+
};
60+
61+
const rehydrateChildren = async (
4862
el: Node,
4963
rehydrators: IRehydrator,
5064
options: IOptions
51-
) => domElementToReact(el, createCustomHandler(rehydrators, options));
65+
) => {
66+
const container = createReactRoot(el);
67+
68+
return {
69+
container,
70+
rehydrated: await domElementToReact(
71+
container,
72+
createCustomHandler(rehydrators, options)
73+
),
74+
};
75+
};
5276

5377
const render = ({
5478
rehydrated,
@@ -67,14 +91,23 @@ const render = ({
6791
ReactDOM.render(rehydrated as React.ReactElement<any>, root);
6892
};
6993

94+
const createQuerySelector = (rehydratableIds: string[]) =>
95+
rehydratableIds.reduce(
96+
(acc: string, rehydratableId: string) =>
97+
`${acc ? `${acc}, ` : ""}[data-rehydratable*="${rehydratableId}"]`,
98+
""
99+
);
100+
70101
export default async (
71102
container: Element,
72103
rehydrators: IRehydrator,
73104
options: IOptions
74105
) => {
106+
const selector = createQuerySelector(Object.keys(rehydrators));
107+
75108
const roots = Array.from(
76109
// TODO: allow setting a container identifier so multiple rehydration instances can exist
77-
container.querySelectorAll("[data-react-from-html-container]")
110+
container.querySelectorAll(selector)
78111
).reduce((acc: Element[], root: Element) => {
79112
// filter roots that are contained within other roots
80113
if (!acc.some(r => r.contains(root))) {
@@ -92,13 +125,12 @@ export default async (
92125
if (container.contains(root)) {
93126
renders.push(async () => {
94127
try {
95-
const rehydrated = await rehydrateChildren(
96-
root,
97-
rehydrators,
98-
options
99-
);
128+
const {
129+
container: rootContainer,
130+
rehydrated,
131+
} = await rehydrateChildren(root, rehydrators, options);
100132

101-
return { root, rehydrated };
133+
return { root: rootContainer, rehydrated };
102134
} catch (e) {
103135
/* tslint:disable-next-line no-console */
104136
console.error("Rehydration failure", e);

0 commit comments

Comments
 (0)