Skip to content

Commit 27b8f22

Browse files
committed
feat: support custom rehydratable query selectors
1 parent 86fcd18 commit 27b8f22

File tree

2 files changed

+57
-22
lines changed

2 files changed

+57
-22
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
interface IOptions {
22
extra: object;
3+
getQuerySelector?: (key: string) => string;
34
}
45

56
export default IOptions;

packages/react-from-markup/src/rehydrator.ts

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import domElementToReact from "dom-element-to-react";
22
import * as ReactDOM from "react-dom";
33

4+
import ILoadedOptions from "./ILoadedOptions";
45
import IOptions from "./IOptions";
56
import IRehydrator from "./IRehydrator";
67

78
const rehydratableToReactElement = async (
89
el: Element,
910
rehydrators: IRehydrator,
10-
options: IOptions
11+
options: ILoadedOptions
1112
): Promise<React.ReactElement<any>> => {
12-
const rehydratorName = el.getAttribute("data-rehydratable");
13+
const rehydratorSelector = Object.keys(options.allSelectors).find(selector =>
14+
el.matches(selector)
15+
);
16+
17+
if (!rehydratorSelector) {
18+
throw new Error("No rehydrator selector matched the element.");
19+
}
20+
21+
const rehydratorName = options.allSelectors[rehydratorSelector];
1322

1423
if (!rehydratorName) {
1524
throw new Error("Rehydrator name is missing from element.");
@@ -31,13 +40,13 @@ const rehydratableToReactElement = async (
3140

3241
const createCustomHandler = (
3342
rehydrators: IRehydrator,
34-
options: IOptions
43+
options: ILoadedOptions
3544
) => async (node: Node) => {
3645
// This function will run on _every_ node that domElementToReact encounters.
3746
// Make sure to keep the conditional highly performant.
3847
if (
3948
node.nodeType === Node.ELEMENT_NODE &&
40-
(node as Element).hasAttribute("data-rehydratable")
49+
(node as Element).matches(options.compoundSelector)
4150
) {
4251
return rehydratableToReactElement(node as Element, rehydrators, options);
4352
}
@@ -61,7 +70,7 @@ const createReactRoot = (el: Node) => {
6170
const rehydrateChildren = async (
6271
el: Node,
6372
rehydrators: IRehydrator,
64-
options: IOptions
73+
options: ILoadedOptions
6574
) => {
6675
const container = createReactRoot(el);
6776

@@ -91,30 +100,55 @@ const render = ({
91100
ReactDOM.render(rehydrated as React.ReactElement<any>, root);
92101
};
93102

94-
const createQuerySelector = (rehydratableIds: string[]) =>
95-
rehydratableIds.reduce(
96-
(acc: string, rehydratableId: string) =>
97-
`${acc ? `${acc}, ` : ""}[data-rehydratable*="${rehydratableId}"]`,
103+
const defaultGetQuerySelector = (key: string) =>
104+
`[data-rehydratable*="${key}"]`;
105+
106+
const createQuerySelectors = (
107+
rehydratableIds: string[],
108+
getQuerySelector: ((key: string) => string) = defaultGetQuerySelector
109+
) => {
110+
const allSelectors: { [key: string]: string } = rehydratableIds.reduce(
111+
(acc, key) => ({ ...acc, [getQuerySelector(key)]: key }),
112+
{}
113+
);
114+
115+
const compoundSelector = Object.keys(allSelectors).reduce(
116+
(acc: string, selector: string) => `${acc ? `${acc}, ` : ""}${selector}`,
98117
""
99118
);
100119

120+
return {
121+
allSelectors,
122+
compoundSelector
123+
};
124+
};
125+
101126
export default async (
102127
container: Element,
103128
rehydrators: IRehydrator,
104129
options: IOptions
105130
) => {
106-
const selector = createQuerySelector(Object.keys(rehydrators));
107-
108-
const roots = Array.from(
109-
// TODO: allow setting a container identifier so multiple rehydration instances can exist
110-
container.querySelectorAll(selector)
111-
).reduce((acc: Element[], root: Element) => {
112-
// filter roots that are contained within other roots
113-
if (!acc.some(r => r.contains(root))) {
114-
acc.push(root);
115-
}
116-
return acc;
117-
}, []);
131+
const { allSelectors, compoundSelector } = createQuerySelectors(
132+
Object.keys(rehydrators),
133+
options.getQuerySelector
134+
);
135+
136+
const loadedOptions: ILoadedOptions = {
137+
allSelectors,
138+
compoundSelector,
139+
extra: options.extra
140+
};
141+
142+
const roots = Array.from(container.querySelectorAll(compoundSelector)).reduce(
143+
(acc: Element[], root: Element) => {
144+
// filter roots that are contained within other roots
145+
if (!acc.some(r => r.contains(root))) {
146+
acc.push(root);
147+
}
148+
return acc;
149+
},
150+
[]
151+
);
118152

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

@@ -128,7 +162,7 @@ export default async (
128162
const {
129163
container: rootContainer,
130164
rehydrated
131-
} = await rehydrateChildren(root, rehydrators, options);
165+
} = await rehydrateChildren(root, rehydrators, loadedOptions);
132166

133167
return { root: rootContainer, rehydrated };
134168
} catch (e) {

0 commit comments

Comments
 (0)