parentComponent: any,
props?: any,
slots?: any,
+ isSingleRoot?: boolean,
) => any
vdomUnmount: UnmountComponentFn
vdomSlot: (
parentComponent: any, // VaporComponentInstance
fallback?: any, // VaporSlot
) => any
+ vdomMountVNode: (
+ vnode: VNode,
+ parentComponent: any, // VaporComponentInstance
+ ) => any
}
/**
resolveComponent,
resolveDirective,
resolveDynamicComponent,
+ NULL_DYNAMIC_COMPONENT,
} from './helpers/resolveAssets'
// For integration with runtime compiler
export { registerRuntimeCompiler, isRuntimeOnly } from './component'
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
})
+ test('nested components (VDOM -> Vapor(multi-root) -> VDOM)', async () => {
+ const data = ref('foo')
+ const { container } = await testWithVDOMApp(
+ `<script setup>const data = _data; const components = _components;</script>
+ <template>
+ <components.VaporChild/>
+ </template>`,
+ {
+ // Vapor component with multiple root nodes, VDOM child as first element
+ // This ensures hydration starts at <!--[--> and tests skipFragmentAnchor
+ VaporChild: {
+ code: `<template><components.VdomChild/><div>second</div></template>`,
+ vapor: true,
+ },
+ VdomChild: {
+ code: `<script setup>const data = _data;</script>
+ <template><span>{{ data }}</span></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><span>foo</span><div>second</div><!--]-->
+ "
+ `,
+ )
+
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+ data.value = 'bar'
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><span>bar</span><div>second</div><!--]-->
+ "
+ `,
+ )
+ })
+
test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
const data = ref(true)
const { container } = await testWithVDOMApp(
)
})
+ test('vapor slot render vdom component (multi-root slot content)', async () => {
+ const data = ref('foo')
+ const { container } = await testWithVaporApp(
+ `<script setup>const data = _data; const components = _components;</script>
+ <template>
+ <components.VaporChild>
+ <components.VdomChild/>
+ <div>vapor content</div>
+ </components.VaporChild>
+ </template>`,
+ {
+ VaporChild: {
+ code: `<template><div><slot/></div></template>`,
+ vapor: true,
+ },
+ VdomChild: {
+ code: `<script setup>const data = _data;</script>
+ <template><span>{{ data }}</span></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "<div>
+ <!--[--><span>foo</span><div>vapor content</div><!--]-->
+ </div>"
+ `,
+ )
+
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+ data.value = 'bar'
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "<div>
+ <!--[--><span>bar</span><div>vapor content</div><!--]-->
+ </div>"
+ `,
+ )
+ })
+
test('vapor slot render vdom component (render function)', async () => {
const data = ref(true)
const { container } = await testWithVaporApp(
`,
)
})
+
+ test('hydrate VNode rendered via createDynamicComponent', async () => {
+ const data = ref('foo')
+ const { container } = await testWithVaporApp(
+ `<script setup>
+ import { h } from 'vue'
+ const data = _data; const components = _components;
+
+ // Simulating RouterView pattern: VDOM component passes VNode through slot
+ const RouterView = {
+ setup(_, { slots }) {
+ return () => {
+ const component = h(components.VaporChild)
+ return slots.default({ Component: component })
+ }
+ }
+ }
+ </script>
+ <template>
+ <RouterView v-slot="{ Component }">
+ <component :is="Component" />
+ </RouterView>
+ </template>`,
+ {
+ VaporChild: {
+ code: `<template><div>{{ data }}</div></template>`,
+ vapor: true,
+ },
+ },
+ data,
+ )
+
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><div>foo</div><!--dynamic-component--><!--]-->
+ "
+ `,
+ )
+
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+ data.value = 'bar'
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><div>bar</div><!--dynamic-component--><!--]-->
+ "
+ `,
+ )
+ })
+
+ test('hydrate VDOM slot content', async () => {
+ const data = ref('foo')
+ const { container } = await testWithVaporApp(
+ `<script setup>
+ const data = _data; const components = _components;
+ </script>
+ <template>
+ <components.VdomWrapper>
+ <div>{{ data }}</div>
+ </components.VdomWrapper>
+ </template>`,
+ {
+ VdomWrapper: {
+ code: `<script setup>const data = _data;</script>
+ <template><slot /></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><div>foo</div><!--]-->
+ "
+ `,
+ )
+
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+ data.value = 'bar'
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><div>bar</div><!--]-->
+ "
+ `,
+ )
+ })
+
+ test('hydrate VDOM slot fallback', async () => {
+ const data = ref('foo')
+ const { container } = await testWithVaporApp(
+ `<script setup>
+ const data = _data; const components = _components;
+ </script>
+ <template>
+ <components.VdomWrapper />
+ </template>`,
+ {
+ VdomWrapper: {
+ code: `<script setup>const data = _data;</script>
+ <template><slot><div>{{ data }}</div></slot></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><div>foo</div><!--]-->
+ "
+ `,
+ )
+
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+ data.value = 'bar'
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><div>bar</div><!--]-->
+ "
+ `,
+ )
+ })
+
+ test('hydrate VDOM component returning Fragment', async () => {
+ const data = ref('foo')
+ const { container } = await testWithVaporApp(
+ `<script setup>
+ const data = _data; const components = _components;
+ </script>
+ <template>
+ <components.VdomFragmentComp />
+ </template>`,
+ {
+ // VDOM component that returns a Fragment (multiple root nodes)
+ VdomFragmentComp: {
+ code: `<script setup>const data = _data;</script>
+ <template><div>first {{ data }}</div><div>second {{ data }}</div></template>`,
+ vapor: false,
+ },
+ },
+ data,
+ )
+
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><div>first foo</div><div>second foo</div><!--]-->
+ "
+ `,
+ )
+
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+ data.value = 'bar'
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><div>first bar</div><div>second bar</div><!--]-->
+ "
+ `,
+ )
+ })
})
} from '@vue/runtime-dom'
import { makeInteropRender } from './_utils'
import {
+ VaporKeepAlive,
applyTextModel,
applyVShow,
child,
createComponent,
+ createDynamicComponent,
defineVaporAsyncComponent,
defineVaporComponent,
renderEffect,
await nextTick()
expect(html()).toBe('<div>vapor child</div>')
})
+
+ describe('render VNodes', () => {
+ it('should render VNode containing vapor component from VDOM slot', async () => {
+ const VaporComp = defineVaporComponent({
+ setup() {
+ return template('<div>vapor comp</div>')() as any
+ },
+ })
+
+ const RouterView = defineComponent({
+ setup(_, { slots }) {
+ return () => {
+ const component = h(VaporComp as any)
+ return slots.default!({ Component: component })
+ }
+ },
+ })
+
+ const App = defineVaporComponent({
+ setup() {
+ return createComponent(
+ RouterView as any,
+ null,
+ {
+ default: (slotProps: { Component: any }) => {
+ return createDynamicComponent(() => slotProps.Component)
+ },
+ },
+ true,
+ )
+ },
+ })
+
+ const { html } = define({
+ setup() {
+ return () => h(App as any)
+ },
+ }).render()
+
+ expect(html()).toBe('<div>vapor comp</div><!--dynamic-component-->')
+ })
+
+ it('should render VNode containing vdom component from VDOM slot', async () => {
+ const VdomComp = defineComponent({
+ setup() {
+ return () => h('div', 'vdom comp')
+ },
+ })
+
+ const RouterView = defineComponent({
+ setup(_, { slots }) {
+ return () => {
+ const component = h(VdomComp)
+ return slots.default!({ Component: component })
+ }
+ },
+ })
+
+ const App = defineVaporComponent({
+ setup() {
+ return createComponent(
+ RouterView as any,
+ null,
+ {
+ default: (slotProps: { Component: any }) => {
+ return createDynamicComponent(() => slotProps.Component)
+ },
+ },
+ true,
+ )
+ },
+ })
+
+ const { html } = define({
+ setup() {
+ return () => h(App as any)
+ },
+ }).render()
+
+ expect(html()).toBe('<div>vdom comp</div><!--dynamic-component-->')
+ })
+
+ it('should update when VNode changes', async () => {
+ const VaporCompA = defineVaporComponent({
+ setup() {
+ return template('<div>vapor A</div>')() as any
+ },
+ })
+
+ const VaporCompB = defineVaporComponent({
+ setup() {
+ return template('<div>vapor B</div>')() as any
+ },
+ })
+
+ const current = shallowRef<any>(VaporCompA)
+
+ const RouterView = defineComponent({
+ setup(_, { slots }) {
+ return () => {
+ const component = h(current.value as any)
+ return slots.default!({ Component: component })
+ }
+ },
+ })
+
+ const App = defineVaporComponent({
+ setup() {
+ return createComponent(
+ RouterView as any,
+ null,
+ {
+ default: (slotProps: { Component: any }) => {
+ return createDynamicComponent(() => slotProps.Component)
+ },
+ },
+ true,
+ )
+ },
+ })
+
+ const { html } = define({
+ setup() {
+ return () => h(App as any)
+ },
+ }).render()
+
+ expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
+ current.value = VaporCompB
+ await nextTick()
+ expect(html()).toBe('<div>vapor B</div><!--dynamic-component-->')
+ })
+
+ describe('with VaporKeepAlive', () => {
+ it('switch VNode with inner vapor components', async () => {
+ const hooksA = {
+ mounted: vi.fn(),
+ activated: vi.fn(),
+ deactivated: vi.fn(),
+ unmounted: vi.fn(),
+ }
+ const hooksB = {
+ mounted: vi.fn(),
+ activated: vi.fn(),
+ deactivated: vi.fn(),
+ unmounted: vi.fn(),
+ }
+
+ const VaporCompA = defineVaporComponent({
+ setup() {
+ onMounted(() => hooksA.mounted())
+ onActivated(() => hooksA.activated())
+ onDeactivated(() => hooksA.deactivated())
+ onUnmounted(() => hooksA.unmounted())
+ return template('<div>vapor A</div>')() as any
+ },
+ })
+
+ const VaporCompB = defineVaporComponent({
+ setup() {
+ onMounted(() => hooksB.mounted())
+ onActivated(() => hooksB.activated())
+ onDeactivated(() => hooksB.deactivated())
+ onUnmounted(() => hooksB.unmounted())
+ return template('<div>vapor B</div>')() as any
+ },
+ })
+
+ const current = shallowRef<any>(VaporCompA)
+
+ const RouterView = defineComponent({
+ setup(_, { slots }) {
+ return () => {
+ const component = h(current.value as any)
+ return slots.default!({ Component: component })
+ }
+ },
+ })
+
+ const App = defineVaporComponent({
+ setup() {
+ return createComponent(
+ RouterView as any,
+ null,
+ {
+ default: (slotProps: { Component: any }) => {
+ return createComponent(VaporKeepAlive, null, {
+ default: () =>
+ createDynamicComponent(() => slotProps.Component),
+ })
+ },
+ },
+ true,
+ )
+ },
+ })
+
+ const { html } = define({
+ setup() {
+ return () => h(App as any)
+ },
+ }).render()
+
+ expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
+ // A: mounted + activated
+ expect(hooksA.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksA.activated).toHaveBeenCalledTimes(1)
+ expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
+ expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
+
+ current.value = VaporCompB
+ await nextTick()
+ expect(html()).toBe('<div>vapor B</div><!--dynamic-component-->')
+ // A: deactivated (cached)
+ expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
+ expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
+ // B: mounted + activated
+ expect(hooksB.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksB.activated).toHaveBeenCalledTimes(1)
+
+ current.value = VaporCompA
+ await nextTick()
+ expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
+ // B: deactivated (cached)
+ expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
+ expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
+ // A: re-activated (not re-mounted)
+ expect(hooksA.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksA.activated).toHaveBeenCalledTimes(2)
+ })
+
+ it('switch VNode with inner VDOM components', async () => {
+ const hooksA = {
+ mounted: vi.fn(),
+ activated: vi.fn(),
+ deactivated: vi.fn(),
+ unmounted: vi.fn(),
+ }
+ const hooksB = {
+ mounted: vi.fn(),
+ activated: vi.fn(),
+ deactivated: vi.fn(),
+ unmounted: vi.fn(),
+ }
+
+ const VDOMCompA = defineComponent({
+ setup() {
+ onMounted(() => hooksA.mounted())
+ onActivated(() => hooksA.activated())
+ onDeactivated(() => hooksA.deactivated())
+ onUnmounted(() => hooksA.unmounted())
+ return () => h('div', 'vdom A')
+ },
+ })
+
+ const VDOMCompB = defineComponent({
+ setup() {
+ onMounted(() => hooksB.mounted())
+ onActivated(() => hooksB.activated())
+ onDeactivated(() => hooksB.deactivated())
+ onUnmounted(() => hooksB.unmounted())
+ return () => h('div', 'vdom B')
+ },
+ })
+
+ const current = shallowRef<any>(VDOMCompA)
+
+ const RouterView = defineComponent({
+ setup(_, { slots }) {
+ return () => {
+ const component = h(current.value as any)
+ return slots.default!({ Component: component })
+ }
+ },
+ })
+
+ const App = defineVaporComponent({
+ setup() {
+ return createComponent(
+ RouterView as any,
+ null,
+ {
+ default: (slotProps: { Component: any }) => {
+ return createComponent(VaporKeepAlive, null, {
+ default: () =>
+ createDynamicComponent(() => slotProps.Component),
+ })
+ },
+ },
+ true,
+ )
+ },
+ })
+
+ const { html } = define({
+ setup() {
+ return () => h(App as any)
+ },
+ }).render()
+
+ expect(html()).toBe('<div>vdom A</div><!--dynamic-component-->')
+ // A: mounted + activated
+ expect(hooksA.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksA.activated).toHaveBeenCalledTimes(1)
+ expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
+ expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
+
+ current.value = VDOMCompB
+ await nextTick()
+ expect(html()).toBe('<div>vdom B</div><!--dynamic-component-->')
+ // A: deactivated (cached)
+ expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
+ expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
+ // B: mounted + activated
+ expect(hooksB.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksB.activated).toHaveBeenCalledTimes(1)
+
+ current.value = VDOMCompA
+ await nextTick()
+ expect(html()).toBe('<div>vdom A</div><!--dynamic-component-->')
+ // B: deactivated (cached)
+ expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
+ expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
+ // A: re-activated (not re-mounted)
+ expect(hooksA.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksA.activated).toHaveBeenCalledTimes(2)
+ })
+
+ it('switch VNode with inner mixed vapor/VDOM components', async () => {
+ const hooksA = {
+ mounted: vi.fn(),
+ activated: vi.fn(),
+ deactivated: vi.fn(),
+ unmounted: vi.fn(),
+ }
+ const hooksB = {
+ mounted: vi.fn(),
+ activated: vi.fn(),
+ deactivated: vi.fn(),
+ unmounted: vi.fn(),
+ }
+
+ const VaporCompA = defineVaporComponent({
+ setup() {
+ onMounted(() => hooksA.mounted())
+ onActivated(() => hooksA.activated())
+ onDeactivated(() => hooksA.deactivated())
+ onUnmounted(() => hooksA.unmounted())
+ return template('<div>vapor A</div>')()
+ },
+ })
+
+ const VDOMCompB = defineComponent({
+ setup() {
+ onMounted(() => hooksB.mounted())
+ onActivated(() => hooksB.activated())
+ onDeactivated(() => hooksB.deactivated())
+ onUnmounted(() => hooksB.unmounted())
+ return () => h('div', 'vdom B')
+ },
+ })
+
+ const current = shallowRef<any>(VaporCompA)
+
+ const RouterView = defineComponent({
+ setup(_, { slots }) {
+ return () => {
+ const component = h(current.value as any)
+ return slots.default!({ Component: component })
+ }
+ },
+ })
+
+ const App = defineVaporComponent({
+ setup() {
+ return createComponent(
+ RouterView as any,
+ null,
+ {
+ default: (slotProps: { Component: any }) => {
+ return createComponent(VaporKeepAlive, null, {
+ default: () =>
+ createDynamicComponent(() => slotProps.Component),
+ })
+ },
+ },
+ true,
+ )
+ },
+ })
+
+ const { html } = define({
+ setup() {
+ return () => h(App as any)
+ },
+ }).render()
+
+ expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
+ // A (vapor): mounted + activated
+ expect(hooksA.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksA.activated).toHaveBeenCalledTimes(1)
+ expect(hooksA.deactivated).toHaveBeenCalledTimes(0)
+ expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
+
+ current.value = VDOMCompB
+ await nextTick()
+ expect(html()).toBe('<div>vdom B</div><!--dynamic-component-->')
+ // A (vapor): deactivated (cached)
+ expect(hooksA.deactivated).toHaveBeenCalledTimes(1)
+ expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
+ // B (vdom): mounted + activated
+ expect(hooksB.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksB.activated).toHaveBeenCalledTimes(1)
+
+ current.value = VaporCompA
+ await nextTick()
+ expect(html()).toBe('<div>vapor A</div><!--dynamic-component-->')
+ // B (vdom): deactivated (cached)
+ expect(hooksB.deactivated).toHaveBeenCalledTimes(1)
+ expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
+ // A (vapor): re-activated (not re-mounted)
+ expect(hooksA.mounted).toHaveBeenCalledTimes(1)
+ expect(hooksA.activated).toHaveBeenCalledTimes(2)
+ })
+ })
+ })
})
describe('attribute fallthrough', () => {
-import { currentInstance, resolveDynamicComponent } from '@vue/runtime-dom'
+import {
+ currentInstance,
+ isKeepAlive,
+ isVNode,
+ resolveDynamicComponent,
+} from '@vue/runtime-dom'
import { insert, isBlock } from './block'
import { createComponentWithFallback, emptyContext } from './component'
import { renderEffect } from './renderEffect'
} from './insertionState'
import { advanceHydrationNode, isHydrating } from './dom/hydration'
import { DynamicFragment, type VaporFragment } from './fragment'
+import type { KeepAliveInstance } from './components/KeepAlive'
export function createDynamicComponent(
getter: () => any,
const value = getter()
const appContext =
(currentInstance && currentInstance.appContext) || emptyContext
- frag.update(
- () =>
- // Support integration with VaporRouterView/VaporRouterLink by accepting blocks
- isBlock(value)
- ? value
- : createComponentWithFallback(
- resolveDynamicComponent(value) as any,
- rawProps,
- rawSlots,
- isSingleRoot,
- once,
- appContext,
- ),
- value,
- )
+ frag.update(() => {
+ // Support integration with VaporRouterView/VaporRouterLink by accepting blocks
+ if (isBlock(value)) return value
+
+ // Handles VNodes passed from VDOM components (e.g., `h(VaporComp)` from slots)
+ if (appContext.vapor && isVNode(value)) {
+ if (isKeepAlive(currentInstance)) {
+ const frag = (
+ currentInstance as KeepAliveInstance
+ ).ctx.getCachedComponent(value.type as any) as VaporFragment
+ if (frag) return frag
+ }
+
+ const frag = appContext.vapor.vdomMountVNode(value, currentInstance)
+ if (isHydrating) {
+ frag.hydrate()
+ if (_isLastInsertion) {
+ advanceHydrationNode(_insertionParent!)
+ }
+ }
+ return frag
+ }
+
+ return createComponentWithFallback(
+ resolveDynamicComponent(value) as any,
+ rawProps,
+ rawSlots,
+ isSingleRoot,
+ once,
+ appContext,
+ )
+ }, value)
}
if (once) renderFn()
type GenericAppContext,
type GenericComponentInstance,
type LifecycleHook,
+ NULL_DYNAMIC_COMPONENT,
type NormalizedPropsOptions,
type ObjectEmitsOptions,
type ShallowUnwrapRef,
locateNextNode,
setCurrentHydrationNode,
} from './dom/hydration'
-import { _next, createElement } from './dom/node'
+import { _next, createComment, createElement, createTextNode } from './dom/node'
import {
type TeleportFragment,
isTeleportFragment,
currentInstance as any,
rawProps,
rawSlots,
+ isSingleRoot,
)
if (!isHydrating) {
if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
* element if the resolution fails.
*/
export function createComponentWithFallback(
- comp: VaporComponent | string,
+ comp: VaporComponent | typeof NULL_DYNAMIC_COMPONENT | string,
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
isSingleRoot?: boolean,
once?: boolean,
appContext?: GenericAppContext,
): HTMLElement | VaporComponentInstance {
+ if (comp === NULL_DYNAMIC_COMPONENT) {
+ return (__DEV__
+ ? createComment('ndc')
+ : createTextNode('')) as any as HTMLElement
+ }
+
if (!isString(comp)) {
return createComponent(
comp,
type App,
type ComponentInternalInstance,
type ConcreteComponent,
- Fragment,
type HydrationRenderer,
type KeepAliveContext,
MoveType,
ensureVaporSlotFallback,
isEmitListener,
isKeepAlive,
- isRef,
isVNode,
normalizeRef,
onScopeDispose,
VaporComponentInstance,
createComponent,
getCurrentScopeId,
- isVaporComponent,
mountComponent,
unmountComponent,
} from './component'
// mounting vapor components and slots in vdom
const vaporInteropImpl: Omit<
VaporInteropInterface,
- 'vdomMount' | 'vdomUnmount' | 'vdomSlot'
+ 'vdomMount' | 'vdomUnmount' | 'vdomSlot' | 'vdomMountVNode'
> = {
mount(vnode, container, anchor, parentComponent, parentSuspense) {
- let selfAnchor = (vnode.el = vnode.anchor = createTextNode())
+ let selfAnchor = (vnode.anchor = createTextNode())
if (isHydrating) {
// avoid vdom hydration children mismatch by the selfAnchor, delay its insertion
queuePostFlushCb(() => container.insertBefore(selfAnchor, anchor))
} else {
+ vnode.el = selfAnchor
container.insertBefore(selfAnchor, anchor)
}
const prev = currentInstance
)
}
})
- return _next(vnode.anchor as Node)
+ return vnode.anchor as Node
},
setTransitionHooks(component, hooks) {
const cached = (parentComponent.ctx as KeepAliveContext).getCachedComponent(
vnode,
)
-
vnode.el = cached.el
vnode.component = cached.component
vnode.anchor = cached.anchor
let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined
+/**
+ * Mount VNode in vapor
+ */
+function mountVNode(
+ internals: RendererInternals,
+ vnode: VNode,
+ parentComponent: VaporComponentInstance | null,
+): VaporFragment {
+ const frag = new VaporFragment([])
+ frag.vnode = vnode
+
+ let isMounted = false
+ const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => {
+ if (transition) setVNodeTransitionHooks(vnode, transition)
+ if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
+ if ((vnode.type as any).__vapor) {
+ deactivate(
+ vnode.component as any,
+ (parentComponent as KeepAliveInstance)!.ctx.getStorageContainer(),
+ )
+ } else {
+ vdomDeactivate(
+ vnode,
+ (parentComponent as KeepAliveInstance)!.ctx.getStorageContainer(),
+ internals,
+ parentComponent as any,
+ null,
+ )
+ }
+ } else {
+ internals.um(vnode, parentComponent as any, null, !!parentNode)
+ }
+ }
+
+ frag.hydrate = () => {
+ hydrateVNode(vnode, parentComponent as any)
+ onScopeDispose(unmount, true)
+ isMounted = true
+ frag.nodes = vnode.el as any
+ }
+
+ frag.insert = (parentNode, anchor, transition) => {
+ if (isHydrating) return
+ if (vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
+ if ((vnode.type as any).__vapor) {
+ activate(vnode.component as any, parentNode, anchor)
+ } else {
+ vdomActivate(
+ vnode,
+ parentNode,
+ anchor,
+ internals,
+ parentComponent as any,
+ null,
+ undefined,
+ false,
+ )
+ }
+ return
+ } else {
+ const prev = currentInstance
+ simpleSetCurrentInstance(parentComponent)
+ if (!isMounted) {
+ if (transition) setVNodeTransitionHooks(vnode, transition)
+ internals.p(
+ null,
+ vnode,
+ parentNode,
+ anchor,
+ parentComponent as any,
+ null, // parentSuspense
+ undefined, // namespace
+ vnode.slotScopeIds,
+ )
+ onScopeDispose(unmount, true)
+ isMounted = true
+ } else {
+ // move
+ internals.m(
+ vnode,
+ parentNode,
+ anchor,
+ MoveType.REORDER,
+ parentComponent as any,
+ )
+ }
+ simpleSetCurrentInstance(prev)
+ }
+ frag.nodes = vnode.el as any
+ if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
+ }
+
+ frag.remove = unmount
+ return frag
+}
+
/**
* Mount vdom component in vapor
*/
parentComponent: VaporComponentInstance | null,
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
+ isSingleRoot?: boolean,
): VaporFragment {
const frag = new VaporFragment([])
const vnode = (frag.vnode = createVNode(
}
frag.hydrate = () => {
- hydrateVNode(vnode, parentComponent as any)
+ hydrateVNode(
+ vnode,
+ parentComponent as any,
+ // skip fragment start anchor for multi-root component
+ // to avoid mismatch
+ !isSingleRoot,
+ )
onScopeDispose(unmount, true)
isMounted = true
frag.nodes = vnode.el as any
vdomMount: createVDOMComponent.bind(null, internals),
vdomUnmount: internals.umt,
vdomSlot: renderVDOMSlot.bind(null, internals),
+ vdomMountVNode: mountVNode.bind(null, internals),
})
const mount = app.mount
app.mount = ((...args) => {
function hydrateVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
+ skipFragmentAnchor: boolean = false,
) {
locateHydrationNode()
- // skip fragment start anchor
let node = currentHydrationNode!
- while (
- isComment(node, '[') &&
- // vnode is not a fragment
- vnode.type !== Fragment &&
- // not inside vdom slot
- !(
- isVaporComponent(parentComponent) &&
- isRef((parentComponent as VaporComponentInstance).rawSlots._)
- )
- ) {
- node = node.nextSibling!
+ if (skipFragmentAnchor && isComment(node, '[')) {
+ setCurrentHydrationNode((node = node.nextSibling!))
}
- if (currentHydrationNode !== node) setCurrentHydrationNode(node)
if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
const nextNode = vdomHydrateNode(
- currentHydrationNode!,
+ node,
vnode,
parentComponent,
null,