UNPKG

@lit/react

Version:

A React component wrapper for web components.

191 lines 8.36 kB
/** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const NODE_MODE = false; const DEV_MODE = true; const reservedReactProperties = new Set([ 'children', 'localName', 'ref', 'style', 'className', ]); const listenedEvents = new WeakMap(); /** * Adds an event listener for the specified event to the given node. In the * React setup, there should only ever be one event listener. Thus, for * efficiency only one listener is added and the handler for that listener is * updated to point to the given listener function. */ const addOrUpdateEventListener = (node, event, listener) => { let events = listenedEvents.get(node); if (events === undefined) { listenedEvents.set(node, (events = new Map())); } let handler = events.get(event); if (listener !== undefined) { // If necessary, add listener and track handler if (handler === undefined) { events.set(event, (handler = { handleEvent: listener })); node.addEventListener(event, handler); // Otherwise just update the listener with new value } else { handler.handleEvent = listener; } // Remove listener if one exists and value is undefined } else if (handler !== undefined) { events.delete(event); node.removeEventListener(event, handler); } }; /** * Sets properties and events on custom elements. These properties and events * have been pre-filtered so we know they should apply to the custom element. */ const setProperty = (node, name, value, old, events) => { const event = events?.[name]; // Dirty check event value. if (event !== undefined) { if (value !== old) { addOrUpdateEventListener(node, event, value); } return; } // But don't dirty check properties; elements are assumed to do this. node[name] = value; // This block is to replicate React's behavior for attributes of native // elements where `undefined` or `null` values result in attributes being // removed. // https://github.com/facebook/react/blob/899cb95f52cc83ab5ca1eb1e268c909d3f0961e7/packages/react-dom-bindings/src/client/DOMPropertyOperations.js#L107-L141 // // It's only needed here for native HTMLElement properties that reflect // attributes of the same name but don't have that behavior like "id" or // "draggable". if ((value === undefined || value === null) && name in HTMLElement.prototype) { node.removeAttribute(name); } }; /** * Creates a React component for a custom element. Properties are distinguished * from attributes automatically, and events can be configured so they are added * to the custom element as event listeners. * * @param options An options bag containing the parameters needed to generate a * wrapped web component. * * @param options.react The React module, typically imported from the `react` * npm package. * @param options.tagName The custom element tag name registered via * `customElements.define`. * @param options.elementClass The custom element class registered via * `customElements.define`. * @param options.events An object listing events to which the component can * listen. The object keys are the event property names passed in via React * props and the object values are the names of the corresponding events * generated by the custom element. For example, given `{onactivate: * 'activate'}` an event function may be passed via the component's `onactivate` * prop and will be called when the custom element fires its `activate` event. * @param options.displayName A React component display name, used in debugging * messages. Default value is inferred from the name of custom element class * registered via `customElements.define`. */ export const createComponent = ({ react: React, tagName, elementClass, events, displayName, }) => { const eventProps = new Set(Object.keys(events ?? {})); if (DEV_MODE && !NODE_MODE) { for (const p of reservedReactProperties) { if (p in elementClass.prototype && !(p in HTMLElement.prototype)) { // Note, this effectively warns only for `ref` since the other // reserved props are on HTMLElement.prototype. To address this // would require crawling down the prototype, which doesn't feel worth // it since implementing these properties on an element is extremely // rare. console.warn(`${tagName} contains property ${p} which is a React reserved ` + `property. It will be used by React and not set on the element.`); } } } const ReactComponent = React.forwardRef((props, ref) => { const prevElemPropsRef = React.useRef(new Map()); const elementRef = React.useRef(null); // Props to be passed to React.createElement const reactProps = {}; // Props to be set on element with setProperty const elementProps = {}; for (const [k, v] of Object.entries(props)) { if (reservedReactProperties.has(k)) { // React does *not* handle `className` for custom elements so // coerce it to `class` so it's handled correctly. reactProps[k === 'className' ? 'class' : k] = v; continue; } if (eventProps.has(k) || k in elementClass.prototype) { elementProps[k] = v; continue; } reactProps[k] = v; } // useLayoutEffect produces warnings during server rendering. if (!NODE_MODE) { // This one has no dependency array so it'll run on every re-render. React.useLayoutEffect(() => { if (elementRef.current === null) { return; } const newElemProps = new Map(); for (const key in elementProps) { setProperty(elementRef.current, key, props[key], prevElemPropsRef.current.get(key), events); prevElemPropsRef.current.delete(key); newElemProps.set(key, props[key]); } // "Unset" any props from previous render that no longer exist. // Setting to `undefined` seems like the correct thing to "unset" // but currently React will set it as `null`. // See https://github.com/facebook/react/issues/28203 for (const [key, value] of prevElemPropsRef.current) { setProperty(elementRef.current, key, undefined, value, events); } prevElemPropsRef.current = newElemProps; }); // Empty dependency array so this will only run once after first render. React.useLayoutEffect(() => { elementRef.current?.removeAttribute('defer-hydration'); }, []); } if (NODE_MODE) { // If component is to be server rendered with `@lit/ssr-react`, pass // element properties in a special bag to be set by the server-side // element renderer. if ((React.createElement.name === 'litPatchedCreateElement' || globalThis.litSsrReactEnabled) && Object.keys(elementProps).length) { // This property needs to remain unminified. reactProps['_$litProps$'] = elementProps; } } else { // Suppress hydration warning for server-rendered attributes. // This property needs to remain unminified. reactProps['suppressHydrationWarning'] = true; } 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; }; //# sourceMappingURL=create-component.js.map