Skip to content

Commit

Permalink
feat: support custom rehydratable query selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Sep 16, 2020
1 parent 0f13d96 commit 83cec13
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 22 deletions.
7 changes: 7 additions & 0 deletions src/ILoadedOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface IOptions {
allSelectors: { [key: string]: string };
compoundSelector: string;
extra: object;
}

export default IOptions;
1 change: 1 addition & 0 deletions src/IOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
interface IOptions {
extra: object;
getQuerySelector?: (key: string) => string;
}

export default IOptions;
2 changes: 2 additions & 0 deletions src/__tests__/__snapshots__/tests.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

exports[`reactFromHtml E2E tests Should rehydrate a basic component 1`] = `"<div class=\\"rehydration-root\\"><span>rehydrated component</span></div>"`;
exports[`reactFromHtml E2E tests Should rehydrate components with custom query selectors 1`] = `"<div class=\\"rehydration-root\\"><span>rehydrated component</span></div>"`;
exports[`reactFromHtml E2E tests Should work for nested rehydratables 1`] = `
"
<div class=\\"rehydration-root\\"><span>
Expand Down
20 changes: 20 additions & 0 deletions src/__tests__/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,24 @@ describe("reactFromHtml E2E tests", async () => {
expect(documentElement.innerHTML).toMatchSnapshot();
expect(mockCall).toBeCalledTimes(2);
});

it("Should rehydrate components with custom query selectors", async () => {
const componentName: string = "myComponent";

const rehydrator = async () => {
return React.createElement("span", {}, "rehydrated component");
};

const rehydrators = { [componentName]: rehydrator };
const documentElement = document.createElement("div");

documentElement.innerHTML = `<div class="test-${componentName}"></div>`;

await reactFromHtml(documentElement, rehydrators, {
extra: {},
getQuerySelector: key => `.test-${key}`,
});

expect(documentElement.innerHTML).toMatchSnapshot();
});
});
78 changes: 56 additions & 22 deletions src/rehydrator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import * as ReactDOM from "react-dom";

import domElementToReact from "./dom-element-to-react";
import ILoadedOptions from "./ILoadedOptions";
import IOptions from "./IOptions";
import IRehydrator from "./IRehydrator";

const rehydratableToReactElement = async (
el: Element,
rehydrators: IRehydrator,
options: IOptions
options: ILoadedOptions
): Promise<React.ReactElement<any>> => {
const rehydratorName = el.getAttribute("data-rehydratable");
const rehydratorSelector = Object.keys(options.allSelectors).find(selector =>
el.matches(selector)
);

if (!rehydratorSelector) {
throw new Error("No rehydrator selector matched the element.");
}

const rehydratorName = options.allSelectors[rehydratorSelector];

if (!rehydratorName) {
throw new Error("Rehydrator name is missing from element.");
Expand All @@ -33,13 +42,13 @@ const rehydratableToReactElement = async (

const createCustomHandler = (
rehydrators: IRehydrator,
options: IOptions
options: ILoadedOptions
) => async (node: Node) => {
// This function will run on _every_ node that domElementToReact encounters.
// Make sure to keep the conditional highly performant.
if (
node.nodeType === Node.ELEMENT_NODE &&
(node as Element).hasAttribute("data-rehydratable")
(node as Element).matches(options.compoundSelector)
) {
return rehydratableToReactElement(node as Element, rehydrators, options);
}
Expand All @@ -63,7 +72,7 @@ const createReactRoot = (el: Node) => {
const rehydrateChildren = async (
el: Node,
rehydrators: IRehydrator,
options: IOptions
options: ILoadedOptions
) => {
const container = createReactRoot(el);

Expand Down Expand Up @@ -93,30 +102,55 @@ 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}"]`,
const defaultGetQuerySelector = (key: string) =>
`[data-rehydratable*="${key}"]`;

const createQuerySelectors = (
rehydratableIds: string[],
getQuerySelector: ((key: string) => string) = defaultGetQuerySelector
) => {
const allSelectors: { [key: string]: string } = rehydratableIds.reduce(
(acc, key) => ({ ...acc, [getQuerySelector(key)]: key }),
{}
);

const compoundSelector = Object.keys(allSelectors).reduce(
(acc: string, selector: string) => `${acc ? `${acc}, ` : ""}${selector}`,
""
);

return {
allSelectors,
compoundSelector,
};
};

const rehydrate = 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(selector)
).reduce((acc: Element[], root: Element) => {
// filter roots that are contained within other roots
if (!acc.some(r => r.contains(root))) {
acc.push(root);
}
return acc;
}, []);
const { allSelectors, compoundSelector } = createQuerySelectors(
Object.keys(rehydrators),
options.getQuerySelector
);

const loadedOptions: ILoadedOptions = {
allSelectors,
compoundSelector,
extra: options.extra,
};

const roots = Array.from(container.querySelectorAll(compoundSelector)).reduce(
(acc: Element[], root: Element) => {
// filter roots that are contained within other roots
if (!acc.some(r => r.contains(root))) {
acc.push(root);
}
return acc;
},
[]
);

// TODO: solve race condition when a second rehydrate runs

Expand All @@ -130,7 +164,7 @@ const rehydrate = async (
const {
container: rootContainer,
rehydrated,
} = await rehydrateChildren(root, rehydrators, options);
} = await rehydrateChildren(root, rehydrators, loadedOptions);

return { root: rootContainer, rehydrated };
} catch (e) {
Expand Down

0 comments on commit 83cec13

Please sign in to comment.