wip: dynamic slots

This commit is contained in:
Evan You 2024-12-07 21:43:08 +08:00
parent e6d4a24f1f
commit aa96762ad4
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
3 changed files with 100 additions and 33 deletions

View File

@ -1,19 +1,54 @@
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { type VaporComponentInstance, isVaporComponent } from './component' 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 Block = Node | Fragment | VaporComponentInstance | Block[]
export type BlockRenderFn = (...args: any[]) => Block
export class Fragment { export class Fragment {
nodes: Block nodes: Block
anchor?: Node anchor?: Node
constructor(nodes: Block, anchorLabel?: string) {
constructor(nodes: Block) {
this.nodes = nodes 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) ? createComment(anchorLabel)
: // eslint-disable-next-line no-restricted-globals : // eslint-disable-next-line no-restricted-globals
document.createTextNode('') 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 = []
} }
} }
} }

View File

@ -114,13 +114,15 @@ export function createComponent(
} }
const setupFn = isFunction(component) ? component : component.setup const setupFn = isFunction(component) ? component : component.setup
const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null const setupContext =
const setupResult = setupFn && setupFn.length > 1 ? new SetupContext(instance) : null
setupFn!( const setupResult = setupFn
instance.props, ? setupFn(
// @ts-expect-error instance.props,
setupContext, // @ts-expect-error
) || EMPTY_OBJ setupContext,
) || EMPTY_OBJ
: EMPTY_OBJ
if (__DEV__ && !isBlock(setupResult)) { if (__DEV__ && !isBlock(setupResult)) {
if (isFunction(component)) { if (isFunction(component)) {
@ -341,10 +343,12 @@ export function createComponentWithFallback(
}) })
} }
const defaultSlot = rawSlots && getSlot(rawSlots, 'default') if (rawSlots) {
if (defaultSlot) { if (rawSlots.$) {
const res = defaultSlot() // TODO dynamic slot fragment
insert(res, el) } else {
insert(getSlot(rawSlots, 'default')!(), el)
}
} }
return el return el

View File

@ -1,8 +1,9 @@
import { NO, hasOwn, isArray, isFunction } from '@vue/shared' import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
import { type Block, Fragment, isValidBlock } from './block' import { type Block, type BlockRenderFn, DynamicFragment } from './block'
import { type RawProps, resolveDynamicProps } from './componentProps' import type { RawProps } from './componentProps'
import { currentInstance } from '@vue/runtime-core' import { currentInstance } from '@vue/runtime-core'
import type { VaporComponentInstance } from './component' import type { VaporComponentInstance } from './component'
import { renderEffect } from './renderEffect'
export type RawSlots = Record<string, Slot> & { export type RawSlots = Record<string, Slot> & {
$?: (StaticSlots | DynamicSlotFn)[] $?: (StaticSlots | DynamicSlotFn)[]
@ -10,7 +11,7 @@ export type RawSlots = Record<string, Slot> & {
export type StaticSlots = Record<string, Slot> export type StaticSlots = Record<string, Slot>
export type Slot = (...args: any[]) => Block export type Slot = BlockRenderFn
export type DynamicSlot = { name: string; fn: Slot } export type DynamicSlot = { name: string; fn: Slot }
export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[]
@ -77,29 +78,56 @@ export function getSlot(target: RawSlots, key: string): Slot | undefined {
} }
} }
// TODO
const dynamicSlotsPropsProxyHandlers: ProxyHandler<RawProps> = {
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( export function createSlot(
name: string | (() => string), name: string | (() => string),
props?: RawProps, rawProps?: RawProps,
fallback?: Slot, fallback?: Slot,
): Block { ): Block {
const slots = (currentInstance as VaporComponentInstance)!.rawSlots const rawSlots = (currentInstance as VaporComponentInstance)!.rawSlots
if (isFunction(name) || slots.$) { 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 // dynamic slot name, or dynamic slot sources
// TODO togglable fragment class const fragment = new DynamicFragment('slot')
const fragment = new Fragment([], '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 return fragment
} else { } else {
// static // static
return renderSlot(name) const slot = resolveSlot()
}
function renderSlot(name: string) {
const slot = getSlot(slots, name)
if (slot) { if (slot) {
const block = slot(props ? resolveDynamicProps(props) : {}) const block = slot(slotProps)
if (isValidBlock(block)) { if (block) return block
return block
}
} }
return fallback ? fallback() : [] return fallback ? fallback() : []
} }