import { onMounted, onUnmounted, ref, watch, toValue, type Ref, type MaybeRefOrGetter, } from 'vue'; import type { HeadlessController } from '@42/core'; /** * Bind a `@42/core` headless controller to a Vue component. * * Usage: * ```ts * const el = useC42(Accordion, () => ({ multiple: props.multiple })); * //
* ``` * * - The returned value is a template ref. Assign it to the element the * controller should attach to. * - The controller is instantiated in `onMounted` (so it never runs on the * server — SSR-safe) and torn down in `onUnmounted` via `destroy()`. * - When the reactive `options` change the controller's `update()` is called * if present; otherwise the instance is destroyed and re-created. * * @param ControllerClass A `@42/core` controller constructor. * @param options Reactive options: a ref, a getter, or a plain value. * @returns A template ref pointing at the controller's root element. */ export function useC42( // The class param uses a permissive `options` type so controllers whose // options are REQUIRED (e.g. Pagination) are accepted alongside those where // they're optional. `O` is still inferred from the controller's return type, // keeping the `options` argument below fully typed. // eslint-disable-next-line @typescript-eslint/no-explicit-any ControllerClass: new (root: HTMLElement, options: any) => HeadlessController, options?: MaybeRefOrGetter, ): Ref { const el = ref(); let instance: HeadlessController | null = null; const create = (): void => { // Guard against missing element (e.g. v-if) and SSR. if (!el.value || typeof window === 'undefined') return; instance = new ControllerClass(el.value, toValue(options)); }; const destroy = (): void => { instance?.destroy(); instance = null; }; onMounted(create); onUnmounted(destroy); watch( () => toValue(options), (next) => { if (!instance) return; if (typeof instance.update === 'function') { instance.update((next ?? {}) as Partial); } else { destroy(); create(); } }, { deep: true }, ); return el; } export default useC42;