vdomInterop.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import {
  2. type App,
  3. type ComponentInternalInstance,
  4. type ConcreteComponent,
  5. MoveType,
  6. type Plugin,
  7. type RendererInternals,
  8. type ShallowRef,
  9. type Slots,
  10. type VNode,
  11. type VaporInteropInterface,
  12. createVNode,
  13. currentInstance,
  14. ensureRenderer,
  15. onScopeDispose,
  16. renderSlot,
  17. shallowRef,
  18. simpleSetCurrentInstance,
  19. } from '@vue/runtime-dom'
  20. import {
  21. type LooseRawProps,
  22. type LooseRawSlots,
  23. type VaporComponent,
  24. VaporComponentInstance,
  25. createComponent,
  26. mountComponent,
  27. unmountComponent,
  28. } from './component'
  29. import { type Block, VaporFragment, insert, remove } from './block'
  30. import { EMPTY_OBJ, extend, isFunction } from '@vue/shared'
  31. import { type RawProps, rawPropsProxyHandlers } from './componentProps'
  32. import type { RawSlots, VaporSlot } from './componentSlots'
  33. import { renderEffect } from './renderEffect'
  34. import { createTextNode } from './dom/node'
  35. import { optimizePropertyLookup } from './dom/prop'
  36. // mounting vapor components and slots in vdom
  37. const vaporInteropImpl: Omit<
  38. VaporInteropInterface,
  39. 'vdomMount' | 'vdomUnmount' | 'vdomSlot'
  40. > = {
  41. mount(vnode, container, anchor, parentComponent) {
  42. const selfAnchor = (vnode.el = vnode.anchor = createTextNode())
  43. container.insertBefore(selfAnchor, anchor)
  44. const prev = currentInstance
  45. simpleSetCurrentInstance(parentComponent)
  46. const propsRef = shallowRef(vnode.props)
  47. const slotsRef = shallowRef(vnode.children)
  48. // @ts-expect-error
  49. const instance = (vnode.component = createComponent(
  50. vnode.type as any as VaporComponent,
  51. {
  52. $: [() => propsRef.value],
  53. } as RawProps,
  54. {
  55. _: slotsRef, // pass the slots ref
  56. } as any as RawSlots,
  57. ))
  58. instance.rawPropsRef = propsRef
  59. instance.rawSlotsRef = slotsRef
  60. mountComponent(instance, container, selfAnchor)
  61. vnode.el = instance.block
  62. simpleSetCurrentInstance(prev)
  63. return instance
  64. },
  65. update(n1, n2, shouldUpdate) {
  66. n2.component = n1.component
  67. n2.el = n2.anchor = n1.anchor
  68. if (shouldUpdate) {
  69. const instance = n2.component as any as VaporComponentInstance
  70. instance.rawPropsRef!.value = n2.props
  71. instance.rawSlotsRef!.value = n2.children
  72. }
  73. },
  74. unmount(vnode, doRemove) {
  75. const container = doRemove ? vnode.anchor!.parentNode : undefined
  76. if (vnode.component) {
  77. unmountComponent(vnode.component as any, container)
  78. } else if (vnode.vb) {
  79. remove(vnode.vb, container)
  80. }
  81. remove(vnode.anchor as Node, container)
  82. },
  83. /**
  84. * vapor slot in vdom
  85. */
  86. slot(n1: VNode, n2: VNode, container, anchor) {
  87. if (!n1) {
  88. // mount
  89. const selfAnchor = (n2.el = n2.anchor = createTextNode())
  90. insert(selfAnchor, container, anchor)
  91. const { slot, fallback } = n2.vs!
  92. const propsRef = (n2.vs!.ref = shallowRef(n2.props))
  93. const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
  94. // TODO fallback for slot with v-if content
  95. // fallback is a vnode slot function here, and slotBlock, if a DynamicFragment,
  96. // expects a Vapor BlockFn as fallback
  97. fallback
  98. insert((n2.vb = slotBlock), container, selfAnchor)
  99. } else {
  100. // update
  101. n2.el = n2.anchor = n1.anchor
  102. n2.vb = n1.vb
  103. ;(n2.vs!.ref = n1.vs!.ref)!.value = n2.props
  104. }
  105. },
  106. move(vnode, container, anchor) {
  107. insert(vnode.vb || (vnode.component as any), container, anchor)
  108. insert(vnode.anchor as any, container, anchor)
  109. },
  110. }
  111. const vaporSlotPropsProxyHandler: ProxyHandler<
  112. ShallowRef<Record<string, any>>
  113. > = {
  114. get(target, key: any) {
  115. return target.value[key]
  116. },
  117. has(target, key: any) {
  118. return target.value[key]
  119. },
  120. ownKeys(target) {
  121. return Object.keys(target.value)
  122. },
  123. }
  124. const vaporSlotsProxyHandler: ProxyHandler<any> = {
  125. get(target, key) {
  126. if (key === '_vapor') {
  127. return target
  128. } else {
  129. return target[key]
  130. }
  131. },
  132. }
  133. /**
  134. * Mount vdom component in vapor
  135. */
  136. function createVDOMComponent(
  137. internals: RendererInternals,
  138. component: ConcreteComponent,
  139. rawProps?: LooseRawProps | null,
  140. rawSlots?: LooseRawSlots | null,
  141. ): VaporFragment {
  142. const frag = new VaporFragment([])
  143. const vnode = createVNode(
  144. component,
  145. rawProps && new Proxy(rawProps, rawPropsProxyHandlers),
  146. )
  147. const wrapper = new VaporComponentInstance(
  148. { props: component.props },
  149. rawProps as RawProps,
  150. rawSlots as RawSlots,
  151. )
  152. // overwrite how the vdom instance handles props
  153. vnode.vi = (instance: ComponentInternalInstance) => {
  154. instance.props = wrapper.props
  155. instance.attrs = wrapper.attrs
  156. instance.slots =
  157. wrapper.slots === EMPTY_OBJ
  158. ? EMPTY_OBJ
  159. : new Proxy(wrapper.slots, vaporSlotsProxyHandler)
  160. }
  161. let isMounted = false
  162. const parentInstance = currentInstance as VaporComponentInstance
  163. const unmount = (parentNode?: ParentNode) => {
  164. internals.umt(vnode.component!, null, !!parentNode)
  165. }
  166. vnode.scopeId = parentInstance.type.__scopeId!
  167. frag.insert = (parentNode, anchor) => {
  168. if (!isMounted) {
  169. internals.mt(
  170. vnode,
  171. parentNode,
  172. anchor,
  173. parentInstance as any,
  174. null,
  175. undefined,
  176. false,
  177. )
  178. onScopeDispose(unmount, true)
  179. isMounted = true
  180. } else {
  181. // move
  182. internals.m(
  183. vnode,
  184. parentNode,
  185. anchor,
  186. MoveType.REORDER,
  187. parentInstance as any,
  188. )
  189. }
  190. // update the fragment nodes
  191. frag.nodes = vnode.el as Block
  192. }
  193. frag.remove = unmount
  194. return frag
  195. }
  196. /**
  197. * Mount vdom slot in vapor
  198. */
  199. function renderVDOMSlot(
  200. internals: RendererInternals,
  201. slotsRef: ShallowRef<Slots>,
  202. name: string | (() => string),
  203. props: Record<string, any>,
  204. parentComponent: VaporComponentInstance,
  205. fallback?: VaporSlot,
  206. ): VaporFragment {
  207. const frag = new VaporFragment([])
  208. let isMounted = false
  209. let fallbackNodes: Block | undefined
  210. let oldVNode: VNode | null = null
  211. frag.insert = (parentNode, anchor) => {
  212. if (!isMounted) {
  213. renderEffect(() => {
  214. const vnode = renderSlot(
  215. slotsRef.value,
  216. isFunction(name) ? name() : name,
  217. props,
  218. )
  219. if ((vnode.children as any[]).length) {
  220. if (fallbackNodes) {
  221. remove(fallbackNodes, parentNode)
  222. fallbackNodes = undefined
  223. }
  224. internals.p(
  225. oldVNode,
  226. vnode,
  227. parentNode,
  228. anchor,
  229. parentComponent as any,
  230. )
  231. oldVNode = vnode
  232. } else {
  233. if (fallback && !fallbackNodes) {
  234. // mount fallback
  235. if (oldVNode) {
  236. internals.um(oldVNode, parentComponent as any, null, true)
  237. }
  238. insert((fallbackNodes = fallback(props)), parentNode, anchor)
  239. }
  240. oldVNode = null
  241. }
  242. })
  243. isMounted = true
  244. } else {
  245. // move
  246. internals.m(
  247. oldVNode!,
  248. parentNode,
  249. anchor,
  250. MoveType.REORDER,
  251. parentComponent as any,
  252. )
  253. }
  254. frag.remove = parentNode => {
  255. if (fallbackNodes) {
  256. remove(fallbackNodes, parentNode)
  257. } else if (oldVNode) {
  258. internals.um(oldVNode, parentComponent as any, null)
  259. }
  260. }
  261. }
  262. return frag
  263. }
  264. export const vaporInteropPlugin: Plugin = app => {
  265. const internals = ensureRenderer().internals
  266. app._context.vapor = extend(vaporInteropImpl, {
  267. vdomMount: createVDOMComponent.bind(null, internals),
  268. vdomUnmount: internals.umt,
  269. vdomSlot: renderVDOMSlot.bind(null, internals),
  270. })
  271. const mount = app.mount
  272. app.mount = ((...args) => {
  273. optimizePropertyLookup()
  274. return mount(...args)
  275. }) satisfies App['mount']
  276. }