import React from 'react';
import ReactDOM from 'react-dom';
import { InjectableComponent } from './wrapper.js';
import { v4 as uuidv4 } from 'uuid';
import { Context } from './context.js';
import { ComponentType, ShadowPortal } from './types.js';
import { createMirror } from './mirror-node.js';
interface InjectOptions
{
shadowHost?: HTMLElement;
includeCssReset?: boolean;
useClosedShadow?: boolean;
mountStrategy?: (Component: ComponentType
, props: P & InjectComponentInternalProps, mountInto: HTMLDivElement) => Promise>;
}
export interface InjectionResult {
id: string;
shadowHost: HTMLElement;
shadowRoot: ShadowRoot;
mountedInto: HTMLDivElement;
stylesWrapper: HTMLDivElement;
updateProps: (newProps: Partial
) => Promise;
unmount: () => Promise;
mirrorStylesInto: (node: HTMLElement) => void;
connectPortal: (portal: ShadowPortal) => void;
}
export interface RenderResult {
updateProps: InjectionResult
['updateProps'];
unmount: InjectionResult
['unmount'];
}
const mountUsingReactDomRender = async
(
Component: ComponentType
,
props: P & InjectComponentInternalProps,
mountInto: HTMLDivElement
): Promise> => {
let propsSaved = { ...props };
return new Promise((resolve) => {
// @ts-ignore
ReactDOM.render(, mountInto, () => {
const result: RenderResult = {
updateProps: (newProps) => {
return new Promise((resolve) => {
propsSaved = { ...propsSaved, ...newProps };
// @ts-ignore
ReactDOM.render(, mountInto, () => {
resolve();
});
});
},
unmount: () => {
return new Promise((resolve) => {
ReactDOM.unmountComponentAtNode(mountInto);
resolve();
});
},
};
resolve(result);
});
});
};
type InjectComponentInternalProps = {
connectedPortals: ShadowPortal[],
};
export const injectComponent = async
(
injectable: InjectableComponent
,
props: P,
options: InjectOptions
= {}
): Promise> => {
const { includeCssReset = true, mountStrategy = mountUsingReactDomRender } = options;
const id = uuidv4();
const shadowHost = options.shadowHost || document.createElement('div');
if (!options.shadowHost) shadowHost.id = id; // Don't mess with elements provided by user
const shadowRoot = shadowHost.attachShadow({ mode: options.useClosedShadow ? 'closed' : 'open' });
const mountedInto = document.createElement('div');
const stylesWrapper = document.createElement('div');
mountedInto.classList.add('inject-react-anywhere-mounted-into');
shadowRoot.appendChild(mountedInto);
shadowRoot.appendChild(stylesWrapper);
const connectedPortals = { current: [] as ShadowPortal[] };
if (includeCssReset) {
const styleTag = document.createElement('style');
styleTag.innerHTML =
'.inject-react-anywhere-mounted-into, .inject-react-anywhere-mounted-into::before, .inject-react-anywhere-mounted-into::after {all: initial;}';
stylesWrapper.appendChild(styleTag);
}
const ComponentWithStyles = await injectable.stylesInjector(
injectable.component,
shadowHost,
shadowRoot,
mountedInto,
stylesWrapper
);
const Component = ({ connectedPortals, ...props }: P & InjectComponentInternalProps) => {
return (
{
renderResults.unmount();
},
connectedPortals,
}}
>
);
};
await new Promise(r => setTimeout(r, 0)); // Give browser type to parse/apply styles
const finalProps = {
...props,
connectedPortals: connectedPortals.current,
};
const renderResults = await mountStrategy(Component, finalProps, mountedInto);
return {
id,
shadowHost,
shadowRoot,
mountedInto,
stylesWrapper,
mirrorStylesInto: (node: HTMLElement) => {
createMirror(stylesWrapper, node);
},
connectPortal: (portal: ShadowPortal) => {
if (connectedPortals.current.includes(portal)) return;
connectedPortals.current = [...connectedPortals.current, portal];
// TODO: maybe we should delay this to next tick? Because if user connects portal before
// appending it into DOM it will break styled-components integration. Or we should add targeted check
// that portal is in DOM and log warning otherwise.
// @ts-ignore Unknown prop because we don't expose internal props in types
renderResults.updateProps({ connectedPortals: connectedPortals.current });
},
...renderResults,
};
};
export const createShadowPortal = (): ShadowPortal => {
const id = uuidv4();
const shadowHost = document.createElement('div');
shadowHost.id = id;
const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); // Should this be an option?
const mountedInto = document.createElement('div');
const stylesWrapper = document.createElement('div');
mountedInto.classList.add('inject-react-anywhere-portal-mounted-into');
shadowRoot.appendChild(mountedInto);
shadowRoot.appendChild(stylesWrapper);
return {
shadowHost,
shadowRoot,
portalInto: mountedInto,
stylesWrapper,
};
};