componentSlots.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
  2. import { type Block, type BlockFn, DynamicFragment } from './block'
  3. import {
  4. type RawProps,
  5. getAttrFromRawProps,
  6. hasAttrFromRawProps,
  7. } from './componentProps'
  8. import { currentInstance } from '@vue/runtime-core'
  9. import type { VaporComponentInstance } from './component'
  10. import { renderEffect } from './renderEffect'
  11. export type RawSlots = Record<string, Slot> & {
  12. $?: DynamicSlotSource[]
  13. }
  14. export type StaticSlots = Record<string, Slot>
  15. export type Slot = BlockFn
  16. export type DynamicSlot = { name: string; fn: Slot }
  17. export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[]
  18. export type DynamicSlotSource = StaticSlots | DynamicSlotFn
  19. export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
  20. get: getSlot,
  21. has: (target, key: string) => !!getSlot(target, key),
  22. getOwnPropertyDescriptor(target, key: string) {
  23. const slot = getSlot(target, key)
  24. if (slot) {
  25. return {
  26. configurable: true,
  27. enumerable: true,
  28. value: slot,
  29. }
  30. }
  31. },
  32. ownKeys(target) {
  33. const keys = Object.keys(target)
  34. const dynamicSources = target.$
  35. if (dynamicSources) {
  36. for (const source of dynamicSources) {
  37. if (isFunction(source)) {
  38. const slot = source()
  39. if (isArray(slot)) {
  40. for (const s of slot) keys.push(s.name)
  41. } else {
  42. keys.push(slot.name)
  43. }
  44. } else {
  45. keys.push(...Object.keys(source))
  46. }
  47. }
  48. }
  49. return keys
  50. },
  51. set: NO,
  52. deleteProperty: NO,
  53. }
  54. export function getSlot(target: RawSlots, key: string): Slot | undefined {
  55. if (key === '$') return
  56. const dynamicSources = target.$
  57. if (dynamicSources) {
  58. let i = dynamicSources.length
  59. let source
  60. while (i--) {
  61. source = dynamicSources[i]
  62. if (isFunction(source)) {
  63. const slot = source()
  64. if (isArray(slot)) {
  65. for (const s of slot) {
  66. if (s.name === key) return s.fn
  67. }
  68. } else if (slot.name === key) {
  69. return slot.fn
  70. }
  71. } else if (hasOwn(source, key)) {
  72. return source[key]
  73. }
  74. }
  75. }
  76. if (hasOwn(target, key)) {
  77. return target[key]
  78. }
  79. }
  80. const dynamicSlotsPropsProxyHandlers: ProxyHandler<RawProps> = {
  81. get: getAttrFromRawProps,
  82. has: hasAttrFromRawProps,
  83. ownKeys: target => Object.keys(target).filter(k => k !== '$'),
  84. }
  85. // TODO how to handle empty slot return blocks?
  86. // e.g. a slot renders a v-if node that may toggle inside.
  87. // we may need special handling by passing the fallback into the slot
  88. // and make the v-if use it as fallback
  89. export function createSlot(
  90. name: string | (() => string),
  91. rawProps?: RawProps,
  92. fallback?: Slot,
  93. ): Block {
  94. const fragment = new DynamicFragment('slot')
  95. const rawSlots = (currentInstance as VaporComponentInstance)!.rawSlots
  96. const slotProps = rawProps
  97. ? new Proxy(rawProps, dynamicSlotsPropsProxyHandlers)
  98. : EMPTY_OBJ
  99. // always create effect because a slot may contain dynamic root inside
  100. // which affects fallback
  101. renderEffect(() => {
  102. const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
  103. if (slot) {
  104. fragment.update(
  105. () => slot(slotProps) || (fallback && fallback()),
  106. // TODO this key needs to account for possible fallback (v-if)
  107. // inside the slot
  108. slot,
  109. )
  110. } else {
  111. fragment.update(fallback)
  112. }
  113. })
  114. return fragment
  115. }