diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index e9e2204e9..ea4cc881c 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -1,19 +1,54 @@ import { isArray } from '@vue/shared' import { type VaporComponentInstance, isVaporComponent } from './component' -import { createComment } from './dom/element' +import { createComment, insert, remove } from './dom/element' +import { EffectScope } from '@vue/reactivity' export type Block = Node | Fragment | VaporComponentInstance | Block[] +export type BlockRenderFn = (...args: any[]) => Block + export class Fragment { nodes: Block anchor?: Node - constructor(nodes: Block, anchorLabel?: string) { + + constructor(nodes: Block) { this.nodes = nodes - if (anchorLabel) { - this.anchor = __DEV__ + } +} + +export class DynamicFragment extends Fragment { + anchor: Node + scope: EffectScope | undefined + key: any + + constructor(anchorLabel?: string) { + super([]) + this.anchor = + __DEV__ && anchorLabel ? createComment(anchorLabel) : // eslint-disable-next-line no-restricted-globals document.createTextNode('') + } + + update(render?: BlockRenderFn, key: any = render): void { + if (key === this.key) return + this.key = key + + const parent = this.anchor.parentNode + + // teardown previous branch + if (this.scope) { + this.scope.off() + parent && remove(this.nodes, parent) + } + + if (render) { + this.scope = new EffectScope() + this.nodes = this.scope.run(render) || [] + if (parent) insert(this.nodes, parent) + } else { + this.scope = undefined + this.nodes = [] } } } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 8e2220cb7..1c98f42f0 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -114,13 +114,15 @@ export function createComponent( } const setupFn = isFunction(component) ? component : component.setup - const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null - const setupResult = - setupFn!( - instance.props, - // @ts-expect-error - setupContext, - ) || EMPTY_OBJ + const setupContext = + setupFn && setupFn.length > 1 ? new SetupContext(instance) : null + const setupResult = setupFn + ? setupFn( + instance.props, + // @ts-expect-error + setupContext, + ) || EMPTY_OBJ + : EMPTY_OBJ if (__DEV__ && !isBlock(setupResult)) { if (isFunction(component)) { @@ -341,10 +343,12 @@ export function createComponentWithFallback( }) } - const defaultSlot = rawSlots && getSlot(rawSlots, 'default') - if (defaultSlot) { - const res = defaultSlot() - insert(res, el) + if (rawSlots) { + if (rawSlots.$) { + // TODO dynamic slot fragment + } else { + insert(getSlot(rawSlots, 'default')!(), el) + } } return el diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 7557f79e0..2e8c898ef 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,8 +1,9 @@ -import { NO, hasOwn, isArray, isFunction } from '@vue/shared' -import { type Block, Fragment, isValidBlock } from './block' -import { type RawProps, resolveDynamicProps } from './componentProps' +import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' +import { type Block, type BlockRenderFn, DynamicFragment } from './block' +import type { RawProps } from './componentProps' import { currentInstance } from '@vue/runtime-core' import type { VaporComponentInstance } from './component' +import { renderEffect } from './renderEffect' export type RawSlots = Record & { $?: (StaticSlots | DynamicSlotFn)[] @@ -10,7 +11,7 @@ export type RawSlots = Record & { export type StaticSlots = Record -export type Slot = (...args: any[]) => Block +export type Slot = BlockRenderFn export type DynamicSlot = { name: string; fn: Slot } export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] @@ -77,29 +78,56 @@ export function getSlot(target: RawSlots, key: string): Slot | undefined { } } +// TODO +const dynamicSlotsPropsProxyHandlers: ProxyHandler = { + get(target, key: string) { + return target[key] + }, + has(target, key) { + return key in target + }, +} + +// TODO how to handle empty slot return blocks? +// e.g. a slot renders a v-if node that may toggle inside. +// we may need special handling by passing the fallback into the slot +// and make the v-if use it as fallback export function createSlot( name: string | (() => string), - props?: RawProps, + rawProps?: RawProps, fallback?: Slot, ): Block { - const slots = (currentInstance as VaporComponentInstance)!.rawSlots - if (isFunction(name) || slots.$) { + const rawSlots = (currentInstance as VaporComponentInstance)!.rawSlots + const resolveSlot = () => getSlot(rawSlots, isFunction(name) ? name() : name) + const slotProps = rawProps + ? rawProps.$ + ? new Proxy(rawProps, dynamicSlotsPropsProxyHandlers) + : rawProps + : EMPTY_OBJ + + if (isFunction(name) || rawSlots.$) { // dynamic slot name, or dynamic slot sources - // TODO togglable fragment class - const fragment = new Fragment([], 'slot') + const fragment = new DynamicFragment('slot') + renderEffect(() => { + const slot = resolveSlot() + if (slot) { + fragment.update( + () => slot(slotProps) || (fallback && fallback()), + // pass the stable slot fn as key to avoid toggling when resolving + // to the same slot + slot, + ) + } else { + fragment.update(fallback) + } + }) return fragment } else { // static - return renderSlot(name) - } - - function renderSlot(name: string) { - const slot = getSlot(slots, name) + const slot = resolveSlot() if (slot) { - const block = slot(props ? resolveDynamicProps(props) : {}) - if (isValidBlock(block)) { - return block - } + const block = slot(slotProps) + if (block) return block } return fallback ? fallback() : [] }