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 { 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 = []
}
}
}

View File

@ -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

View File

@ -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<string, Slot> & {
$?: (StaticSlots | DynamicSlotFn)[]
@ -10,7 +11,7 @@ export type RawSlots = 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 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(
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() : []
}