;
_isPatching?: boolean;
[key: string]: any;
}
/**
* Create slots object from children and slots prop.
* Uses a version signal to trigger re-renders when children change.
*
* A slot reads as a callable accessor **only when content was provided** for
* it; an unprovided slot — `default` included — reads as `undefined`. So
* presence is a plain truthiness/optional-call check (`slots.header?.()`,
* `slots.header?.() ?? fallback`), and presence stays reactive: the accessor
* lookup reads the version signal, so a slot appearing or disappearing
* re-renders the consumer.
*
* Supports named slots via:
* - `slots` prop object (e.g., `slots={{ header: () => ...
}}`) —
* the typed form, checked against the consumer's declared slots, and the
* only way to fill a named slot with a component
* - `slot` prop on HOST-ELEMENT children (e.g., `...
`,
* mirroring the HTML attribute); on a component child `slot` is an ordinary
* prop and does not route (see {@link namedSlotFor}, #588)
*
* A **function child** is a render-prop fill: it is invoked with the scoped
* props the consumer passed to the accessor, and its result takes its place.
* Function and element children may be mixed freely in one default slot —
* every function is invoked with the same scoped props and element children
* pass through, in source order — so a slot is not all-or-nothing about the
* form its content takes. A function only ever fills the DEFAULT slot: routing
* a child to a named slot requires a `slot` prop on it, and a function is not
* an object, so it can never be routed there.
*
* @example
* ```tsx
* // Parent component
* Title
}}>
* Default content
* Footer text
*
*
* // Card component setup
* const slots = createSlots(children, slotsFromProps);
* return () => (
*
* {slots.header?.() ??
Fallback heading
}
* {slots.default?.()}
* {slots.footer?.()}
*
* );
* ```
*/
export function createSlots(children: any, slotsFromProps?: Record): InternalSlotsObject {
// Use a simple version signal - bump version to trigger reactivity
const versionSignal = signal({ v: 0 });
// Extraction cache keyed by the version counter. The renderer only
// reassigns _children together with a version bump, so a matching
// version means the cached scan of the children is still valid —
// repeated slot calls per render skip the O(n) walk and its
// allocations. Results are sliced on return so callers can't
// corrupt the cache.
// Null-prototype dictionaries: slot names come from user-controlled
// `slot` props, so a name like "__proto__" must be a plain key, not
// a prototype mutation.
let cachedVersion = -1;
let cachedDefault: any[] = [];
// Whether any default child is a function, recorded by the scan that
// collects them. Without it every slot read of ordinary element children
// would pay a hand loop looking for a function that is almost never there.
let cachedDefaultHasFn = false;
let cachedNamed: Record = Object.create(null);
// Extract default children (filtered of null/boolean conditional
// results) and named slots (children with a `slot` prop).
function extract(target: { _children: any }, version: number): void {
if (version === cachedVersion) return;
const defaultChildren: any[] = [];
const namedSlots: Record = Object.create(null);
let defaultHasFn = false;
const c = target._children;
if (c != null) {
const items = Array.isArray(c) ? c : [c];
for (const child of items) {
const slotName = namedSlotFor(child);
if (slotName) {
if (!namedSlots[slotName]) {
namedSlots[slotName] = [];
}
namedSlots[slotName].push(child);
} else if (child != null && child !== false && child !== true) {
// A function is `typeof 'function'`, never `'object'`, so it
// can never satisfy `namedSlotFor` — a function child always
// lands here, in `default`. So does a COMPONENT child, even
// one carrying a `slot` prop (see `namedSlotFor`).
if (typeof child === 'function') defaultHasFn = true;
defaultChildren.push(child);
}
}
}
cachedVersion = version;
cachedDefault = defaultChildren;
cachedDefaultHasFn = defaultHasFn;
cachedNamed = namedSlots;
}
const slotsObj = {
_children: children,
_slotsFromProps: slotsFromProps || {},
_version: versionSignal,
_isPatching: false, // Flag to prevent infinite loops during patching
};
// Only OWN keys count — both for the internal-property passthrough and
// for `slots` prop lookups — so inherited `Object.prototype` members
// (`toString`, `constructor`, …) never masquerade as a present slot.
const hasOwn = Object.prototype.hasOwnProperty;
// Slot accessor functions are minted once per name and reused across
// renders (they read live state on every call). `default` shares this
// path so it gets the same presence semantics as named slots.
const slotFns = new Map any[]>();
function accessorFor(name: string): (scopedProps?: any) => any[] {
let fn = slotFns.get(name);
if (!fn) {
fn = function (scopedProps?: any) {
// Reading version creates a reactive dependency (and is the
// cache key)
const version = slotsObj._version.v;
// First check for slots from the `slots` prop
const fromProps = slotsObj._slotsFromProps;
if (fromProps && hasOwn.call(fromProps, name) && typeof fromProps[name] === 'function') {
return invokeSlotFn(fromProps[name], scopedProps, name);
}
// Then fall back to element-based slots: `default` collects
// the un-slotted children, named slots collect children with
// a matching `slot` prop. Function items among them are
// invoked with `scopedProps` (render-prop form) — the mapping
// happens on return so the extraction cache keeps caching the
// RAW children.
extract(slotsObj, version);
if (name === 'default') {
// Only the default slot can hold a function child, and only
// then is the walk worth its cost — `extract()` recorded the
// answer while collecting the children, so the ordinary case
// is the plain copy it was before render-prop children
// existed.
return cachedDefaultHasFn
? invokeFunctionChildren(cachedDefault, scopedProps, name)
: cachedDefault.slice();
}
// A named slot is element-only by construction (see `extract`),
// so it is always just a defensive copy.
const list = cachedNamed[name];
return list ? list.slice() : [];
};
slotFns.set(name, fn);
}
return fn;
}
// Whether content was provided for a slot. Reads the version signal so
// presence is reactive — a slot appearing or disappearing across a
// re-render flips the accessor between a function and `undefined` and
// re-renders the consumer. A slot provided via the `slots` prop counts as
// present regardless of what it returns (matching scoped-slot semantics);
// element-based slots count as present only when they have children.
function hasContent(name: string): boolean {
const version = slotsObj._version.v;
const fromProps = slotsObj._slotsFromProps;
if (fromProps && hasOwn.call(fromProps, name) && typeof fromProps[name] === 'function') return true;
extract(slotsObj, version);
if (name === 'default') return cachedDefault.length > 0;
const list = cachedNamed[name];
return list != null && list.length > 0;
}
// Create a proxy to handle slot access dynamically
return new Proxy(slotsObj, {
get(target, prop) {
// Pass through only OWN tracking properties (`_children`,
// `_version`, …). Using `in` here would match inherited
// `Object.prototype` keys (`toString`, `constructor`,
// `__proto__`, …), making those slot names unreachable and
// always-truthy — breaking the `slots.x?.() ?? fallback`
// presence semantics for them. Own-key check lets every such
// name fall through to the slot path instead.
if (hasOwn.call(target, prop)) {
return (target as any)[prop];
}
// Handle slot access (named or `default`): expose a callable
// accessor only when content was provided, otherwise `undefined`
// so `slots.x?.()` and `?? fallback` behave intuitively.
if (typeof prop === 'string') {
return hasContent(prop) ? accessorFor(prop) : undefined;
}
return undefined;
}
}) as InternalSlotsObject;
}