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 = []
}
}
}
}
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)) {
})
}
- 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
-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)[]
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[]
}
}
+// 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() : []
}