From: daiwei Date: Tue, 21 Oct 2025 05:52:00 +0000 (+0800) Subject: chore: merge minor X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F13422%2Fhead;p=thirdparty%2Fvuejs%2Fcore.git chore: merge minor --- 13386832b6089e1838d019bbd2807de7a6696edd diff --cc packages/compiler-vapor/src/ir/index.ts index c08de38081,76ef7c53c4..894cb5d912 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@@ -196,7 -205,8 +205,9 @@@ export interface CreateComponentIRNode dynamic?: SimpleExpressionNode parent?: number anchor?: number + scopeId?: string | null + append?: boolean + last?: boolean } export interface DeclareOldRefIRNode extends BaseIRNode { diff --cc packages/runtime-core/src/apiCreateApp.ts index 531496fb74,caa39c4436..b0712e8962 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@@ -187,13 -187,27 +187,32 @@@ export interface VaporInteropInterface unmount(vnode: VNode, doRemove?: boolean): void move(vnode: VNode, container: any, anchor: any): void slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void + hydrate( + vnode: VNode, + node: any, + container: any, + anchor: any, + parentComponent: ComponentInternalInstance | null, + ): Node + hydrateSlot(vnode: VNode, node: any): Node + activate( + vnode: VNode, + container: any, + anchor: any, + parentComponent: ComponentInternalInstance, + ): void + deactivate(vnode: VNode, container: any): void + setTransitionHooks( + component: ComponentInternalInstance, + transition: TransitionHooks, + ): void - vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any + vdomMount: ( + component: ConcreteComponent, + props?: any, + slots?: any, + scopeId?: string, + ) => any vdomUnmount: UnmountComponentFn vdomSlot: ( slots: any, diff --cc packages/runtime-core/src/index.ts index 8e427d7cda,b15fe1e696..97ad176311 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@@ -505,11 -516,7 +516,12 @@@ export { type VaporInteropInterface } f /** * @internal */ -export { type RendererInternals, MoveType, invalidateMount } from './renderer' +export { + type RendererInternals, + MoveType, + getInheritedScopeIds, ++ invalidateMount, +} from './renderer' /** * @internal */ diff --cc packages/runtime-vapor/__tests__/componentSlots.spec.ts index 9528e91de1,1beeecf2d0..e3b0c72a63 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@@ -550,972 -554,1349 +554,2316 @@@ describe('component: slots', () => await nextTick() expect(host.innerHTML).toBe('

') }) + + test('render fallback when slot content is not valid', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + return template('')() + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + }) + + test('render fallback when v-if condition is false', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const toggle = ref(false) + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + return createIf( + () => toggle.value, + () => { + return document.createTextNode('content') + }, + ) + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + + toggle.value = true + await nextTick() + expect(html()).toBe('content') + + toggle.value = false + await nextTick() + expect(html()).toBe('fallback') + }) + + test('render fallback with nested v-if', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const outerShow = ref(false) + const innerShow = ref(false) + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + return createIf( + () => outerShow.value, + () => { + return createIf( + () => innerShow.value, + () => { + return document.createTextNode('content') + }, + ) + }, + ) + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + + outerShow.value = true + await nextTick() + expect(html()).toBe('fallback') + + innerShow.value = true + await nextTick() + expect(html()).toBe('content') + + innerShow.value = false + await nextTick() + expect(html()).toBe('fallback') + + outerShow.value = false + await nextTick() + expect(html()).toBe('fallback') + + outerShow.value = true + await nextTick() + expect(html()).toBe('fallback') + + innerShow.value = true + await nextTick() + expect(html()).toBe('content') + }) + + test('render fallback with v-for', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const items = ref([1]) + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + const n2 = createFor( + () => items.value, + for_item0 => { + const n4 = template(' ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(for_item0.value)), + ) + return n4 + }, + ) + return n2 + }, + }) + }, + }).render() + + expect(html()).toBe('1') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.push(2) + await nextTick() + expect(html()).toBe('2') + }) + + test('render fallback with v-for (empty source)', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const items = ref([]) + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + const n2 = createFor( + () => items.value, + for_item0 => { + const n4 = template(' ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(for_item0.value)), + ) + return n4 + }, + ) + return n2 + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + + items.value.push(1) + await nextTick() + expect(html()).toBe('1') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.push(2) + await nextTick() + expect(html()).toBe('2') + }) + }) + + describe('forwarded slot', () => { + test('should work', async () => { + const Child = defineVaporComponent({ + setup() { + return createSlot('foo', null) + }, + }) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + Child, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const foo = ref('foo') + const { host } = define({ + setup() { + const n2 = createComponent( + Parent, + null, + { + foo: () => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, foo.value)) + return n0 + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(host.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(host.innerHTML).toBe('bar') + }) + + test('mixed with non-forwarded slot', async () => { + const Child = defineVaporComponent({ + setup() { + return [createSlot('foo', null)] + }, + }) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent(Child, null, { + foo: () => { + const n0 = createForwardedSlot('foo', null) + return n0 + }, + }) + const n3 = createSlot('default', null) + return [n2, n3] + }, + }) + + const foo = ref('foo') + const { host } = define({ + setup() { + const n2 = createComponent( + Parent, + null, + { + foo: () => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, foo.value)) + return n0 + }, + default: () => { + const n3 = template(' ')() as any + renderEffect(() => setText(n3, foo.value)) + return n3 + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(host.innerHTML).toBe('foofoo') + + foo.value = 'bar' + await nextTick() + expect(host.innerHTML).toBe('barbar') + }) + + test('forwarded slot with fallback', async () => { + const Child = defineVaporComponent({ + setup() { + return createSlot('default', null, () => template('child fallback')()) + }, + }) + + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent(Child, null, { + default: () => { + const n0 = createForwardedSlot('default', null, () => { + return template('')() + }) + return n0 + }, + }) + return n2 + }, + }) + + const { html } = define({ + setup() { + return createComponent(Parent, null, { + default: () => template('')(), + }) + }, + }).render() + + expect(html()).toBe('child fallback') + }) + + test('forwarded slot with fallback (v-if)', async () => { + const Child = defineVaporComponent({ + setup() { + return createSlot('default', null, () => template('child fallback')()) + }, + }) + + const show = ref(false) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent(Child, null, { + default: () => { + const n0 = createForwardedSlot('default', null, () => { + const n2 = createIf( + () => show.value, + () => { + const n4 = template('
if content
')() + return n4 + }, + ) + return n2 + }) + return n0 + }, + }) + return n2 + }, + }) + + const { html } = define({ + setup() { + return createComponent(Parent, null, { + default: () => template('')(), + }) + }, + }).render() + + expect(html()).toBe('child fallback') + + show.value = true + await nextTick() + expect(html()).toBe( + '
if content
', + ) + }) + + test('forwarded slot with fallback (v-for)', async () => { + const Child = defineVaporComponent({ + setup() { + return createSlot('default', null, () => template('child fallback')()) + }, + }) + + const items = ref([]) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent(Child, null, { + default: () => { + const n0 = createForwardedSlot('default', null, () => { + const n2 = createFor( + () => items.value, + for_item0 => { + const n4 = template(' ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(for_item0.value)), + ) + return n4 + }, + ) + return n2 + }) + return n0 + }, + }) + return n2 + }, + }) + + const { html } = define({ + setup() { + return createComponent(Parent, null, { + default: () => template('')(), + }) + }, + }).render() + + expect(html()).toBe('child fallback') + + items.value.push(1) + await nextTick() + expect(html()).toBe('1') + + items.value.pop() + await nextTick() + expect(html()).toBe('child fallback') + }) + + describe('vdom interop', () => { + const createVaporSlot = (fallbackText = 'fallback') => { + return defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template(`
${fallbackText}
`)() + return n2 + }) + return n0 + }, + }) + } + + const createVdomSlot = (fallbackText = 'fallback') => { + return { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', fallbackText), + ]) + }, + } + } + + const createVaporForwardedSlot = ( + targetComponent: any, + fallbackText?: string, + ) => { + return defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + targetComponent, + null, + { + foo: () => { + return fallbackText + ? createForwardedSlot('foo', null, () => { + const n2 = template(`
${fallbackText}
`)() + return n2 + }) + : createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + } + + const createVdomForwardedSlot = ( + targetComponent: any, + fallbackText?: string, + ) => { + return { + render(this: any) { + return h(targetComponent, null, { + foo: () => [ + fallbackText + ? renderSlot(this.$slots, 'foo', {}, () => [ + h('div', fallbackText), + ]) + : renderSlot(this.$slots, 'foo'), + ], + _: 3 /* FORWARDED */, + }) + }, + } + } + + const createMultipleVaporForwardedSlots = ( + targetComponent: any, + count: number, + ) => { + let current = targetComponent + for (let i = 0; i < count; i++) { + current = createVaporForwardedSlot(current) + } + return current + } + + const createMultipleVdomForwardedSlots = ( + targetComponent: any, + count: number, + ) => { + let current = targetComponent + for (let i = 0; i < count; i++) { + current = createVdomForwardedSlot(current) + } + return current + } + + const createTestApp = ( + rootComponent: any, + foo: Ref, + show: Ref, + ) => { + return { + setup() { + return () => + h( + rootComponent, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + } + + const createEmptyTestApp = (rootComponent: any) => { + return { + setup() { + return () => h(rootComponent) + }, + } + } + + test('vdom slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VaporSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VdomSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomForwardedSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot(empty) > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => { + const VaporSlot = createVaporSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createEmptyTestApp(VaporForwardedSlot) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('
vdom fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomForwardedSlot, + 'vapor fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlot = createMultipleVaporForwardedSlots( + VdomForwardedSlot, + 3, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createMultipleVaporForwardedSlots( + VdomForwardedSlotWithFallback, + 3, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe( + '
vdom fallback
', + ) + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const App = createTestApp(VdomForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const VdomForwardedSlot = createVdomForwardedSlot(VaporForwardedSlot) + const App = createTestApp(VdomForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const VdomForwardedSlot = createMultipleVdomForwardedSlots( + VaporForwardedSlot, + 3, + ) + const App = createTestApp(VdomForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot( + VaporSlot, + 'vapor fallback', + ) + const VdomForwardedSlot = createMultipleVdomForwardedSlots( + VaporForwardedSlot, + 3, + ) + const App = createTestApp(VdomForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot1 = createMultipleVaporForwardedSlots( + VdomSlot, + 2, + ) + const App = createTestApp(VaporForwardedSlot1, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2 = createVaporForwardedSlot(VdomSlot) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VdomSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1 = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + ) + const App = createTestApp(VaporForwardedSlot1, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor2 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot2 = createVaporForwardedSlot(VaporSlot) + const VaporForwardedSlot1 = + createVaporForwardedSlot(VaporForwardedSlot2) + const App = createTestApp(VaporForwardedSlot1, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VdomSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VaporSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe( + '
vapor1 fallback
', + ) + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) (multiple) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot3WithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom3 fallback', + ) + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VdomForwardedSlot3WithFallback, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + }) + + test('consecutive slots with insertion state', async () => { + const { component: Child } = define({ + setup() { + const n2 = template('
baz
', true)() as any + setInsertionState(n2, 0) + createSlot('default', null) + setInsertionState(n2, 0) + createSlot('foo', null) + return n2 + }, + }) + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => template('default')(), + foo: () => template('foo')(), + }) + }, + }).render() + + expect(html()).toBe( + `
` + + `default` + + `foo` + + `
baz
` + + `
`, + ) + }) }) + + describe('forwarded slot', () => { + test('should work', async () => { + const Child = defineVaporComponent({ + setup() { + return createSlot('foo', null) + }, + }) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + Child, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const foo = ref('foo') + const { host } = define({ + setup() { + const n2 = createComponent( + Parent, + null, + { + foo: () => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, foo.value)) + return n0 + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(host.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(host.innerHTML).toBe('bar') + }) + + test('mixed with non-forwarded slot', async () => { + const Child = defineVaporComponent({ + setup() { + return [createSlot('foo', null)] + }, + }) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent(Child, null, { + foo: () => { + const n0 = createForwardedSlot('foo', null) + return n0 + }, + }) + const n3 = createSlot('default', null) + return [n2, n3] + }, + }) + + const foo = ref('foo') + const { host } = define({ + setup() { + const n2 = createComponent( + Parent, + null, + { + foo: () => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, foo.value)) + return n0 + }, + default: () => { + const n3 = template(' ')() as any + renderEffect(() => setText(n3, foo.value)) + return n3 + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(host.innerHTML).toBe('foofoo') + + foo.value = 'bar' + await nextTick() + expect(host.innerHTML).toBe('barbar') + }) + + describe('vdom interop', () => { + const createVaporSlot = (fallbackText = 'fallback') => { + return defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template(`
${fallbackText}
`)() + return n2 + }) + return n0 + }, + }) + } + + const createVdomSlot = (fallbackText = 'fallback') => { + return { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', fallbackText), + ]) + }, + } + } + + const createVaporForwardedSlot = ( + targetComponent: any, + fallbackText?: string, + ) => { + return defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + targetComponent, + null, + { + foo: () => { + return fallbackText + ? createForwardedSlot('foo', null, () => { + const n2 = template(`
${fallbackText}
`)() + return n2 + }) + : createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + } + + const createVdomForwardedSlot = ( + targetComponent: any, + fallbackText?: string, + ) => { + return { + render(this: any) { + return h(targetComponent, null, { + foo: () => [ + fallbackText + ? renderSlot(this.$slots, 'foo', {}, () => [ + h('div', fallbackText), + ]) + : renderSlot(this.$slots, 'foo'), + ], + _: 3 /* FORWARDED */, + }) + }, + } + } + + const createMultipleVaporForwardedSlots = ( + targetComponent: any, + count: number, + ) => { + let current = targetComponent + for (let i = 0; i < count; i++) { + current = createVaporForwardedSlot(current) + } + return current + } + + const createMultipleVdomForwardedSlots = ( + targetComponent: any, + count: number, + ) => { + let current = targetComponent + for (let i = 0; i < count; i++) { + current = createVdomForwardedSlot(current) + } + return current + } + + const createTestApp = ( + rootComponent: any, + foo: Ref, + show: Ref, + ) => { + return { + setup() { + return () => + h( + rootComponent, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + } + + const createEmptyTestApp = (rootComponent: any) => { + return { + setup() { + return () => h(rootComponent) + }, + } + } + + test('vdom slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VaporSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VdomSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomForwardedSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot(empty) > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => { + const VaporSlot = createVaporSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createEmptyTestApp(VaporForwardedSlot) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('
vdom fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomForwardedSlot, + 'vapor fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlot = createMultipleVaporForwardedSlots( + VdomForwardedSlot, + 3, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createMultipleVaporForwardedSlots( + VdomForwardedSlotWithFallback, + 3, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe( + '
vdom fallback
', + ) + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const App = createTestApp(VdomForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const VdomForwardedSlot = createVdomForwardedSlot(VaporForwardedSlot) + const App = createTestApp(VdomForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const VdomForwardedSlot = createMultipleVdomForwardedSlots( + VaporForwardedSlot, + 3, + ) + const App = createTestApp(VdomForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot( + VaporSlot, + 'vapor fallback', + ) + const VdomForwardedSlot = createMultipleVdomForwardedSlots( + VaporForwardedSlot, + 3, + ) + const App = createTestApp(VdomForwardedSlot, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot1 = createMultipleVaporForwardedSlots( + VdomSlot, + 2, + ) + const App = createTestApp(VaporForwardedSlot1, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2 = createVaporForwardedSlot(VdomSlot) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VdomSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1 = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + ) + const App = createTestApp(VaporForwardedSlot1, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor2 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot2 = createVaporForwardedSlot(VaporSlot) + const VaporForwardedSlot1 = + createVaporForwardedSlot(VaporForwardedSlot2) + const App = createTestApp(VaporForwardedSlot1, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VdomSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VaporSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe( + '
vapor1 fallback
', + ) + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = createVdomSlot() + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) (multiple) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VdomForwardedSlot3WithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom3 fallback', + ) + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VdomForwardedSlot3WithFallback, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + }) + }) }) diff --cc packages/runtime-vapor/src/apiCreateDynamicComponent.ts index ed9143e436,8f43e86529..8ac11c9817 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@@ -41,7 -42,7 +43,8 @@@ export function createDynamicComponent rawSlots, isSingleRoot, once, + scopeId, + appContext, ), value, ) diff --cc packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 0000000000,dd6143950e..263986e16b mode 000000,100644..100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@@ -1,0 -1,202 +1,203 @@@ + import { + type AsyncComponentLoader, + type AsyncComponentOptions, + ErrorCodes, + createAsyncComponentContext, + currentInstance, + handleError, + markAsyncBoundary, + performAsyncHydrate, + useAsyncComponentState, + watch, + } from '@vue/runtime-dom' + import { defineVaporComponent } from './apiDefineComponent' + import { + type VaporComponent, + type VaporComponentInstance, + createComponent, + } from './component' + import { renderEffect } from './renderEffect' + import { DynamicFragment } from './fragment' + import { + hydrateNode, + isComment, + isHydrating, + locateEndAnchor, + removeFragmentNodes, + } from './dom/hydration' + import { invokeArrayFns } from '@vue/shared' + import { insert, remove } from './block' + import { parentNode } from './dom/node' + + /*@ __NO_SIDE_EFFECTS__ */ + export function defineVaporAsyncComponent( + source: AsyncComponentLoader | AsyncComponentOptions, + ): T { + const { + load, + getResolvedComp, + setPendingRequest, + source: { + loadingComponent, + errorComponent, + delay, + hydrate: hydrateStrategy, + timeout, + suspensible = true, + }, + } = createAsyncComponentContext(source) + + return defineVaporComponent({ + name: 'VaporAsyncComponentWrapper', + + __asyncLoader: load, + + __asyncHydrate( + el: Element, + instance: VaporComponentInstance, + // Note: this hydrate function essentially calls the setup method of the component + // not the actual hydrate function + hydrate: () => void, + ) { + // if async component needs to be updated before hydration, hydration is no longer needed. + let isHydrated = false + watch( + () => instance.attrs, + () => { + // early return if already hydrated + if (isHydrated) return + + // call the beforeUpdate hook to avoid calling hydrate in performAsyncHydrate + instance.bu && invokeArrayFns(instance.bu) + + // mount the inner component and remove the placeholder + const parent = parentNode(el)! + load().then(() => { + if (instance.isUnmounted) return + hydrate() + if (isComment(el, '[')) { + const endAnchor = locateEndAnchor(el)! + removeFragmentNodes(el, endAnchor) + insert(instance.block, parent, endAnchor) + } else { + insert(instance.block, parent, el) + remove(el, parent) + } + }) + }, + { deep: true, once: true }, + ) + + performAsyncHydrate( + el, + instance, + () => { + hydrateNode(el, () => { + hydrate() + insert(instance.block, parentNode(el)!, el) + isHydrated = true + }) + }, + getResolvedComp, + load, + hydrateStrategy, + ) + }, + + get __asyncResolved() { + return getResolvedComp() + }, + + setup() { + const instance = currentInstance as VaporComponentInstance + markAsyncBoundary(instance) + + const frag = + __DEV__ || isHydrating + ? new DynamicFragment('async component') + : new DynamicFragment() + + // already resolved + let resolvedComp = getResolvedComp() + if (resolvedComp) { + frag!.update(() => createInnerComp(resolvedComp!, instance)) + return frag + } + + const onError = (err: Error) => { + setPendingRequest(null) + handleError( + err, + instance, + ErrorCodes.ASYNC_COMPONENT_LOADER, + !errorComponent /* do not throw in dev if user provided error component */, + ) + } + + // TODO suspense-controlled + if (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) { + } + + const { loaded, error, delayed } = useAsyncComponentState( + delay, + timeout, + onError, + ) + + load() + .then(() => { + loaded.value = true + // TODO parent is keep-alive, force update so the loaded component's + // name is taken into account + }) + .catch(err => { + onError(err) + error.value = err + }) + + renderEffect(() => { + resolvedComp = getResolvedComp() + let render + if (loaded.value && resolvedComp) { + render = () => createInnerComp(resolvedComp!, instance, frag) + } else if (error.value && errorComponent) { + render = () => + createComponent(errorComponent, { error: () => error.value }) + } else if (loadingComponent && !delayed.value) { + render = () => createComponent(loadingComponent) + } + frag!.update(render) + }) + + return frag + }, + }) as T + } + + function createInnerComp( + comp: VaporComponent, + parent: VaporComponentInstance, + frag?: DynamicFragment, + ): VaporComponentInstance { + const { rawProps, rawSlots, isSingleRoot, appContext } = parent + const instance = createComponent( + comp, + rawProps, + rawSlots, + isSingleRoot, + undefined, ++ undefined, + appContext, + ) + + // set ref + // @ts-expect-error + frag && frag.setRef && frag.setRef(instance) + + // TODO custom element + // pass the custom element callback on to the inner comp + // and remove it from the async wrapper + // i.ce = ce + // delete parent.ce + return instance + } diff --cc packages/runtime-vapor/src/block.ts index 6bbc08cc6e,c4c2f0e188..aad5840f10 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@@ -5,88 -5,43 +5,44 @@@ import mountComponent, unmountComponent, } from './component' - import { createComment, createTextNode } from './dom/node' - import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' - import { isHydrating } from './dom/hydration' - import { getInheritedScopeIds } from '@vue/runtime-dom' - - export type Block = - | Node - | VaporFragment - | DynamicFragment - | VaporComponentInstance - | Block[] - - export type BlockFn = (...args: any[]) => Block - - export class VaporFragment { - nodes: Block - anchor?: Node - insert?: (parent: ParentNode, anchor: Node | null) => void - remove?: (parent?: ParentNode) => void - fallback?: BlockFn + import { _child } from './dom/node' + import { isComment, isHydrating } from './dom/hydration' + import { + type TransitionHooks, + type TransitionProps, + type TransitionState, ++ getInheritedScopeIds, + performTransitionEnter, + performTransitionLeave, + } from '@vue/runtime-dom' + import { + type DynamicFragment, + type VaporFragment, + isFragment, + } from './fragment' + import { TeleportFragment } from './components/Teleport' - constructor(nodes: Block) { - this.nodes = nodes - } + export interface VaporTransitionHooks extends TransitionHooks { + state: TransitionState + props: TransitionProps + instance: VaporComponentInstance + // mark transition hooks as disabled so that it skips during + // inserting + disabled?: boolean } - export class DynamicFragment extends VaporFragment { - anchor: Node - scope: EffectScope | undefined - current?: BlockFn - fallback?: BlockFn - /** - * slot only - * indicates forwarded slot - */ - forwarded?: boolean - - constructor(anchorLabel?: string) { - super([]) - this.anchor = - __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() - } - - update(render?: BlockFn, key: any = render): void { - if (key === this.current) { - return - } - this.current = key - - pauseTracking() - const parent = this.anchor.parentNode - - // teardown previous branch - if (this.scope) { - this.scope.stop() - parent && remove(this.nodes, parent) - } - - if (render) { - this.scope = new EffectScope() - this.nodes = this.scope.run(render) || [] - if (parent) insert(this.nodes, parent, this.anchor) - } else { - this.scope = undefined - this.nodes = [] - } - - if (this.fallback && !isValidBlock(this.nodes)) { - parent && remove(this.nodes, parent) - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] - parent && insert(this.nodes, parent, this.anchor) - } - - resetTracking() - } + export interface TransitionOptions { + $key?: any + $transition?: VaporTransitionHooks } - export function isFragment(val: NonNullable): val is VaporFragment { - return val instanceof VaporFragment - } + export type TransitionBlock = + | (Node & TransitionOptions) + | (VaporFragment & TransitionOptions) + | (DynamicFragment & TransitionOptions) + + export type Block = TransitionBlock | VaporComponentInstance | Block[] + export type BlockFn = (...args: any[]) => Block export function isBlock(val: NonNullable): val is Block { return ( @@@ -198,40 -179,44 +180,81 @@@ export function normalizeBlock(block: B return nodes } +export function setScopeId(block: Block, scopeId: string): void { + if (block instanceof Element) { + block.setAttribute(scopeId, '') + } else if (isVaporComponent(block)) { + setScopeId(block.block, scopeId) + } else if (isArray(block)) { + for (const b of block) { + setScopeId(b, scopeId) + } + } else if (isFragment(block)) { + setScopeId(block.nodes, scopeId) + } +} + +export function setComponentScopeId(instance: VaporComponentInstance): void { + const parent = instance.parent + if (!parent) return + if (isArray(instance.block) && instance.block.length > 1) return + + const scopeId = parent.type.__scopeId + if (scopeId) { + setScopeId(instance.block, scopeId) + } + + // inherit scopeId from vdom parent + if ( + parent.subTree && + (parent.subTree.component as any) === instance && + parent.vnode!.scopeId + ) { + setScopeId(instance.block, parent.vnode!.scopeId) + const scopeIds = getInheritedScopeIds(parent.vnode!, parent.parent) + for (const id of scopeIds) { + setScopeId(instance.block, id) + } + } +} + export function findBlockNode(block: Block): { + parentNode: Node | null + nextNode: Node | null + } { + let { parentNode, nextSibling: nextNode } = findLastChild(block)! + + // if nodes render as a fragment and the current nextNode is fragment + // end anchor, need to move to the next node + if (nextNode && isComment(nextNode, ']') && isFragmentBlock(block)) { + nextNode = nextNode.nextSibling + } + + return { + parentNode, + nextNode, + } + } + + function findLastChild(node: Block): Node | undefined | null { + if (node && node instanceof Node) { + return node + } else if (isArray(node)) { + return findLastChild(node[node.length - 1]) + } else if (isVaporComponent(node)) { + return findLastChild(node.block!) + } else { + if (node.anchor) return node.anchor + return findLastChild(node.nodes!) + } + } + + export function isFragmentBlock(block: Block): boolean { + if (isArray(block)) { + return true + } else if (isVaporComponent(block)) { + return isFragmentBlock(block.block!) + } else if (isFragment(block)) { + return isFragmentBlock(block.nodes) + } + return false + } diff --cc packages/runtime-vapor/src/component.ts index 6e59c0bdef,1952b31048..bafb042bc1 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@@ -25,15 -28,7 +28,14 @@@ import unregisterHMR, warn, } from '@vue/runtime-dom' -import { type Block, insert, isBlock, remove } from './block' +import { + type Block, - DynamicFragment, + insert, + isBlock, + remove, + setComponentScopeId, + setScopeId, +} from './block' import { type ShallowRef, markRaw, @@@ -146,8 -164,7 +171,8 @@@ export function createComponent rawProps?: LooseRawProps | null, rawSlots?: LooseRawSlots | null, isSingleRoot?: boolean, - once?: boolean, // TODO once support + once?: boolean, + scopeId?: string, appContext: GenericAppContext = (currentInstance && currentInstance.appContext) || emptyContext, @@@ -486,7 -611,7 +619,8 @@@ export function createComponentWithFall rawSlots?: LooseRawSlots | null, isSingleRoot?: boolean, once?: boolean, + scopeId?: string, + appContext?: GenericAppContext, ): HTMLElement | VaporComponentInstance { if (!isString(comp)) { return createComponent( @@@ -495,7 -620,7 +629,8 @@@ rawSlots, isSingleRoot, once, + scopeId, + appContext, ) } @@@ -511,13 -640,11 +650,16 @@@ // mark single root ;(el as any).$root = isSingleRoot - scopeId = scopeId || currentInstance!.type.__scopeId - if (scopeId) setScopeId(el, scopeId) ++ if (!isHydrating) { ++ scopeId = scopeId || currentInstance!.type.__scopeId ++ if (scopeId) setScopeId(el, scopeId) ++ } + if (rawProps) { - renderEffect(() => { + const setFn = () => setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)]) - }) + if (once) setFn() + else renderEffect(setFn) } if (rawSlots) { @@@ -544,9 -688,16 +703,17 @@@ export function mountComponent startMeasure(instance, `mount`) } if (instance.bm) invokeArrayFns(instance.bm) - insert(instance.block, parent, anchor) - setComponentScopeId(instance) - if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!)) + if (!isHydrating) { + insert(instance.block, parent, anchor) ++ setComponentScopeId(instance) + } + if (instance.m) queuePostFlushCb(instance.m!) + if ( + instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE && + instance.a + ) { + queuePostFlushCb(instance.a!) + } instance.isMounted = true if (__DEV__) { endMeasure(instance, `mount`) diff --cc packages/runtime-vapor/src/componentSlots.ts index 020c729fa3,aa0651658c..a0814e5b32 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@@ -1,13 -1,5 +1,5 @@@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' - import { - type Block, - type BlockFn, - DynamicFragment, - type VaporFragment, - insert, - isFragment, - setScopeId, - } from './block' -import { type Block, type BlockFn, insert } from './block' ++import { type Block, type BlockFn, insert, setScopeId } from './block' import { rawPropsProxyHandlers } from './componentProps' import { currentInstance, isRef } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' @@@ -189,36 -160,16 +160,34 @@@ export function createSlot } } + if (i) fragment.forwarded = true - if (i || !hasForwardedSlot(fragment.nodes)) { - const scopeId = instance!.type.__scopeId - if (scopeId) setScopeId(fragment, `${scopeId}-s`) - } + - if (!isHydrating && _insertionParent) { - insert(fragment, _insertionParent, _insertionAnchor) + if (!isHydrating) { ++ if (i || !hasForwardedSlot(fragment.nodes)) { ++ const scopeId = instance!.type.__scopeId ++ if (scopeId) setScopeId(fragment, `${scopeId}-s`) ++ } + if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor) + } else { + if (fragment.insert) { + ;(fragment as VaporFragment).hydrate!() + } + if (_isLastInsertion) { + advanceHydrationNode(_insertionParent!) + } } return fragment } + +function isForwardedSlot(block: Block): block is DynamicFragment { + return block instanceof DynamicFragment && !!block.forwarded +} + +function hasForwardedSlot(block: Block): block is DynamicFragment { + if (isArray(block)) { + return block.some(isForwardedSlot) + } else { + return isForwardedSlot(block) + } +} - - function ensureVaporSlotFallback( - block: VaporFragment, - fallback?: VaporSlot, - ): void { - if (block.insert && !block.fallback && fallback) { - block.fallback = fallback - } - } diff --cc packages/runtime-vapor/src/fragment.ts index 0000000000,07f1243e4e..dbd986d59e mode 000000,100644..100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@@ -1,0 -1,273 +1,274 @@@ + import { EffectScope, setActiveSub } from '@vue/reactivity' + import { createComment, createTextNode } from './dom/node' + import { + type Block, + type BlockFn, + type TransitionOptions, + type VaporTransitionHooks, + findBlockNode, + insert, + isValidBlock, + remove, + } from './block' + import { + type GenericComponentInstance, + type TransitionHooks, + type VNode, + currentInstance, + isKeepAlive, + queuePostFlushCb, + } from '@vue/runtime-dom' + import type { VaporComponentInstance } from './component' + import type { NodeRef } from './apiTemplateRef' + import type { KeepAliveInstance } from './components/KeepAlive' + import { + applyTransitionHooks, + applyTransitionLeaveHooks, + } from './components/Transition' + import { + currentHydrationNode, + isComment, + isHydrating, + locateFragmentEndAnchor, + locateHydrationNode, + } from './dom/hydration' + + export class VaporFragment + implements TransitionOptions + { + $key?: any + $transition?: VaporTransitionHooks | undefined + nodes: T + vnode?: VNode | null = null + anchor?: Node + fallback?: BlockFn + insert?: ( + parent: ParentNode, + anchor: Node | null, + transitionHooks?: TransitionHooks, + ) => void + remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void + hydrate?: (...args: any[]) => void + setRef?: ( + instance: VaporComponentInstance, + ref: NodeRef, + refFor: boolean, + refKey: string | undefined, + ) => void + + constructor(nodes: T) { + this.nodes = nodes + } + } + + export class ForFragment extends VaporFragment { + constructor(nodes: Block[]) { + super(nodes) + } + } + + export class DynamicFragment extends VaporFragment { + anchor!: Node + scope: EffectScope | undefined + current?: BlockFn + fallback?: BlockFn + anchorLabel?: string ++ forwarded?: boolean + + constructor(anchorLabel?: string) { + super([]) + if (isHydrating) { + this.anchorLabel = anchorLabel + locateHydrationNode() + } else { + this.anchor = + __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() + } + } + + update(render?: BlockFn, key: any = render): void { + if (key === this.current) { + if (isHydrating) this.hydrate(true) + return + } + this.current = key + + const prevSub = setActiveSub() + const parent = isHydrating ? null : this.anchor.parentNode + const transition = this.$transition + const instance = currentInstance! + + // teardown previous branch + if (this.scope) { + if (isKeepAlive(instance)) { + ;(instance as KeepAliveInstance).process(this.nodes) + } else { + this.scope.stop() + } + const mode = transition && transition.mode + if (mode) { + applyTransitionLeaveHooks(this.nodes, transition, () => + this.render(render, instance, transition, parent), + ) + parent && remove(this.nodes, parent) + if (mode === 'out-in') { + setActiveSub(prevSub) + return + } + } else { + parent && remove(this.nodes, parent) + } + } + + this.render(render, instance, transition, parent) + + if (this.fallback) { + // set fallback for nested fragments + const hasNestedFragment = isFragment(this.nodes) + if (hasNestedFragment) { + setFragmentFallback(this.nodes as VaporFragment, this.fallback) + } + + const invalidFragment = findInvalidFragment(this) + if (invalidFragment) { + parent && remove(this.nodes, parent) + const scope = this.scope || (this.scope = new EffectScope()) + scope.run(() => { + // for nested fragments, render invalid fragment's fallback + if (hasNestedFragment) { + renderFragmentFallback(invalidFragment) + } else { + this.nodes = this.fallback!() || [] + } + }) + parent && insert(this.nodes, parent, this.anchor) + } + } + + setActiveSub(prevSub) + + if (isHydrating) this.hydrate() + } + + private render( + render: BlockFn | undefined, + instance: GenericComponentInstance, + transition: VaporTransitionHooks | undefined, + parent: ParentNode | null, + ) { + if (render) { + this.scope = new EffectScope() + this.nodes = this.scope.run(render) || [] + if (isKeepAlive(instance)) { + ;(instance as KeepAliveInstance).process(this.nodes) + } + if (transition) { + this.$transition = applyTransitionHooks(this.nodes, transition) + } + if (parent) insert(this.nodes, parent, this.anchor) + } else { + this.scope = undefined + this.nodes = [] + } + } + + hydrate = (isEmpty = false): void => { + // avoid repeated hydration during fallback rendering + if (this.anchor) return + + if (this.anchorLabel === 'if') { + // reuse the empty comment node as the anchor for empty if + // e.g. `
` -> `` + if (isEmpty) { + this.anchor = locateFragmentEndAnchor('')! + if (__DEV__ && !this.anchor) { + throw new Error( + 'Failed to locate if anchor. this is likely a Vue internal bug.', + ) + } else { + if (__DEV__) { + ;(this.anchor as Comment).data = this.anchorLabel + } + return + } + } + } else if (this.anchorLabel === 'slot') { + // reuse the empty comment node for empty slot + // e.g. `` + if (isEmpty && isComment(currentHydrationNode!, '')) { + this.anchor = currentHydrationNode! + if (__DEV__) { + ;(this.anchor as Comment).data = this.anchorLabel! + } + return + } + + // reuse the vdom fragment end anchor + this.anchor = locateFragmentEndAnchor()! + if (__DEV__ && !this.anchor) { + throw new Error( + 'Failed to locate slot anchor. this is likely a Vue internal bug.', + ) + } else { + return + } + } + + const { parentNode, nextNode } = findBlockNode(this.nodes)! + // create an anchor + queuePostFlushCb(() => { + parentNode!.insertBefore( + (this.anchor = __DEV__ + ? createComment(this.anchorLabel!) + : createTextNode()), + nextNode, + ) + }) + } + } + + export function setFragmentFallback( + fragment: VaporFragment, + fallback: BlockFn, + ): void { + if (fragment.fallback) { + const originalFallback = fragment.fallback + // if the original fallback also renders invalid blocks, + // this ensures proper fallback chaining + fragment.fallback = () => { + const fallbackNodes = originalFallback() + if (isValidBlock(fallbackNodes)) { + return fallbackNodes + } + return fallback() + } + } else { + fragment.fallback = fallback + } + + if (isFragment(fragment.nodes)) { + setFragmentFallback(fragment.nodes, fragment.fallback) + } + } + + function renderFragmentFallback(fragment: VaporFragment): void { + if (fragment instanceof ForFragment) { + fragment.nodes[0] = [fragment.fallback!() || []] as Block[] + } else if (fragment instanceof DynamicFragment) { + fragment.update(fragment.fallback) + } else { + // vdom slots + } + } + + function findInvalidFragment(fragment: VaporFragment): VaporFragment | null { + if (isValidBlock(fragment.nodes)) return null + + return isFragment(fragment.nodes) + ? findInvalidFragment(fragment.nodes) || fragment + : fragment + } + + export function isFragment(val: NonNullable): val is VaporFragment { + return val instanceof VaporFragment + } diff --cc packages/runtime-vapor/src/vdomInterop.ts index 79959119e2,d3cb8d243a..d6bfb8cf09 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@@ -173,13 -274,12 +274,13 @@@ function createVDOMComponent component: ConcreteComponent, rawProps?: LooseRawProps | null, rawSlots?: LooseRawSlots | null, + scopeId?: string, ): VaporFragment { const frag = new VaporFragment([]) - const vnode = createVNode( + const vnode = (frag.vnode = createVNode( component, rawProps && new Proxy(rawProps, rawPropsProxyHandlers), - ) + )) const wrapper = new VaporComponentInstance( { props: component.props }, rawProps as RawProps, @@@ -209,10 -324,33 +325,35 @@@ internals.umt(vnode.component!, null, !!parentNode) } + vnode.scopeId = scopeId || parentInstance.type.__scopeId! + - frag.insert = (parentNode, anchor) => { + frag.hydrate = () => { + hydrateVNode(vnode, parentInstance 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) { + vdomActivate( + vnode, + parentNode, + anchor, + internals, + parentInstance as any, + null, + undefined, + false, + ) + return + } + + const prev = currentInstance + simpleSetCurrentInstance(parentInstance) if (!isMounted) { + if (transition) setVNodeTransitionHooks(vnode, transition) internals.mt( vnode, parentNode, @@@ -235,8 -375,8 +378,9 @@@ ) } + // update the fragment nodes - frag.nodes = vnode.el as Block + frag.nodes = vnode.el as any + simpleSetCurrentInstance(prev) } frag.remove = unmount