import type { ComponentObjectPropsOptions, DefineComponent, Slot } from 'vue'; import { camelize, defineComponent, shallowRef } from 'vue'; /** * Map of slot name -> slot props object (or `undefined` for prop-less slots). * The inner `Record` is the idiomatic "any slot-props shape" bound: * interface-typed slot props (which lack an implicit index signature) must satisfy * it, so `Record` would wrongly reject legitimate callers. */ type SlotPropsMap = Record | undefined>; /** Turn a {@link SlotPropsMap} into a record of typed `Slot`s */ type GenerateSlotsFromSlotMap = { [K in keyof T]: Slot }; // `Bindings extends Record` is the idiomatic "any object shape" bound, // matching Vue/Reka's own component-binding generics: an interface-typed `Bindings` // must satisfy it, which `Record` would reject. Applies to every // `extends Record` constraint in this file. export type DefineTemplateComponent, Slots extends SlotPropsMap> = DefineComponent & (new () => { $slots: { // Slot render fn: returns `any` to match Vue's own `Slot` return type. default: (_: Bindings & { $slots: GenerateSlotsFromSlotMap }) => any; }; }); export type ReuseTemplateComponent, Slots extends SlotPropsMap> = DefineComponent & (new () => { $slots: GenerateSlotsFromSlotMap }); /** * The pair returned by {@link createReusableTemplate}. Usable both as a tuple * (`const [Define, Reuse] = ...`) and as an object (`const { define, reuse } = ...`). */ export type ReusableTemplatePair, Slots extends SlotPropsMap> = [DefineTemplateComponent, ReuseTemplateComponent] & { define: DefineTemplateComponent; reuse: ReuseTemplateComponent; }; export interface CreateReusableTemplateOptions> { /** * Inherit attrs from the reuse component onto its single root vnode. * * @default true */ inheritAttrs?: boolean; /** * Name used for the define/reuse components (helpful in Vue devtools). * * @default 'ReusableTemplate' */ name?: string; /** * Props definition for the reuse component. When provided, bindings are taken * from typed props instead of raw (camelized) attrs. */ props?: ComponentObjectPropsOptions; } /** Re-key an attrs object so every key is camelCased */ function keysToCamelCase(obj: Record): Record { const result: Record = {}; for (const key in obj) result[camelize(key)] = obj[key]; return result; } /** * Wrap a `{ define, reuse }` object so it can also be destructured as the tuple * `[define, reuse]`. Avoids a runtime dependency on `@vueuse/shared`. */ function makePair< Bindings extends Record, Slots extends SlotPropsMap, >( define: DefineTemplateComponent, reuse: ReuseTemplateComponent, ): ReusableTemplatePair { const pair = [define, reuse] as unknown as ReusableTemplatePair; pair.define = define; pair.reuse = reuse; return pair; } /** * @name createReusableTemplate * @category Component * @description Define a template once and reuse it multiple times within the * same component. Returns a `[DefineTemplate, ReuseTemplate]` pair (also * destructurable as `{ define, reuse }`). The template captured by * `DefineTemplate`'s default slot is rendered wherever `ReuseTemplate` appears, * receiving its props/attrs as slot bindings. Supports a generic for typed * bindings, typed slots, custom `props`, and `inheritAttrs`. * * Render-only and fully SSR-safe — it never touches `window`/`document`. The pair * is created lazily and shares a single `shallowRef` for the captured render * function, so there are no watchers and no per-render allocations beyond the * vnode itself. * * @param {CreateReusableTemplateOptions} [options] - `name`, `inheritAttrs`, and `props` * @returns {ReusableTemplatePair} A `[define, reuse]` tuple, also accessible as `{ define, reuse }` * * @example * const [DefineTemplate, ReuseTemplate] = createReusableTemplate(); * // Template: * // Hello * // * * @example * // Typed bindings + custom props * const [DefineItem, ReuseItem] = createReusableTemplate<{ label: string }>(); * // {{ label }} * // * * @since 0.0.14 */ export function createReusableTemplate< Bindings extends Record, Slots extends SlotPropsMap = Record<'default', undefined>, >( options: CreateReusableTemplateOptions = {}, ): ReusableTemplatePair { const { inheritAttrs = true, name = 'ReusableTemplate', props, } = options; // Shared captured render fn — no watchers, single allocation. const render = shallowRef(); const define = defineComponent({ name: `${name}.define`, setup(_, { slots }) { return () => { render.value = slots.default; }; }, }) as unknown as DefineTemplateComponent; const reuse = defineComponent({ name: `${name}.reuse`, inheritAttrs, props, setup(reuseProps, { attrs, slots }) { return () => { if (!render.value) { // Local cast so the dev-only guard type-checks without @types/node and stays // tree-shakeable in production builds (where NODE_ENV is statically replaced). const nodeEnv = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env?.NODE_ENV; if (nodeEnv !== 'production') throw new Error('[createReusableTemplate] Failed to find the template definition. Did you render the Define component before the Reuse component?'); return undefined; } const vnode = render.value({ ...(props === undefined ? keysToCamelCase(attrs) : reuseProps), $slots: slots, }); // When inheriting attrs onto a single root, unwrap the fragment so Vue // can merge the reuse component's attrs onto that root vnode. return inheritAttrs && vnode?.length === 1 ? vnode[0] : vnode; }; }, }) as unknown as ReuseTemplateComponent; return makePair(define, reuse); }