Browse Source

wip(vapor): vdom slots in vapor component

Evan You 1 year ago
parent
commit
99d70ddd31

+ 7 - 0
packages/runtime-core/src/apiCreateApp.ts

@@ -194,6 +194,13 @@ export interface VaporInteropInterface {
   move(vnode: VNode, container: any, anchor: any): void
   vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
   vdomUnmount: UnmountComponentFn
+  vdomSlot: (
+    slots: any,
+    name: string | (() => string),
+    props: Record<string, any>,
+    parentComponent: any, // VaporComponentInstance
+    fallback?: any, // VaporSlot
+  ) => any
 }
 
 /**

+ 5 - 4
packages/runtime-core/src/helpers/renderSlot.ts

@@ -32,10 +32,11 @@ export function renderSlot(
   noSlotted?: boolean,
 ): VNode {
   if (
-    currentRenderingInstance!.ce ||
-    (currentRenderingInstance!.parent &&
-      isAsyncWrapper(currentRenderingInstance!.parent) &&
-      currentRenderingInstance!.parent.ce)
+    currentRenderingInstance &&
+    (currentRenderingInstance.ce ||
+      (currentRenderingInstance.parent &&
+        isAsyncWrapper(currentRenderingInstance.parent) &&
+        currentRenderingInstance.parent.ce))
   ) {
     // in custom element mode, render <slot/> as actual slot outlets
     // wrap it with a fragment because in shadowRoot: false mode the slot

+ 7 - 3
packages/runtime-core/src/hmr.ts

@@ -7,7 +7,7 @@ import {
   type GenericComponentInstance,
   isClassComponent,
 } from './component'
-import { queueJob, queuePostFlushCb } from './scheduler'
+import { nextTick, queueJob, queuePostFlushCb } from './scheduler'
 import { extend, getGlobalThis } from '@vue/shared'
 
 type HMRComponent = ComponentOptions | ClassComponent
@@ -102,7 +102,9 @@ function rerender(id: string, newRender?: Function): void {
       i.renderCache = []
       i.update()
     }
-    isHmrUpdating = false
+    nextTick(() => {
+      isHmrUpdating = false
+    })
   })
 }
 
@@ -160,7 +162,9 @@ function reload(id: string, newComp: HMRComponent): void {
           } else {
             ;(parent as ComponentInternalInstance).update()
           }
-          isHmrUpdating = false
+          nextTick(() => {
+            isHmrUpdating = false
+          })
           // #6930, #11248 avoid infinite recursion
           dirtyInstances.delete(instance)
         })

+ 9 - 7
packages/runtime-core/src/renderer.ts

@@ -2530,15 +2530,17 @@ function resolveChildrenNamespace(
 }
 
 function toggleRecurse(
-  { effect, job }: ComponentInternalInstance,
+  { effect, job, vapor }: ComponentInternalInstance,
   allowed: boolean,
 ) {
-  if (allowed) {
-    effect.flags |= EffectFlags.ALLOW_RECURSE
-    job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
-  } else {
-    effect.flags &= ~EffectFlags.ALLOW_RECURSE
-    job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE
+  if (!vapor) {
+    if (allowed) {
+      effect.flags |= EffectFlags.ALLOW_RECURSE
+      job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
+    } else {
+      effect.flags &= ~EffectFlags.ALLOW_RECURSE
+      job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE
+    }
   }
 }
 

+ 4 - 1
packages/runtime-vapor/src/component.ts

@@ -313,10 +313,13 @@ export class VaporComponentInstance implements GenericComponentInstance {
   props: Record<string, any>
   attrs: Record<string, any>
   propsDefaults: Record<string, any> | null
-  rawPropsRef?: ShallowRef<any> // to hold vnode props in vdom interop mode
 
   slots: StaticSlots
 
+  // to hold vnode props / slots in vdom interop mode
+  rawPropsRef?: ShallowRef<any>
+  rawSlotsRef?: ShallowRef<any>
+
   emit: EmitFn
   emitted: Record<string, boolean> | null
 

+ 12 - 6
packages/runtime-vapor/src/componentSlots.ts

@@ -85,10 +85,6 @@ export function getSlot(
   }
 }
 
-// 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),
   rawProps?: LooseRawProps | null,
@@ -96,12 +92,22 @@ export function createSlot(
 ): Block {
   const instance = currentInstance as VaporComponentInstance
   const rawSlots = instance.rawSlots
-  const isDynamicName = isFunction(name)
-  const fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
   const slotProps = rawProps
     ? new Proxy(rawProps, rawPropsProxyHandlers)
     : EMPTY_OBJ
 
+  if (rawSlots._) {
+    return instance.appContext.vapor!.vdomSlot(
+      rawSlots._,
+      name,
+      slotProps,
+      instance,
+      fallback,
+    )
+  }
+
+  const isDynamicName = isFunction(name)
+  const fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
   const renderSlot = () => {
     const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
     if (slot) {

+ 87 - 11
packages/runtime-vapor/src/vdomInterop.ts

@@ -3,41 +3,57 @@ import {
   type ConcreteComponent,
   type Plugin,
   type RendererInternals,
+  type ShallowRef,
+  type Slots,
+  type VNode,
   type VaporInteropInterface,
   createVNode,
   currentInstance,
   ensureRenderer,
+  renderSlot,
   shallowRef,
   simpleSetCurrentInstance,
 } from '@vue/runtime-dom'
 import {
   type LooseRawProps,
   type LooseRawSlots,
+  type VaporComponent,
   VaporComponentInstance,
   createComponent,
   mountComponent,
   unmountComponent,
 } from './component'
-import { VaporFragment, insert } from './block'
-import { extend, remove } from '@vue/shared'
+import { type Block, VaporFragment, insert, remove } from './block'
+import { extend, isFunction, remove as removeItem } from '@vue/shared'
 import { type RawProps, rawPropsProxyHandlers } from './componentProps'
-import type { RawSlots } from './componentSlots'
+import type { RawSlots, VaporSlot } from './componentSlots'
+import { renderEffect } from './renderEffect'
 
 const vaporInteropImpl: Omit<
   VaporInteropInterface,
-  'vdomMount' | 'vdomUnmount'
+  'vdomMount' | 'vdomUnmount' | 'vdomSlot'
 > = {
   mount(vnode, container, anchor, parentComponent) {
     const selfAnchor = (vnode.anchor = document.createComment('vapor'))
     container.insertBefore(selfAnchor, anchor)
     const prev = currentInstance
     simpleSetCurrentInstance(parentComponent)
+
     const propsRef = shallowRef(vnode.props)
+    const slotsRef = shallowRef(vnode.children)
+
     // @ts-expect-error
-    const instance = (vnode.component = createComponent(vnode.type, {
-      $: [() => propsRef.value],
-    }))
+    const instance = (vnode.component = createComponent(
+      vnode.type as any as VaporComponent,
+      {
+        $: [() => propsRef.value],
+      } as RawProps,
+      {
+        _: slotsRef, // pass the slots ref
+      } as any as RawSlots,
+    ))
     instance.rawPropsRef = propsRef
+    instance.rawSlotsRef = slotsRef
     mountComponent(instance, container, selfAnchor)
     simpleSetCurrentInstance(prev)
     return instance
@@ -46,8 +62,9 @@ const vaporInteropImpl: Omit<
   update(n1, n2, shouldUpdate) {
     n2.component = n1.component
     if (shouldUpdate) {
-      ;(n2.component as any as VaporComponentInstance).rawPropsRef!.value =
-        n2.props
+      const instance = n2.component as any as VaporComponentInstance
+      instance.rawPropsRef!.value = n2.props
+      instance.rawSlotsRef!.value = n2.children
     }
   },
 
@@ -109,8 +126,66 @@ function createVDOMComponent(
   }
   frag.remove = () => {
     internals.umt(vnode.component!, null, true)
-    remove(parentInstance.vdomChildren!, vnode.component)
-    isMounted = false
+    removeItem(parentInstance.vdomChildren!, vnode.component)
+  }
+
+  return frag
+}
+
+function renderVDOMSlot(
+  internals: RendererInternals,
+  slotsRef: ShallowRef<Slots>,
+  name: string | (() => string),
+  props: Record<string, any>,
+  parentComponent: VaporComponentInstance,
+  fallback?: VaporSlot,
+): VaporFragment {
+  const frag = new VaporFragment([])
+
+  let isMounted = false
+  let fallbackNodes: Block | undefined
+  let parentNode: ParentNode
+  let oldVNode: VNode | null = null
+
+  frag.insert = (parent, anchor) => {
+    parentNode = parent
+    if (!isMounted) {
+      renderEffect(() => {
+        const vnode = renderSlot(
+          slotsRef.value,
+          isFunction(name) ? name() : name,
+          props,
+        )
+        if ((vnode.children as any[]).length) {
+          if (fallbackNodes) {
+            remove(fallbackNodes, parentNode)
+            fallbackNodes = undefined
+          }
+          internals.p(oldVNode, vnode, parent, anchor, parentComponent as any)
+          oldVNode = vnode
+        } else {
+          if (fallback && !fallbackNodes) {
+            // mount fallback
+            if (oldVNode) {
+              internals.um(oldVNode, parentComponent as any, null, true)
+            }
+            insert((fallbackNodes = fallback(props)), parent, anchor)
+          }
+          oldVNode = null
+        }
+      })
+      isMounted = true
+    } else {
+      // TODO move
+    }
+
+    frag.remove = () => {
+      if (fallbackNodes) {
+        remove(fallbackNodes, parentNode)
+      } else if (oldVNode) {
+        internals.um(oldVNode, parentComponent as any, null)
+      }
+    }
   }
 
   return frag
@@ -121,5 +196,6 @@ export const vaporInteropPlugin: Plugin = app => {
   app._context.vapor = extend(vaporInteropImpl, {
     vdomMount: createVDOMComponent.bind(null, internals),
     vdomUnmount: internals.umt,
+    vdomSlot: renderVDOMSlot.bind(null, internals),
   })
 }