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;