UNPKG

@oruga-ui/oruga-next

Version:

UI components for Vue.js and CSS framework agnostic

181 lines (156 loc) 6.96 kB
import { createApp, toValue, type App, type ComponentInternalInstance, type EmitsToProps, type MaybeRefOrGetter, type VNodeTypes, } from "vue"; import InstanceRegistry from "@/components/programmatic/InstanceRegistry"; import { getActiveOruga } from "@/utils/config"; import { getTeleportDefault, resolveElement } from "@/composables"; import { ProgrammaticComponent, type ProgrammaticComponentProps, type ProgrammaticComponentEmits, type ProgrammaticComponentExpose, } from "./ProgrammaticComponent"; declare module "../../index" { interface OrugaProgrammatic { programmatic: Required< InstanceType<typeof ProgrammaticComponentFactory> >; } } /** programmatic component options */ export type ProgrammaticOptions<C extends VNodeTypes> = { /** * Specify the template `id` attribute for the programmatic container element. * @default `programmatic-app` */ appId?: string; /** * Configure a prefix for all IDs generated via [`useId()`](https://vuejs.org/api/composition-api-helpers.html#useid) inside this programmatic instance. * By default a unique id prefix is generated. * See {@link https://vuejs.org/api/application.html#app-config-idprefix}. */ idPrefix?: string; } & Omit<ProgrammaticComponentProps<C>, "component"> & // component props EmitsToProps<Pick<Required<ProgrammaticComponentEmits<C>>, "close">>; // component emit props /** public options interface for programmatically called components */ export type ProgrammaticComponentOptions<C extends VNodeTypes> = EmitsToProps< Pick<Required<ProgrammaticComponentEmits<C>>, "close"> > & // make the type extendable Record<string, any>; /** programmatic component public interface */ export type ProgrammaticExpose<C extends VNodeTypes = VNodeTypes> = ProgrammaticComponentExpose<C>; /** target container to render the programmatic component into */ export type ProgrammaticTarget = MaybeRefOrGetter<string | HTMLElement | null>; export abstract class ProgrammaticFactory { /** programmatic instance registry for the factory instance */ private _registry = new InstanceRegistry<ComponentInternalInstance>(); /** Returns the number of registered active instances. */ public count(): number { return this._registry.count(); } /** Close the last registred instance in the global programmatic instance registry. */ public close(...args: unknown[]): void { this._registry.last()?.exposed?.close(...args); } /** Close all instances in the global programmatic instance registry. */ public closeAll(...args: unknown[]): void { this._registry.walk((entry) => entry.exposed?.close(...args)); } abstract open(...args: any[]): ProgrammaticExpose; /** * Create a new programmatic component instance. * @param component - The component to render. * @param options - Programmatic component render options. * @param target - A target container the component get rendered into - default is `document.body`. * @returns ProgrammaticExpose - programmatic component expose interface */ protected _create<C extends VNodeTypes>( component: C, options: ProgrammaticOptions<C>, target?: ProgrammaticTarget, ): ProgrammaticExpose<C> { options = { registry: this._registry, ...options }; const targetQuery = toValue(target); // define the target container const targetElement: HTMLElement | null = // either by a given query selector / element (targetQuery && resolveElement(targetQuery)) || // or by the default teleport target config resolveElement(getTeleportDefault()); if (!targetElement) throw new Error("ComponentProgrammatic - no target is defined."); // create app container let container: HTMLDivElement | undefined = document.createElement("div"); // set the HTML #id of the programmatic app container.id = options.appId || "programmatic-app"; // place the app container into the target element targetElement.appendChild(container); // clear instance handler function onDestroy(): void { // destroy app/component if (app) { app.unmount(); app = undefined; } // clear container if (container && targetElement) { targetElement.removeChild(container); container = undefined; } } // create a new vue app instance with the ProgrammaticComponent as root let app: App | undefined = createApp(ProgrammaticComponent, { registry: options.registry, // programmatic registry instance component, // the component which should be rendered props: { ...options.props, container: targetElement }, // component props including the target as `container` onClose: options.onClose, // custom onClose handler onDestroy, // node destory cleanup handler }); // get the current active oruga instance const orugaConfig = getActiveOruga(); // provide the oruga config to the new app instance orugaConfig?.provide(app); // if running inside another app share the current app context to the new app instance if (orugaConfig?._app) app._context = Object.assign( app._context, orugaConfig._app._context, ); // set a prefix for all IDs generated via useId() to prevent duplication in multiple programmatic instances app.config.idPrefix = options.idPrefix ?? "programmatic-" + options.registry!.getCounter(); // render the new vue instance into the container element const instance = app.mount(container); // return exposed programmatic functionalities from the mounted component instance return instance as unknown as ProgrammaticExpose<C>; } } export class ProgrammaticComponentFactory extends ProgrammaticFactory { /** * Create a new programmatic component instance. * @param component - The component to render. * @param options - Programmatic component render options. * @param target - A target container the component get rendered into - default is `document.body`. * @returns ProgrammaticExpose - programmatic component expose interface */ public open<C extends VNodeTypes>( component: C, options?: ProgrammaticOptions<C>, target?: ProgrammaticTarget, ): ProgrammaticExpose<C> { return this._create(component, options ?? {}, target); } } export default function useProgrammaticComponent(): ProgrammaticComponentFactory { return new ProgrammaticComponentFactory(); }