1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
import { createElement, startTransition } from 'react';
import { type Root, createRoot, hydrateRoot } from 'react-dom/client';
import StaticHtml from './static-html.js';
function isAlreadyHydrated(element: HTMLElement) {
for (const key in element) {
if (key.startsWith('__reactContainer')) {
return key as keyof HTMLElement;
}
}
}
function createReactElementFromDOMElement(element: any): any {
let attrs: Record<string, string> = {};
for (const attr of element.attributes) {
attrs[attr.name] = attr.value;
}
// If the element has no children, we can create a simple React element
if (element.firstChild === null) {
return createElement(element.localName, attrs);
}
return createElement(
element.localName,
attrs,
Array.from(element.childNodes)
.map((c: any) => {
if (c.nodeType === Node.TEXT_NODE) {
return c.data;
} else if (c.nodeType === Node.ELEMENT_NODE) {
return createReactElementFromDOMElement(c);
} else {
return undefined;
}
})
.filter((a) => !!a),
);
}
function getChildren(childString: string, experimentalReactChildren: boolean) {
if (experimentalReactChildren && childString) {
let children = [];
let template = document.createElement('template');
template.innerHTML = childString;
for (let child of template.content.children) {
children.push(createReactElementFromDOMElement(child));
}
return children;
} else if (childString) {
return createElement(StaticHtml, { value: childString });
} else {
return undefined;
}
}
// Keep a map of roots so we can reuse them on re-renders
let rootMap = new WeakMap<HTMLElement, Root>();
const getOrCreateRoot = (element: HTMLElement, creator: () => Root) => {
let root = rootMap.get(element);
if (!root) {
root = creator();
rootMap.set(element, root);
}
return root;
};
export default (element: HTMLElement) =>
(
Component: any,
props: Record<string, any>,
{ default: children, ...slotted }: Record<string, any>,
{ client }: Record<string, string>,
) => {
if (!element.hasAttribute('ssr')) return;
const actionKey = element.getAttribute('data-action-key');
const actionName = element.getAttribute('data-action-name');
const stringifiedActionResult = element.getAttribute('data-action-result');
const formState =
actionKey && actionName && stringifiedActionResult
? [JSON.parse(stringifiedActionResult), actionKey, actionName]
: undefined;
const renderOptions = {
identifierPrefix: element.getAttribute('prefix'),
formState,
};
for (const [key, value] of Object.entries(slotted)) {
props[key] = createElement(StaticHtml, { value, name: key });
}
const componentEl = createElement(
Component,
props,
getChildren(children, element.hasAttribute('data-react-children')),
);
const rootKey = isAlreadyHydrated(element);
// HACK: delete internal react marker for nested components to suppress aggressive warnings
if (rootKey) {
delete element[rootKey];
}
if (client === 'only') {
return startTransition(() => {
const root = getOrCreateRoot(element, () => {
const r = createRoot(element);
element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
return r;
});
root.render(componentEl);
});
}
startTransition(() => {
const root = getOrCreateRoot(element, () => {
const r = hydrateRoot(element, componentEl, renderOptions as any);
element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
return r;
});
root.render(componentEl);
});
};
|