wip: slots

This commit is contained in:
Evan You 2024-12-07 15:12:32 +08:00
parent 8331aa43c4
commit 4b6100623f
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
6 changed files with 76 additions and 27 deletions

View File

@ -1,17 +1,25 @@
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'
export const fragmentKey: unique symbol = Symbol(__DEV__ ? `fragmentKey` : ``)
export type Block = Node | Fragment | VaporComponentInstance | Block[] export type Block = Node | Fragment | VaporComponentInstance | Block[]
export type Fragment = {
export class Fragment {
nodes: Block nodes: Block
anchor?: Node anchor?: Node
[fragmentKey]: true constructor(nodes: Block, anchorLabel?: string) {
this.nodes = nodes
if (anchorLabel) {
this.anchor = __DEV__
? createComment(anchorLabel)
: // eslint-disable-next-line no-restricted-globals
document.createTextNode('')
}
}
} }
export function isFragment(val: NonNullable<unknown>): val is Fragment { export function isFragment(val: NonNullable<unknown>): val is Fragment {
return fragmentKey in val return val instanceof Fragment
} }
export function isBlock(val: NonNullable<unknown>): val is Block { export function isBlock(val: NonNullable<unknown>): val is Block {
@ -59,6 +67,7 @@ export function getFirstNode(block: Block | null): Node | undefined {
} }
} }
// TODO optimize
export function isValidBlock(block: Block): boolean { export function isValidBlock(block: Block): boolean {
return ( return (
normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0 normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0

View File

@ -36,7 +36,9 @@ import {
type RawSlots, type RawSlots,
type StaticSlots, type StaticSlots,
dynamicSlotsProxyHandlers, dynamicSlotsProxyHandlers,
getSlot,
} from './componentSlots' } from './componentSlots'
import { insert } from './dom/element'
export { currentInstance } from '@vue/runtime-dom' export { currentInstance } from '@vue/runtime-dom'
@ -84,7 +86,8 @@ interface SharedInternalOptions {
export function createComponent( export function createComponent(
component: VaporComponent, component: VaporComponent,
rawProps?: RawProps, rawProps?: RawProps | null,
rawSlots?: RawSlots | null,
isSingleRoot?: boolean, isSingleRoot?: boolean,
): VaporComponentInstance { ): VaporComponentInstance {
// check if we are the single root of the parent // check if we are the single root of the parent
@ -102,7 +105,7 @@ export function createComponent(
} }
} }
const instance = new VaporComponentInstance(component, rawProps) const instance = new VaporComponentInstance(component, rawProps, rawSlots)
const resetCurrentInstance = setCurrentInstance(instance) const resetCurrentInstance = setCurrentInstance(instance)
pauseTracking() pauseTracking()
@ -175,12 +178,14 @@ export class VaporComponentInstance implements GenericComponentInstance {
block: Block block: Block
scope: EffectScope scope: EffectScope
rawProps: RawProps
props: Record<string, any> props: Record<string, any>
attrs: Record<string, any> attrs: Record<string, any>
slots: StaticSlots slots: StaticSlots
exposed: Record<string, any> | null exposed: Record<string, any> | null
rawProps: RawProps
rawSlots: RawSlots
emitted: Record<string, boolean> | null emitted: Record<string, boolean> | null
propsDefaults: Record<string, any> | null propsDefaults: Record<string, any> | null
@ -221,7 +226,11 @@ export class VaporComponentInstance implements GenericComponentInstance {
propsOptions?: NormalizedPropsOptions propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null emitsOptions?: ObjectEmitsOptions | null
constructor(comp: VaporComponent, rawProps?: RawProps, rawSlots?: RawSlots) { constructor(
comp: VaporComponent,
rawProps?: RawProps | null,
rawSlots?: RawSlots | null,
) {
this.vapor = true this.vapor = true
this.uid = nextUid() this.uid = nextUid()
this.type = comp this.type = comp
@ -257,6 +266,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
} }
// init slots // init slots
this.rawSlots = rawSlots || EMPTY_OBJ
this.slots = rawSlots this.slots = rawSlots
? rawSlots.$ ? rawSlots.$
? new Proxy(rawSlots, dynamicSlotsProxyHandlers) ? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
@ -304,12 +314,12 @@ export class SetupContext<E = EmitsOptions> {
*/ */
export function createComponentWithFallback( export function createComponentWithFallback(
comp: VaporComponent | string, comp: VaporComponent | string,
rawProps: RawProps | undefined, rawProps: RawProps | null | undefined,
// TODO slots: RawSlots | null rawSlots: RawSlots | null | undefined,
isSingleRoot?: boolean, isSingleRoot?: boolean,
): HTMLElement | VaporComponentInstance { ): HTMLElement | VaporComponentInstance {
if (!isString(comp)) { if (!isString(comp)) {
return createComponent(comp, rawProps, isSingleRoot) return createComponent(comp, rawProps, rawSlots, isSingleRoot)
} }
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
@ -331,17 +341,11 @@ export function createComponentWithFallback(
}) })
} }
// TODO const defaultSlot = rawSlots && getSlot(rawSlots, 'default')
// if (slots) { if (defaultSlot) {
// if (!Array.isArray(slots)) slots = [slots] const res = defaultSlot()
// for (let i = 0; i < slots.length; i++) { insert(res, el)
// const slot = slots[i] }
// if (!isDynamicSlotFn(slot) && slot.default) {
// const block = slot.default && slot.default()
// if (block) el.append(...normalizeBlock(block))
// }
// }
// }
return el return el
} }

View File

@ -213,7 +213,7 @@ function resolveDefault(
export function hasFallthroughAttrs( export function hasFallthroughAttrs(
comp: VaporComponent, comp: VaporComponent,
rawProps: RawProps | undefined, rawProps: RawProps | null | undefined,
): boolean { ): boolean {
if (rawProps) { if (rawProps) {
// determine fallthrough // determine fallthrough

View File

@ -1,5 +1,9 @@
import { NO, hasOwn, isArray, isFunction } from '@vue/shared' import { NO, hasOwn, isArray, isFunction } from '@vue/shared'
import type { Block } from './block' import { type Block, Fragment, isValidBlock } from './block'
import { type RawProps, resolveDynamicProps } from './componentProps'
import { currentInstance } from '@vue/runtime-core'
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)[]
@ -47,7 +51,8 @@ export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
deleteProperty: NO, deleteProperty: NO,
} }
function getSlot(target: RawSlots, key: string) { export function getSlot(target: RawSlots, key: string): Slot | undefined {
if (key === '$') return
const dynamicSources = target.$ const dynamicSources = target.$
if (dynamicSources) { if (dynamicSources) {
let i = dynamicSources.length let i = dynamicSources.length
@ -72,3 +77,31 @@ function getSlot(target: RawSlots, key: string) {
return target[key] return target[key]
} }
} }
export function createSlot(
name: string | (() => string),
props?: RawProps,
fallback?: Slot,
): Block {
const slots = (currentInstance as VaporComponentInstance)!.rawSlots
if (isFunction(name) || slots.$) {
// dynamic slot name, or dynamic slot sources
// TODO togglable fragment class
const fragment = new Fragment([], 'slot')
return fragment
} else {
// static
return renderSlot(name)
}
function renderSlot(name: string) {
const slot = getSlot(slots, name)
if (slot) {
const block = slot(props ? resolveDynamicProps(props) : {})
if (isValidBlock(block)) {
return block
}
}
return fallback ? fallback() : []
}
}

View File

@ -27,6 +27,7 @@ export function insert(
} else { } else {
// fragment // fragment
insert(block.nodes, parent, anchor) insert(block.nodes, parent, anchor)
if (block.anchor) parent.insertBefore(block.anchor, anchor)
} }
} }
@ -47,12 +48,13 @@ export function remove(block: Block, parent: ParentNode): void {
export function createTextNode(values?: any[] | (() => any[])): Text { export function createTextNode(values?: any[] | (() => any[])): Text {
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
const node = document.createTextNode('') const node = document.createTextNode('')
if (values) if (values) {
if (isArray(values)) { if (isArray(values)) {
setText(node, ...values) setText(node, ...values)
} else { } else {
renderEffect(() => setText(node, ...values())) renderEffect(() => setText(node, ...values()))
} }
}
return node return node
} }

View File

@ -2,6 +2,7 @@ export { createComponent, createComponentWithFallback } from './component'
export { renderEffect } from './renderEffect' export { renderEffect } from './renderEffect'
export { createVaporApp } from './apiCreateApp' export { createVaporApp } from './apiCreateApp'
export { defineComponent } from './apiDefineComponent' export { defineComponent } from './apiDefineComponent'
export { createSlot } from './componentSlots'
// DOM // DOM
export { template, children, next } from './dom/template' export { template, children, next } from './dom/template'