{"version":3,"file":"react-angularjs-adapter.umd.cjs","sources":["../react-angularjs-adapter.ts"],"sourcesContent":["import angular from \"angular\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport ReactDOMClient from \"react-dom/client\";\n\nconst LOWERCASE_START = /^[a-z]/;\nconst INVALID_CHARACTERS = /[^a-zA-Z0-9]/g;\nconst CAMEL_TO_KEBAB_REGEXP = /[A-Z]/g;\nconst VALID_BINDINGS = /[@<]/;\n\nlet nextPortalId = 0;\nlet $defaultInjector: angular.auto.IInjectorService | undefined;\n\n/**\n * Set an $injector to use by default for the `angular2react` function\n */\nexport function setDefaultInjector($injector: angular.auto.IInjectorService | undefined) {\n  $defaultInjector = $injector;\n}\n\ninterface Adapter<Props> {\n  createPortalRoot(target: HTMLElement): ReactDOMClient.Root;\n  $scope: angular.IScope & { props?: Props };\n}\n\ninterface AugmentedHTMLElement<Props> extends HTMLElement {\n  __ReactAngularJSAdapter: Adapter<Props>;\n}\n\nfunction isAugmented(element: HTMLElement | null): element is AugmentedHTMLElement<unknown> {\n  return element !== null && \"__ReactAngularJSAdapter\" in element;\n}\n\nfunction logWarning(...messages: unknown[]) {\n  console.warn(\"react-angularjs-adapter:\", ...messages);\n}\n\nexport class ReactAngularJSAdapterError extends Error {\n  constructor(message: string) {\n    super(`react-angularjs-adapter: ${message}`);\n  }\n}\n\ntype OnChanges<T> = {\n  [K in keyof T]: angular.IChangesObject<T[K]>;\n};\n\n/**\n * Wraps an AngularJS component in React. Returns a new React component.\n *\n * @param componentName The name of the AngularJS component\n * @param component The AngularJS component definition\n * @param $injector The AngularJS `$injector` for the application the component is registered in.\n *                  You can omit this if you have used the `setDefaultInjector` function to set a\n *                  default injector.\n *\n * @example\n * ```tsx\n * import { angular2react, setDefaultInjector } from \"react-angularjs-adapter\"\n *\n * const angularComponent = {\n *   bindings: { fooBar: '<', baz: '<' },\n *   template: \"<p>FooBar: {this.$ctrl.fooBar}</p><p>Baz: {this.$ctrl.baz}</p>\"\n * }\n *\n * angular\n *   .module(\"myModule\", [])\n *   .component(\"angularComponent\", angularComponent)\n *\n * // Set the default injector for angular2react. This only needs to be done once.\n * angular.module(\"myModule\").run([\"$injector\", setDefaultInjector]);\n *\n * // Define the Prop types based on the component's bindings\n * interface Props {\n *   fooBar: number;\n *   baz: string;\n * }\n *\n * // Create the React component\n * const ReactComponent = angular2react<Props>('angularComponent', angularComponent, $injector);\n *\n * // Then in your JSX:\n * <ReactComponent fooBar={42} baz='lorem ipsum' />\n * ```\n */\nexport function angular2react<Props extends object = Record<string, unknown>>(\n  componentName: string,\n  component: angular.IComponentOptions,\n  $injector?: angular.auto.IInjectorService,\n): React.FunctionComponent<Props> {\n  function getInjector() {\n    if (typeof $injector !== \"undefined\") {\n      return $injector;\n    }\n    if (typeof $defaultInjector === \"undefined\") {\n      throw new ReactAngularJSAdapterError(\n        \"$injector is unset. Please pass an $injector to the angular2react function, or use the setDefaultInjector function to set a default one.\",\n      );\n    }\n    return $defaultInjector;\n  }\n\n  return function Component(props: Props) {\n    const elementRef = React.useRef<AugmentedHTMLElement<Props>>(null);\n    const portalsRef = React.useRef(\n      new Map<number, { target: HTMLElement; content: React.ReactNode }>(),\n    );\n    const [, forceRerender] = React.useReducer((x) => x + 1, 0);\n\n    const bindings = React.useMemo<Record<string, string>>(() => {\n      if (!component.bindings) {\n        return {};\n      }\n\n      if (Object.values(component.bindings).some((b) => !VALID_BINDINGS.test(b))) {\n        logWarning(\n          `${componentName} has '=' or '&' bindings which will not work properly with React:`,\n          component.bindings,\n        );\n      }\n\n      return Object.fromEntries(\n        Object.entries(component.bindings).map(([property, binding]) => [\n          kebabCase(property),\n          binding.includes(\"@\") ? `{{props.${property}}}` : `props.${property}`,\n        ]),\n      );\n    }, []);\n\n    React.useEffect(() => {\n      // Apply bindings as attributes to the element. We do this here instead of just setting them\n      // as props so that we can ensure they are the correct values. As an example of when they\n      // could be incorrect: in React strict mode during development, this Effect is run twice, so\n      // the element may have already had AngularJS set up and destroyed on it. This would cause\n      // interpolated strings in the attributes (which we use for '@' bindings) to be updated\n      // to their resolved string values, which would cause those bindings to no longer respond to\n      // changes. So to avoid issues like that, we set the attributes immediately before compiling.\n      for (const [key, value] of Object.entries(bindings)) {\n        elementRef.current!.setAttribute(key, value);\n      }\n\n      const $injector = getInjector();\n\n      // Set up new scope for the element\n      const $scope = $injector.get(\"$rootScope\").$new(true);\n\n      // Augment the element with our adapter object\n      (elementRef.current as AugmentedHTMLElement<Props>).__ReactAngularJSAdapter = {\n        createPortalRoot(target) {\n          const id = nextPortalId++;\n          return {\n            render(content) {\n              portalsRef.current.set(id, { target, content });\n              forceRerender();\n            },\n            unmount() {\n              portalsRef.current.delete(id);\n              forceRerender();\n            },\n          };\n        },\n        $scope,\n      };\n\n      // Finally, compile the element with our scope\n      $injector.get(\"$compile\")(elementRef.current!)($scope);\n\n      // Destroy scope on unmount\n      return () => {\n        $scope.$destroy();\n      };\n    }, []);\n\n    React.useEffect(() => {\n      // Update scope and digest after every render\n      if (elementRef.current) {\n        elementRef.current.__ReactAngularJSAdapter.$scope.props = writable(props);\n        elementRef.current.__ReactAngularJSAdapter.$scope.$digest();\n      }\n    });\n\n    return [\n      React.createElement(kebabCase(componentName), {\n        ref: elementRef,\n        key: componentName,\n      }),\n      ...Array.from(portalsRef.current.entries()).map(([key, { content, target }]) =>\n        ReactDOM.createPortal(content, target, key),\n      ),\n    ];\n  };\n}\n\n/**\n * Wraps a React component in AngularJS. Returns a new AngularJS component.\n *\n * @param Component The React component to wrap\n * @param bindingNames The bindings for the component, which will be passed as props\n * @param injectNames Any AngularJS dependencies that should be injected as props to the component\n *\n * @example\n * ```tsx\n * // Define a React component\n * interface Props {\n *   foo: number;\n *   $location: angular.ILocationService;\n * }\n *\n * function ReactComponent(props: Props) {\n *   return (\n *     <div>\n *       foo: {props.foo}. Location: {props.$location.absUrl()}\n *     </div>\n *   );\n * }\n *\n * // Convert it to an AngularJS component\n * const angularComponent = react2angular(ReactComponent, ['foo'], [\"$location\"]);\n * angular.module(\"myModule\", []).component(\"angularComponent\", angularComponent);\n *\n * // Then in your HTML\n * <angular-component foo-bar=\"42\" baz=\"'lorem ipsum'\"></angular-component>\n * ```\n */\nexport function react2angular<Props extends object>(\n  Component: React.ComponentType<Props>,\n  bindingNames: (keyof Props)[] = [],\n  injectNames: (keyof Props)[] = [],\n): angular.IComponentOptions {\n  return {\n    bindings: Object.fromEntries(bindingNames.map((_) => [_, \"<\"])),\n    controller: [\n      \"$element\",\n      ...(injectNames as string[]),\n      class implements angular.IController {\n        static $$ngIsClass = true;\n\n        private element: HTMLElement;\n        private root: ReactDOMClient.Root;\n        private injectedProps: Partial<Props>;\n\n        public props = {} as Partial<Props>;\n\n        public constructor($element: angular.IAugmentedJQuery, ...services: unknown[]) {\n          this.element = $element[0];\n\n          this.injectedProps = {};\n          injectNames.forEach((name, i) => {\n            this.injectedProps[name] = services[i] as Props[keyof Props];\n          });\n\n          // Search through the element ancestors for an angular2react element we can portal from\n          let reactAncestor: HTMLElement | null = this.element;\n          while (reactAncestor && !isAugmented(reactAncestor)) {\n            reactAncestor = reactAncestor.parentElement;\n          }\n\n          if (isAugmented(reactAncestor)) {\n            this.root = reactAncestor.__ReactAngularJSAdapter.createPortalRoot(this.element);\n          } else {\n            this.root = ReactDOMClient.createRoot(this.element);\n          }\n        }\n\n        public $onChanges(changes: OnChanges<Partial<Props>>) {\n          const newProps = {} as Partial<Props>;\n          for (const k of Object.keys(changes)) {\n            newProps[k as keyof Props] = changes[k as keyof Props]?.currentValue;\n          }\n\n          const nextProps = { ...this.props, ...newProps };\n          this.props = nextProps;\n\n          const reactElement = React.createElement(Component, {\n            ...this.props,\n            ...this.injectedProps,\n          } as Props);\n          this.root.render(reactElement);\n        }\n\n        public $onDestroy() {\n          this.root.unmount();\n        }\n      },\n    ],\n  };\n}\n\n/**\n * AngularJS may try to bind back a value via 2-way binding, but React marks all properties on\n * `props` as non-configurable and non-writable.\n *\n * If we use a `Proxy` to intercept writes to these non-writable properties, we run into an issue\n * where the proxy throws when trying to write anyway, even if we `return false`.\n *\n * Instead, we use the below ad-hoc proxy to catch writes to non-writable properties in `object`,\n * and log a helpful warning when it happens.\n */\nfunction writable<T extends object>(object: T): T {\n  const _object = {} as T;\n  for (const key in object) {\n    if (Object.prototype.hasOwnProperty.call(object, key)) {\n      Object.defineProperty(_object, key, {\n        get() {\n          return object[key];\n        },\n        set(value: unknown) {\n          const d = Object.getOwnPropertyDescriptor(object, key);\n          if (d?.writable) {\n            object[key] = value as T[typeof key];\n            return;\n          } else {\n            logWarning(\n              `Tried to write to non-writable property \"${key}\" of`,\n              object,\n              `. Consider using a callback instead of a 2-way binding.`,\n            );\n          }\n        },\n      });\n    }\n  }\n  return _object;\n}\n\n/**\n * Convert a camelCase string to kebab-case. Logs an error if the given string contains invalid\n * characters.\n *\n * Assuming none of the error conditions are hit, this function should generate a kebab-case string\n * which AngularJS will convert back into the source string.\n *\n * AngularJS converts from kebab-case to camelCase following the rules here:\n * https://docs.angularjs.org/guide/directive#matching-directives\n *\n * You can see the actual code AngularJS uses here:\n * https://github.com/angular/code.angularjs.org/blob/master/1.8.3/angular.js#L11576\n *\n * The valid characters are [a-zA-Z0-9] because those are the characters AngularJS allows in\n * identifiers: https://github.com/angular/code.angularjs.org/blob/master/1.8.3/angular.js#L15844\n */\nfunction kebabCase(str: string) {\n  if (!LOWERCASE_START.test(str)) {\n    throw new ReactAngularJSAdapterError(\n      `Cannot convert \"${str}\" to kebab-case because it does not start with a lowercase letter`,\n    );\n  }\n\n  if (INVALID_CHARACTERS.test(str)) {\n    throw new ReactAngularJSAdapterError(\n      `Cannot convert \"${str}\" to kebab-case because it contains characters outside the range [a-zA-Z0-9]`,\n    );\n  }\n\n  return str.replace(CAMEL_TO_KEBAB_REGEXP, (letter) => \"-\" + letter.toLowerCase());\n}\n"],"names":["$injector","_a"],"mappings":"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"}