Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 8 additions & 21 deletions src/__tests__/__snapshots__/tests.ts.snap
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`reactFromMarkupContainer E2E tests Should rehydrate a basic component 1`] = `
"
<div data-react-from-html-container=\\"\\">
<span>rehydrated component</span>
</div>"
`;

exports[`reactFromMarkupContainer E2E tests Should rehydrate valid HTML markup 1`] = `
"
<div data-react-from-html-container=\\"\\">
<p>paragraph</p>
</div>"
`;
exports[`reactFromHtml E2E tests Should rehydrate a basic component 1`] = `"<div class=\\"rehydration-root\\"><span>rehydrated component</span></div>"`;

exports[`reactFromMarkupContainer E2E tests Should work for nested markup containers 1`] = `
exports[`reactFromHtml E2E tests Should work for nested rehydratables 1`] = `
"
<div data-react-from-html-container=\\"\\">
<span>rehydrated component</span>
<div data-react-from-html-container=\\"\\">
<span>rehydrated component</span>
<span>rehydrated component</span>
</div>
</div>"
<div class=\\"rehydration-root\\"><span>
<div class=\\"rehydration-root\\"><span>
Hello, World!
</span></div>
</span></div>
"
`;
57 changes: 20 additions & 37 deletions src/__tests__/tests.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-env jest */
import * as React from "react";
import reactFromMarkupContainer from "..";
import reactFromHtml from "..";

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

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

documentElement.innerHTML = `
<div data-react-from-html-container>
<div data-rehydratable="${componentName}"></div>
</div>`;
documentElement.innerHTML = `<div data-rehydratable="${componentName}"></div>`;

await reactFromMarkupContainer(documentElement, rehydrators, {
await reactFromHtml(documentElement, rehydrators, {
extra: {},
});

expect(documentElement.innerHTML).toMatchSnapshot();
});

it("Should rehydrate valid HTML markup", async () => {
const documentElement = document.createElement("div");

documentElement.innerHTML = `
<div data-react-from-html-container>
<p>paragraph</p>
</div>`;

await reactFromMarkupContainer(documentElement, {}, { extra: {} });

expect(documentElement.innerHTML).toMatchSnapshot();
});

it("Should work for nested markup containers", async () => {
it("Should work for nested rehydratables", async () => {
const componentName: string = "mycomponentName";

const mockCall = jest.fn();
const rehydrators = {
[componentName]: async () => {
[componentName]: async (node: HTMLElement) => {
mockCall();

return React.createElement("span", {}, "rehydrated component");
await reactFromHtml(node, rehydrators, { extra: {} });

return React.createElement("span", {
dangerouslySetInnerHTML: { __html: node.innerHTML },
});
},
};

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

documentElement.innerHTML = `
<div data-react-from-html-container>
<div data-rehydratable="${componentName}"></div>
<div data-react-from-html-container>
<div data-rehydratable="${componentName}">
<div data-react-from-html-container>
<div data-rehydratable="${componentName}"></div>
</div>
</div>
<div data-rehydratable="${componentName}"></div>
</div>
</div>`;

await reactFromMarkupContainer(documentElement, rehydrators, {
<div data-rehydratable="${componentName}">
<div data-rehydratable="${componentName}">
Hello, World!
</div>
</div>
`;

await reactFromHtml(documentElement, rehydrators, {
extra: {},
});

expect(documentElement.innerHTML).toMatchSnapshot();
expect(mockCall).toBeCalledTimes(3);
expect(mockCall).toBeCalledTimes(2);
});
});
52 changes: 42 additions & 10 deletions src/rehydrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const rehydratableToReactElement = async (

return rehydrator(
el,
children => rehydrateChildren(children, rehydrators, options),
async children =>
(await rehydrateChildren(children, rehydrators, options)).rehydrated,
options.extra
);
};
Expand All @@ -44,11 +45,34 @@ const createCustomHandler = (
return false;
};

const rehydrateChildren = (
const createReactRoot = (el: Node) => {
const container = document.createElement("div");

if (el.parentNode) {
el.parentNode.replaceChild(container, el);
}

container.appendChild(el);
container.classList.add("rehydration-root");

return container;
};

const rehydrateChildren = async (
el: Node,
rehydrators: IRehydrator,
options: IOptions
) => domElementToReact(el, createCustomHandler(rehydrators, options));
) => {
const container = createReactRoot(el);

return {
container,
rehydrated: await domElementToReact(
container,
createCustomHandler(rehydrators, options)
),
};
};

const render = ({
rehydrated,
Expand All @@ -67,14 +91,23 @@ const render = ({
ReactDOM.render(rehydrated as React.ReactElement<any>, root);
};

const createQuerySelector = (rehydratableIds: string[]) =>
rehydratableIds.reduce(
(acc: string, rehydratableId: string) =>
`${acc ? `${acc}, ` : ""}[data-rehydratable*="${rehydratableId}"]`,
""
);

export default async (
container: Element,
rehydrators: IRehydrator,
options: IOptions
) => {
const selector = createQuerySelector(Object.keys(rehydrators));

const roots = Array.from(
// TODO: allow setting a container identifier so multiple rehydration instances can exist
container.querySelectorAll("[data-react-from-html-container]")
container.querySelectorAll(selector)
).reduce((acc: Element[], root: Element) => {
// filter roots that are contained within other roots
if (!acc.some(r => r.contains(root))) {
Expand All @@ -92,13 +125,12 @@ export default async (
if (container.contains(root)) {
renders.push(async () => {
try {
const rehydrated = await rehydrateChildren(
root,
rehydrators,
options
);
const {
container: rootContainer,
rehydrated,
} = await rehydrateChildren(root, rehydrators, options);

return { root, rehydrated };
return { root: rootContainer, rehydrated };
} catch (e) {
/* tslint:disable-next-line no-console */
console.error("Rehydration failure", e);
Expand Down