ce-la-react
Version:
Create a React component from a custom element.
143 lines (142 loc) • 4.68 kB
JavaScript
/**
* @license
* Copyright 2018 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*
* Modified version of `@lit/react` for vanilla custom elements with support for SSR.
*/
const reservedReactProps = /* @__PURE__ */ new Set([
"style",
"children",
"ref",
"key",
"suppressContentEditableWarning",
"suppressHydrationWarning",
"dangerouslySetInnerHTML"
]);
const reactPropToAttrNameMap = {
className: "class",
htmlFor: "for"
};
function defaultToAttributeName(propName) {
return propName.toLowerCase();
}
function defaultToAttributeValue(propValue) {
if (typeof propValue === "boolean") return propValue ? "" : void 0;
if (typeof propValue === "function") return void 0;
if (typeof propValue === "object" && propValue !== null) return void 0;
return propValue;
}
function createComponent({
react: React,
tagName,
elementClass,
events,
displayName,
toAttributeName = defaultToAttributeName,
toAttributeValue = defaultToAttributeValue
}) {
const IS_REACT_19_OR_NEWER = Number.parseInt(React.version) >= 19;
const ReactComponent = React.forwardRef((props, ref) => {
const elementRef = React.useRef(null);
const prevElemPropsRef = React.useRef(/* @__PURE__ */ new Map());
const eventProps = {};
const attrs = {};
const reactProps = {};
const elementProps = {};
for (const [k, v] of Object.entries(props)) {
if (reservedReactProps.has(k)) {
reactProps[k] = v;
continue;
}
const attrName = toAttributeName(reactPropToAttrNameMap[k] ?? k);
if (k in elementClass.prototype && !(k in (globalThis.HTMLElement?.prototype ?? {})) && !elementClass.observedAttributes?.some((attr) => attr === attrName)) {
elementProps[k] = v;
continue;
}
if (k.startsWith("on")) {
eventProps[k] = v;
continue;
}
const attrValue = toAttributeValue(v);
if (attrName && attrValue != null) {
attrs[attrName] = String(attrValue);
if (!IS_REACT_19_OR_NEWER) {
reactProps[attrName] = attrValue;
}
}
if (attrName && IS_REACT_19_OR_NEWER) {
reactProps[attrName] = v;
}
}
if (typeof window !== "undefined") {
for (const propName in eventProps) {
const callback = eventProps[propName];
const useCapture = propName.endsWith("Capture");
const eventName = (events?.[propName] ?? propName.slice(2).toLowerCase()).slice(
0,
useCapture ? -7 : void 0
);
React.useLayoutEffect(() => {
const eventTarget = elementRef?.current;
if (!eventTarget || typeof callback !== "function") return;
eventTarget.addEventListener(eventName, callback, useCapture);
return () => {
eventTarget.removeEventListener(eventName, callback, useCapture);
};
}, [elementRef?.current, callback]);
}
React.useLayoutEffect(() => {
if (elementRef.current === null) return;
const newElemProps = /* @__PURE__ */ new Map();
for (const key in elementProps) {
setProperty(elementRef.current, key, elementProps[key]);
prevElemPropsRef.current.delete(key);
newElemProps.set(key, elementProps[key]);
}
for (const [key, _value] of prevElemPropsRef.current) {
setProperty(elementRef.current, key, void 0);
}
prevElemPropsRef.current = newElemProps;
});
}
if (typeof window === "undefined" && elementClass?.getTemplateHTML && elementClass?.shadowRootOptions) {
const { mode, delegatesFocus } = elementClass.shadowRootOptions;
const templateShadowRoot = React.createElement("template", {
shadowrootmode: mode,
shadowrootdelegatesfocus: delegatesFocus,
dangerouslySetInnerHTML: {
__html: elementClass.getTemplateHTML(attrs, props)
}
});
reactProps.children = [templateShadowRoot, reactProps.children];
}
return React.createElement(tagName, {
...reactProps,
ref: React.useCallback(
(node) => {
elementRef.current = node;
if (typeof ref === "function") {
ref(node);
} else if (ref !== null) {
ref.current = node;
}
},
[ref]
)
});
});
ReactComponent.displayName = displayName ?? elementClass.name;
return ReactComponent;
}
function setProperty(node, name, value) {
node[name] = value;
if (value == null && name in (globalThis.HTMLElement?.prototype ?? {})) {
node.removeAttribute(name);
}
}
export {
createComponent,
defaultToAttributeName,
defaultToAttributeValue
};