]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): vdom slots in vapor component
authorEvan You <evan@vuejs.org>
Thu, 6 Feb 2025 10:12:54 +0000 (18:12 +0800)
committerEvan You <evan@vuejs.org>
Thu, 6 Feb 2025 10:12:54 +0000 (18:12 +0800)
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/hmr.ts
packages/runtime-core/src/renderer.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/vdomInterop.ts

index 66dac629181dc69910af7e21da6d6b5a4eb16b33..a76758d4841e94c6a48aaef64f8e4f27a01fd2bd 100644 (file)
@@ -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
 }
 
 /**
index 738c51aa6be65acbb8ffccd38328cef8b476f4e6..a043ab8d5140cadb92e512b4dded0b3c076c48c1 100644 (file)
@@ -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
index 6fd6bb3f2eacfa7e7bf0d2e9be10a49b7bd1f636..ed5d8b081a0ff8d9eddc149f21d37938501e9c59 100644 (file)
@@ -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)
         })
index 60bc81f0c339a4e291d8bc6524f75f99c1e564ce..d711218886ab9a0206470266daf74bfdc6716a25 100644 (file)
@@ -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
+    }
   }
 }
 
index b2207f7d48a409a23db7b1fcea1120fab65b8896..e46390f46104bc4d12d46b5d4957f772b4b2f1f9 100644 (file)
@@ -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
 
index 02a6ebfbbe0bbd8bd252163525c5deb6013e7fb0..b67b6e98e80c99eb5f825a155c94cb3a10548948 100644 (file)
@@ -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) {
index 7b9ee19cbfe0385fa8f0c170f901eaf73403f052..d258066d6316c133b859c8475127409ce9782770 100644 (file)
@@ -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),
   })
 }