componentSlots.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
  2. import { type Block, type BlockFn, insert, setScopeId } from './block'
  3. import { rawPropsProxyHandlers, resolveFunctionSource } from './componentProps'
  4. import {
  5. type GenericComponentInstance,
  6. currentInstance,
  7. isAsyncWrapper,
  8. isRef,
  9. } from '@vue/runtime-dom'
  10. import type { LooseRawProps, VaporComponentInstance } from './component'
  11. import { renderEffect } from './renderEffect'
  12. import {
  13. insertionAnchor,
  14. insertionParent,
  15. isLastInsertion,
  16. resetInsertionState,
  17. } from './insertionState'
  18. import {
  19. advanceHydrationNode,
  20. isHydrating,
  21. locateHydrationNode,
  22. } from './dom/hydration'
  23. import {
  24. type DynamicFragment,
  25. SlotFragment,
  26. type VaporFragment,
  27. } from './fragment'
  28. import { createElement } from './dom/node'
  29. import { setDynamicProps } from './dom/prop'
  30. import { isInteropEnabled } from './vdomInteropState'
  31. /**
  32. * Flag to indicate if we are executing a once slot.
  33. * When true, renderEffect should skip creating reactive effect.
  34. */
  35. export let inOnceSlot = false
  36. /**
  37. * Current slot scopeIds for vdom interop
  38. */
  39. export let currentSlotScopeIds: string[] | null = null
  40. function setCurrentSlotScopeIds(scopeIds: string[] | null): string[] | null {
  41. try {
  42. return currentSlotScopeIds
  43. } finally {
  44. currentSlotScopeIds = scopeIds
  45. }
  46. }
  47. export type RawSlots = Record<string, VaporSlot> & {
  48. $?: DynamicSlotSource[]
  49. }
  50. export type StaticSlots = Record<string, VaporSlot>
  51. export type VaporSlot = BlockFn
  52. export type DynamicSlot = { name: string; fn: VaporSlot }
  53. export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[]
  54. export type DynamicSlotSource = StaticSlots | DynamicSlotFn
  55. export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
  56. get: getSlot,
  57. has: (target, key: string) => !!getSlot(target, key),
  58. getOwnPropertyDescriptor(target, key: string) {
  59. const slot = getSlot(target, key)
  60. if (slot) {
  61. return {
  62. configurable: true,
  63. enumerable: true,
  64. value: slot,
  65. }
  66. }
  67. },
  68. ownKeys(target) {
  69. let keys = Object.keys(target)
  70. const dynamicSources = target.$
  71. if (dynamicSources) {
  72. keys = keys.filter(k => k !== '$')
  73. for (const source of dynamicSources) {
  74. if (isFunction(source)) {
  75. const slot = resolveFunctionSource(source)
  76. if (slot) {
  77. if (isArray(slot)) {
  78. for (const s of slot) keys.push(String(s.name))
  79. } else {
  80. keys.push(String(slot.name))
  81. }
  82. }
  83. } else {
  84. keys.push(...Object.keys(source))
  85. }
  86. }
  87. }
  88. return keys
  89. },
  90. set: NO,
  91. deleteProperty: NO,
  92. }
  93. export function getSlot(
  94. target: RawSlots,
  95. key: string,
  96. ):
  97. | (VaporSlot & { _boundMap?: WeakMap<DynamicFragment, VaporSlot> })
  98. | undefined {
  99. if (key === '$') return
  100. const dynamicSources = target.$
  101. if (dynamicSources) {
  102. let i = dynamicSources.length
  103. let source
  104. while (i--) {
  105. source = dynamicSources[i]
  106. if (isFunction(source)) {
  107. const slot = resolveFunctionSource(source)
  108. if (slot) {
  109. if (isArray(slot)) {
  110. for (const s of slot) {
  111. if (String(s.name) === key) return s.fn
  112. }
  113. } else if (String(slot.name) === key) {
  114. return slot.fn
  115. }
  116. }
  117. } else if (hasOwn(source, key)) {
  118. return source[key]
  119. }
  120. }
  121. }
  122. if (hasOwn(target, key)) {
  123. return target[key]
  124. }
  125. }
  126. /**
  127. * Tracks the slot owner (the component that defines the slot content).
  128. * This is used for:
  129. * 1. Getting the correct rawSlots in forwarded slots (via createSlot)
  130. * 2. Inheriting the slot owner's scopeId
  131. */
  132. export let currentSlotOwner: VaporComponentInstance | null = null
  133. export function setCurrentSlotOwner(
  134. owner: VaporComponentInstance | null,
  135. ): VaporComponentInstance | null {
  136. try {
  137. return currentSlotOwner
  138. } finally {
  139. currentSlotOwner = owner
  140. }
  141. }
  142. /**
  143. * Get the effective slot instance for accessing rawSlots and scopeId.
  144. * Prefers currentSlotOwner (if inside a slot), falls back to currentInstance.
  145. */
  146. export function getScopeOwner(): VaporComponentInstance | null {
  147. return (currentSlotOwner || currentInstance) as VaporComponentInstance | null
  148. }
  149. /**
  150. * Wrap a slot function to track the slot owner.
  151. *
  152. * This ensures:
  153. * 1. createSlot gets rawSlots from the correct instance (slot owner)
  154. * 2. elements inherit the slot owner's scopeId
  155. */
  156. export function withVaporCtx(fn: Function): BlockFn {
  157. const owner = getScopeOwner()
  158. return (...args: any[]) => {
  159. const prevOwner = setCurrentSlotOwner(owner)
  160. try {
  161. return fn(...args)
  162. } finally {
  163. setCurrentSlotOwner(prevOwner)
  164. }
  165. }
  166. }
  167. export function createSlot(
  168. name: string | (() => string),
  169. rawProps?: LooseRawProps | null,
  170. fallback?: VaporSlot,
  171. noSlotted?: boolean,
  172. once?: boolean,
  173. ): Block {
  174. const _insertionParent = insertionParent
  175. const _insertionAnchor = insertionAnchor
  176. const _isLastInsertion = isLastInsertion
  177. if (!isHydrating) resetInsertionState()
  178. const instance = getScopeOwner()!
  179. const rawSlots = instance.rawSlots
  180. const slotProps = rawProps
  181. ? new Proxy(rawProps, rawPropsProxyHandlers)
  182. : EMPTY_OBJ
  183. let fragment: SlotFragment
  184. if (isRef(rawSlots._) && isInteropEnabled) {
  185. if (isHydrating) locateHydrationNode()
  186. fragment = instance.appContext.vapor!.vdomSlot(
  187. rawSlots._,
  188. name,
  189. slotProps,
  190. instance,
  191. fallback,
  192. )
  193. } else {
  194. fragment = new SlotFragment()
  195. // mark the slot as forwarded
  196. fragment.forwarded =
  197. currentSlotOwner != null && currentSlotOwner !== currentInstance
  198. const isDynamicName = isFunction(name)
  199. // Calculate slotScopeIds once (for vdom interop)
  200. const slotScopeIds: string[] = []
  201. if (!noSlotted) {
  202. const scopeId = instance.type.__scopeId
  203. if (scopeId) {
  204. slotScopeIds.push(`${scopeId}-s`)
  205. }
  206. }
  207. const renderSlot = () => {
  208. const slotName = isFunction(name) ? name() : name
  209. // in custom element mode, render <slot/> as actual slot outlets
  210. // because in shadowRoot: false mode the slot element gets
  211. // replaced by injected content
  212. if (
  213. (instance as GenericComponentInstance).ce ||
  214. (instance.parent &&
  215. isAsyncWrapper(instance.parent) &&
  216. instance.parent.ce)
  217. ) {
  218. const el = createElement('slot')
  219. renderEffect(() => {
  220. setDynamicProps(el, [
  221. slotProps,
  222. slotName !== 'default' ? { name: slotName } : {},
  223. ])
  224. })
  225. if (fallback) insert(fallback(), el)
  226. fragment.nodes = el
  227. return
  228. }
  229. const slot = getSlot(rawSlots, slotName)
  230. if (slot) {
  231. // Create and cache bound slot to keep it stable and avoid unnecessary
  232. // updates when it resolves to the same slot. Cache per-fragment
  233. // (v-for creates multiple fragments) so each fragment keeps its own
  234. // slotProps without cross-talk.
  235. const boundMap = slot._boundMap || (slot._boundMap = new WeakMap())
  236. let bound = boundMap.get(fragment)
  237. if (!bound) {
  238. bound = () => {
  239. const prevSlotScopeIds = setCurrentSlotScopeIds(
  240. slotScopeIds.length > 0 ? slotScopeIds : null,
  241. )
  242. const prev = inOnceSlot
  243. try {
  244. if (once) inOnceSlot = true
  245. return slot(slotProps)
  246. } finally {
  247. inOnceSlot = prev
  248. setCurrentSlotScopeIds(prevSlotScopeIds)
  249. }
  250. }
  251. boundMap.set(fragment, bound)
  252. }
  253. fragment.updateSlot(bound, fallback)
  254. } else {
  255. fragment.updateSlot(undefined, fallback)
  256. }
  257. }
  258. // dynamic slot name or has dynamicSlots
  259. if (!once && (isDynamicName || rawSlots.$)) {
  260. renderEffect(renderSlot)
  261. } else {
  262. renderSlot()
  263. }
  264. }
  265. if (!isHydrating) {
  266. if (!noSlotted) {
  267. const scopeId = instance.type.__scopeId
  268. if (scopeId) {
  269. setScopeId(fragment, [`${scopeId}-s`])
  270. }
  271. }
  272. if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor)
  273. } else {
  274. if (fragment.insert) {
  275. ;(fragment as VaporFragment).hydrate!()
  276. }
  277. if (_isLastInsertion) {
  278. advanceHydrationNode(_insertionParent!)
  279. }
  280. }
  281. return fragment
  282. }