@lit/react
Version:
191 lines • 8.36 kB
JavaScript
/**
* @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