demarches-normaliennes/app/javascript/shared/register-react-components.tsx

98 lines
2.9 KiB
TypeScript
Raw Normal View History

2022-02-02 17:16:50 +01:00
import React, { Suspense, lazy, createElement, ComponentClass } from 'react';
2021-12-21 16:30:29 +01:00
import { render } from 'react-dom';
// This attribute holds the name of component which should be mounted
// example: `data-react-class="MyApp.Items.EditForm"`
const CLASS_NAME_ATTR = 'data-react-class';
// This attribute holds JSON stringified props for initializing the component
// example: `data-react-props="{\"item\": { \"id\": 1, \"name\": \"My Item\"} }"`
const PROPS_ATTR = 'data-react-props';
const CLASS_NAME_SELECTOR = `[${CLASS_NAME_ATTR}]`;
// helper method for the mount and unmount methods to find the
// `data-react-class` DOM elements
2022-02-02 17:16:50 +01:00
function findDOMNodes(searchSelector?: string): NodeListOf<HTMLDivElement> {
2021-12-21 16:30:29 +01:00
const [selector, parent] = getSelector(searchSelector);
2022-02-02 17:16:50 +01:00
return parent.querySelectorAll<HTMLDivElement>(selector);
2021-12-21 16:30:29 +01:00
}
2022-02-02 17:16:50 +01:00
function getSelector(searchSelector?: string): [string, Document] {
2021-12-21 16:30:29 +01:00
switch (typeof searchSelector) {
case 'undefined':
return [CLASS_NAME_SELECTOR, document];
case 'object':
return [CLASS_NAME_SELECTOR, searchSelector];
case 'string':
return [
['', ' ']
.map(
(separator) => `${searchSelector}${separator}${CLASS_NAME_SELECTOR}`
)
.join(', '),
document
];
}
}
class ReactComponentRegistry {
#components;
2022-02-02 17:16:50 +01:00
constructor(components: Record<string, ComponentClass>) {
2021-12-21 16:30:29 +01:00
this.#components = components;
}
2022-02-02 17:16:50 +01:00
getConstructor(className: string | null) {
return className ? this.#components[className] : null;
2021-12-21 16:30:29 +01:00
}
2022-02-02 17:16:50 +01:00
mountComponents(searchSelector?: string) {
2021-12-21 16:30:29 +01:00
const nodes = findDOMNodes(searchSelector);
for (const node of nodes) {
const className = node.getAttribute(CLASS_NAME_ATTR);
const ComponentClass = this.getConstructor(className);
const propsJson = node.getAttribute(PROPS_ATTR);
const props = propsJson && JSON.parse(propsJson);
if (!ComponentClass) {
const message = `Cannot find component: "${className}"`;
console?.log(
`%c[react-rails] %c${message} for element`,
'font-weight: bold',
'',
node
);
throw new Error(
`${message}. Make sure your component is available to render.`
);
} else {
render(createElement(ComponentClass, props), node);
2021-12-21 16:30:29 +01:00
}
}
}
}
const Loader = () => <div className="spinner left" />;
2022-02-02 17:16:50 +01:00
export function Loadable(loader: () => Promise<{ default: ComponentClass }>) {
2021-12-21 16:30:29 +01:00
const LazyComponent = lazy(loader);
2022-02-02 17:16:50 +01:00
return function PureComponent(props: Record<string, unknown>) {
2021-12-21 16:30:29 +01:00
return (
<Suspense fallback={<Loader />}>
<LazyComponent {...props} />
</Suspense>
);
};
}
2022-02-02 17:16:50 +01:00
export function registerReactComponents(
components: Record<string, ComponentClass>
) {
2021-12-21 16:30:29 +01:00
const registry = new ReactComponentRegistry(components);
addEventListener('ds:page:update', () => registry.mountComponents());
}