All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.create-component.js.map Maven / Gradle / Ivy

The newest version!
{"version":3,"file":"create-component.js","sources":["src/create-component.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\n\nimport type React from 'react';\n\nconst NODE_MODE = false;\nconst DEV_MODE = true;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype DistributiveOmit = T extends any\n  ? K extends keyof T\n    ? Omit\n    : T\n  : T;\ntype PropsWithoutRef = DistributiveOmit;\n\n/**\n * Creates a type to be used for the props of a web component used directly in\n * React JSX.\n *\n * Example:\n *\n * ```ts\n * declare module \"react\" {\n *   namespace JSX {\n *     interface IntrinsicElements {\n *       'x-foo': WebComponentProps;\n *     }\n *   }\n * }\n * ```\n */\nexport type WebComponentProps = React.DetailedHTMLProps<\n  React.HTMLAttributes,\n  I\n> &\n  ElementProps;\n\n/**\n * Type of the React component wrapping the web component. This is the return\n * type of `createComponent`.\n */\nexport type ReactWebComponent<\n  I extends HTMLElement,\n  E extends EventNames = {},\n> = React.ForwardRefExoticComponent<\n  // TODO(augustjk): Remove and use `React.PropsWithoutRef` when\n  // https://github.com/preactjs/preact/issues/4124 is fixed.\n  PropsWithoutRef> & React.RefAttributes\n>;\n\n// Props derived from custom element class. Currently has limitations of making\n// all properties optional and also surfaces life cycle methods in autocomplete.\n// TODO(augustjk) Consider omitting keyof LitElement to remove \"internal\"\n// lifecycle methods or allow user to explicitly provide props.\ntype ElementProps = Partial>;\n\n// Acceptable props to the React component.\ntype ComponentProps = Omit<\n  React.HTMLAttributes,\n  // Prefer type of provided event handler props or those on element over\n  // built-in HTMLAttributes\n  keyof E | keyof ElementProps\n> &\n  EventListeners &\n  ElementProps;\n\n/**\n * Type used to cast an event name with an event type when providing the\n * `events` option to `createComponent` for better typing of the event handler\n * prop.\n *\n * Example:\n *\n * ```ts\n * const FooComponent = createComponent({\n *   ...\n *   events: {\n *     onfoo: 'foo' as EventName,\n *   }\n * });\n * ```\n *\n * `onfoo` prop will have the type `(e: FooEvent) => void`.\n */\nexport type EventName = string & {\n  __eventType: T;\n};\n\n// A key value map matching React prop names to event names.\ntype EventNames = Record;\n\n// A map of expected event listener types based on EventNames.\ntype EventListeners = {\n  [K in keyof R]?: R[K] extends EventName\n    ? (e: R[K]['__eventType']) => void\n    : (e: Event) => void;\n};\n\nexport interface Options {\n  react: typeof React;\n  tagName: string;\n  elementClass: Constructor;\n  events?: E;\n  displayName?: string;\n}\n\ntype Constructor = {new (): T};\n\nconst reservedReactProperties = new Set([\n  'children',\n  'localName',\n  'ref',\n  'style',\n  'className',\n]);\n\nconst listenedEvents = new WeakMap>();\n\n/**\n * Adds an event listener for the specified event to the given node. In the\n * React setup, there should only ever be one event listener. Thus, for\n * efficiency only one listener is added and the handler for that listener is\n * updated to point to the given listener function.\n */\nconst addOrUpdateEventListener = (\n  node: Element,\n  event: string,\n  listener: (event?: Event) => void\n) => {\n  let events = listenedEvents.get(node);\n  if (events === undefined) {\n    listenedEvents.set(node, (events = new Map()));\n  }\n  let handler = events.get(event);\n  if (listener !== undefined) {\n    // If necessary, add listener and track handler\n    if (handler === undefined) {\n      events.set(event, (handler = {handleEvent: listener}));\n      node.addEventListener(event, handler);\n      // Otherwise just update the listener with new value\n    } else {\n      handler.handleEvent = listener;\n    }\n    // Remove listener if one exists and value is undefined\n  } else if (handler !== undefined) {\n    events.delete(event);\n    node.removeEventListener(event, handler);\n  }\n};\n\n/**\n * Sets properties and events on custom elements. These properties and events\n * have been pre-filtered so we know they should apply to the custom element.\n */\nconst setProperty = (\n  node: E,\n  name: string,\n  value: unknown,\n  old: unknown,\n  events?: EventNames\n) => {\n  const event = events?.[name];\n  // Dirty check event value.\n  if (event !== undefined) {\n    if (value !== old) {\n      addOrUpdateEventListener(node, event, value as (e?: Event) => void);\n    }\n    return;\n  }\n  // But don't dirty check properties; elements are assumed to do this.\n  node[name as keyof E] = value as E[keyof E];\n\n  // This block is to replicate React's behavior for attributes of native\n  // elements where `undefined` or `null` values result in attributes being\n  // removed.\n  // https://github.com/facebook/react/blob/899cb95f52cc83ab5ca1eb1e268c909d3f0961e7/packages/react-dom-bindings/src/client/DOMPropertyOperations.js#L107-L141\n  //\n  // It's only needed here for native HTMLElement properties that reflect\n  // attributes of the same name but don't have that behavior like \"id\" or\n  // \"draggable\".\n  if (\n    (value === undefined || value === null) &&\n    name in HTMLElement.prototype\n  ) {\n    node.removeAttribute(name);\n  }\n};\n\n/**\n * Creates a React component for a custom element. Properties are distinguished\n * from attributes automatically, and events can be configured so they are added\n * to the custom element as event listeners.\n *\n * @param options An options bag containing the parameters needed to generate a\n * wrapped web component.\n *\n * @param options.react The React module, typically imported from the `react`\n * npm package.\n * @param options.tagName The custom element tag name registered via\n * `customElements.define`.\n * @param options.elementClass The custom element class registered via\n * `customElements.define`.\n * @param options.events An object listing events to which the component can\n * listen. The object keys are the event property names passed in via React\n * props and the object values are the names of the corresponding events\n * generated by the custom element. For example, given `{onactivate:\n * 'activate'}` an event function may be passed via the component's `onactivate`\n * prop and will be called when the custom element fires its `activate` event.\n * @param options.displayName A React component display name, used in debugging\n * messages. Default value is inferred from the name of custom element class\n * registered via `customElements.define`.\n */\nexport const createComponent = <\n  I extends HTMLElement,\n  E extends EventNames = {},\n>({\n  react: React,\n  tagName,\n  elementClass,\n  events,\n  displayName,\n}: Options): ReactWebComponent => {\n  const eventProps = new Set(Object.keys(events ?? {}));\n\n  if (DEV_MODE) {\n    for (const p of reservedReactProperties) {\n      if (p in elementClass.prototype && !(p in HTMLElement.prototype)) {\n        // Note, this effectively warns only for `ref` since the other\n        // reserved props are on HTMLElement.prototype. To address this\n        // would require crawling down the prototype, which doesn't feel worth\n        // it since implementing these properties on an element is extremely\n        // rare.\n        console.warn(\n          `${tagName} contains property ${p} which is a React reserved ` +\n            `property. It will be used by React and not set on the element.`\n        );\n      }\n    }\n  }\n\n  type Props = ComponentProps;\n\n  const ReactComponent = React.forwardRef((props, ref) => {\n    const prevElemPropsRef = React.useRef(new Map());\n    const elementRef = React.useRef(null);\n\n    // Props to be passed to React.createElement\n    const reactProps: Record = {};\n    // Props to be set on element with setProperty\n    const elementProps: Record = {};\n\n    for (const [k, v] of Object.entries(props)) {\n      if (reservedReactProperties.has(k)) {\n        // React does *not* handle `className` for custom elements so\n        // coerce it to `class` so it's handled correctly.\n        reactProps[k === 'className' ? 'class' : k] = v;\n        continue;\n      }\n\n      if (eventProps.has(k) || k in elementClass.prototype) {\n        elementProps[k] = v;\n        continue;\n      }\n\n      reactProps[k] = v;\n    }\n\n    // useLayoutEffect produces warnings during server rendering.\n    if (!NODE_MODE) {\n      // This one has no dependency array so it'll run on every re-render.\n      React.useLayoutEffect(() => {\n        if (elementRef.current === null) {\n          return;\n        }\n        const newElemProps = new Map();\n        for (const key in elementProps) {\n          setProperty(\n            elementRef.current,\n            key,\n            props[key],\n            prevElemPropsRef.current.get(key),\n            events\n          );\n          prevElemPropsRef.current.delete(key);\n          newElemProps.set(key, props[key]);\n        }\n        // \"Unset\" any props from previous render that no longer exist.\n        // Setting to `undefined` seems like the correct thing to \"unset\"\n        // but currently React will set it as `null`.\n        // See https://github.com/facebook/react/issues/28203\n        for (const [key, value] of prevElemPropsRef.current) {\n          setProperty(elementRef.current, key, undefined, value, events);\n        }\n        prevElemPropsRef.current = newElemProps;\n      });\n\n      // Empty dependency array so this will only run once after first render.\n      React.useLayoutEffect(() => {\n        elementRef.current?.removeAttribute('defer-hydration');\n      }, []);\n    }\n\n    if (NODE_MODE) {\n      // If component is to be server rendered with `@lit/ssr-react`, pass\n      // element properties in a special bag to be set by the server-side\n      // element renderer.\n      if (\n        React.createElement.name === 'litPatchedCreateElement' &&\n        Object.keys(elementProps).length\n      ) {\n        // This property needs to remain unminified.\n        reactProps['_$litProps$'] = elementProps;\n      }\n    } else {\n      // Suppress hydration warning for server-rendered attributes.\n      // This property needs to remain unminified.\n      reactProps['suppressHydrationWarning'] = true;\n    }\n\n    return React.createElement(tagName, {\n      ...reactProps,\n      ref: React.useCallback(\n        (node: I) => {\n          elementRef.current = node;\n          if (typeof ref === 'function') {\n            ref(node);\n          } else if (ref !== null) {\n            ref.current = node;\n          }\n        },\n        [ref]\n      ),\n    });\n  });\n\n  ReactComponent.displayName = displayName ?? elementClass.name;\n\n  return ReactComponent;\n};\n"],"names":["reservedReactProperties","Set","listenedEvents","WeakMap","setProperty","node","name","value","old","events","event","undefined","HTMLElement","prototype","removeAttribute","listener","get","set","Map","handler","handleEvent","addEventListener","delete","removeEventListener","addOrUpdateEventListener","createComponent","react","React","tagName","elementClass","displayName","eventProps","Object","keys","ReactComponent","forwardRef","props","ref","prevElemPropsRef","useRef","elementRef","reactProps","elementProps","k","v","entries","has","useLayoutEffect","current","newElemProps","key","createElement","useCallback"],"mappings":";;;;;AAgHA,MAAMA,EAA0B,IAAIC,IAAI,CACtC,WACA,YACA,MACA,QACA,cAGIC,EAAiB,IAAIC,QAsCrBC,EAAc,CAClBC,EACAC,EACAC,EACAC,EACAC,KAEA,MAAMC,EAAQD,IAASH,QAETK,IAAVD,GAOJL,EAAKC,GAAmBC,EAWtB,MAACA,GACDD,KAAQM,YAAYC,WAEpBR,EAAKS,gBAAgBR,IApBjBC,IAAUC,GAxCe,EAC/BH,EACAK,EACAK,KAEA,IAAIN,EAASP,EAAec,IAAIX,QACjBM,IAAXF,GACFP,EAAee,IAAIZ,EAAOI,EAAS,IAAIS,KAEzC,IAAIC,EAAUV,EAAOO,IAAIN,QACRC,IAAbI,OAEcJ,IAAZQ,GACFV,EAAOQ,IAAIP,EAAQS,EAAU,CAACC,YAAaL,IAC3CV,EAAKgB,iBAAiBX,EAAOS,IAG7BA,EAAQC,YAAcL,OAGHJ,IAAZQ,IACTV,EAAOa,OAAOZ,GACdL,EAAKkB,oBAAoBb,EAAOS,GACjC,EAkBGK,CAAyBnB,EAAMK,EAAOH,EAoBzC,EA2BUkB,EAAkB,EAI7BC,MAAOC,EACPC,UACAC,eACApB,SACAqB,kBAEA,MAAMC,EAAa,IAAI9B,IAAI+B,OAAOC,KAAKxB,GAAU,CAAE,IAoB7CyB,EAAiBP,EAAMQ,YAAqB,CAACC,EAAOC,KACxD,MAAMC,EAAmBX,EAAMY,OAAO,IAAIrB,KACpCsB,EAAab,EAAMY,OAAiB,MAGpCE,EAAsC,CAAA,EAEtCC,EAAwC,CAAA,EAE9C,IAAK,MAAOC,EAAGC,KAAMZ,OAAOa,QAAQT,GAC9BpC,EAAwB8C,IAAIH,GAG9BF,EAAiB,cAANE,EAAoB,QAAUA,GAAKC,EAI5Cb,EAAWe,IAAIH,IAAMA,KAAKd,EAAahB,UACzC6B,EAAaC,GAAKC,EAIpBH,EAAWE,GAAKC,EAuDlB,OAjDEjB,EAAMoB,iBAAgB,KACpB,GAA2B,OAAvBP,EAAWQ,QACb,OAEF,MAAMC,EAAe,IAAI/B,IACzB,IAAK,MAAMgC,KAAOR,EAChBtC,EACEoC,EAAWQ,QACXE,EACAd,EAAMc,GACNZ,EAAiBU,QAAQhC,IAAIkC,GAC7BzC,GAEF6B,EAAiBU,QAAQ1B,OAAO4B,GAChCD,EAAahC,IAAIiC,EAAKd,EAAMc,IAM9B,IAAK,MAAOA,EAAK3C,KAAU+B,EAAiBU,QAC1C5C,EAAYoC,EAAWQ,QAASE,OAAKvC,EAAWJ,EAAOE,GAEzD6B,EAAiBU,QAAUC,CAAY,IAIzCtB,EAAMoB,iBAAgB,KACpBP,EAAWQ,SAASlC,gBAAgB,kBAAkB,GACrD,IAiBH2B,EAAqC,0BAAI,EAGpCd,EAAMwB,cAAcvB,EAAS,IAC/Ba,EACHJ,IAAKV,EAAMyB,aACR/C,IACCmC,EAAWQ,QAAU3C,EACF,mBAARgC,EACTA,EAAIhC,GACa,OAARgC,IACTA,EAAIW,QAAU3C,EACf,GAEH,CAACgC,KAEH,IAKJ,OAFAH,EAAeJ,YAAcA,GAAeD,EAAavB,KAElD4B,CAAc"}