): CodeFragment[] {
const [frag, push] = buildCodeFragment()
if (isBlockOperation(oper) && oper.parent) {
- push(...genInsertionstate(oper, context))
+ push(...genInsertionState(oper, context))
}
push(...genOperation(oper, context))
return frag
return frag
}
-function genInsertionstate(
+function genInsertionState(
operation: InsertionStateTypes,
context: CodegenContext,
): CodeFragment[] {
`<span></span>` +
`</div>`,
)
+
+ data.value.splice(0, 1)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<span></span>` +
+ `<!--[-->` +
+ `<span>b</span>` +
+ `<span>c</span>` +
+ `<span>d</span>` +
+ `<!--]-->` +
+ `<span></span>` +
+ `</div>`,
+ )
})
test('consecutive v-for with anchor insertion', async () => {
`<span></span>` +
`</div>`,
)
+
+ data.value.splice(0, 2)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<span></span>` +
+ `<!--[-->` +
+ `<span>c</span>` +
+ `<span>d</span>` +
+ `<!--]-->` +
+ `<!--[-->` +
+ `<span>c</span>` +
+ `<span>d</span>` +
+ `<!--]-->` +
+ `<span></span>` +
+ `</div>`,
+ )
})
- // TODO wait for slots hydration support
- test.todo('v-for on component', async () => {})
+ test('v-for on component', async () => {
+ const { container, data } = await testHydration(
+ `<template>
+ <div>
+ <components.Child v-for="item in data" :key="item"/>
+ </div>
+ </template>`,
+ {
+ Child: `<template><div>comp</div></template>`,
+ },
+ ref(['a', 'b', 'c']),
+ )
- // TODO wait for slots hydration support
- test.todo('on fragment component', async () => {})
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<!--[-->` +
+ `<div>comp</div>` +
+ `<div>comp</div>` +
+ `<div>comp</div>` +
+ `<!--]-->` +
+ `</div>`,
+ )
+
+ data.value.push('d')
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<!--[-->` +
+ `<div>comp</div>` +
+ `<div>comp</div>` +
+ `<div>comp</div>` +
+ `<div>comp</div>` +
+ `<!--]-->` +
+ `</div>`,
+ )
+ })
+
+ test('v-for on component with slots', async () => {
+ const { container, data } = await testHydration(
+ `<template>
+ <div>
+ <components.Child v-for="item in data" :key="item">
+ <span>{{ item }}</span>
+ </components.Child>
+ </div>
+ </template>`,
+ {
+ Child: `<template><slot/></template>`,
+ },
+ ref(['a', 'b', 'c']),
+ )
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<!--[-->` +
+ `<!--[--><span>a</span><!--]--><!--slot-->` +
+ `<!--[--><span>b</span><!--]--><!--slot-->` +
+ `<!--[--><span>c</span><!--]--><!--slot-->` +
+ `<!--]-->` +
+ `</div>`,
+ )
+
+ data.value.push('d')
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<!--[-->` +
+ `<!--[--><span>a</span><!--]--><!--slot-->` +
+ `<!--[--><span>b</span><!--]--><!--slot-->` +
+ `<!--[--><span>c</span><!--]--><!--slot-->` +
+ `<span>d</span><!--slot-->` +
+ `<!--]-->` +
+ `</div>`,
+ )
+ })
+
+ test('on fragment component', async () => {
+ const { container, data } = await testHydration(
+ `<template>
+ <div>
+ <components.Child v-for="item in data" :key="item"/>
+ </div>
+ </template>`,
+ {
+ Child: `<template><div>foo</div>-bar-</template>`,
+ },
+ ref(['a', 'b', 'c']),
+ )
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<!--[-->` +
+ `<!--[--><div>foo</div>-bar-<!--]-->` +
+ `<!--[--><div>foo</div>-bar-<!--]-->` +
+ `<!--[--><div>foo</div>-bar-<!--]-->` +
+ `<!--]-->` +
+ `</div>`,
+ )
+
+ data.value.push('d')
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div>` +
+ `<!--[-->` +
+ `<!--[--><div>foo</div>-bar-<!--]-->` +
+ `<!--[--><div>foo</div>-bar-<!--]-->` +
+ `<!--[--><div>foo</div>-bar-<!--]-->` +
+ `<div>foo</div>-bar-` +
+ `<!--]-->` +
+ `</div>`,
+ )
+ })
// TODO wait for vapor TransitionGroup support
// v-for inside TransitionGroup does not render as a fragment
test.todo('v-for in TransitionGroup', async () => {})
})
- test.todo('slots')
+ describe('slots', () => {
+ test('basic slot', async () => {
+ const { data, container } = await testHydration(
+ `<template>
+ <components.Child>
+ <span>{{data}}</span>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><slot/></template>`,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>foo</span><!--]--><!--slot-->`,
+ )
+
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>bar</span><!--]--><!--slot-->`,
+ )
+ })
+
+ test('named slot', async () => {
+ const { data, container } = await testHydration(
+ `<template>
+ <components.Child>
+ <template #foo>
+ <span>{{data}}</span>
+ </template>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><slot name="foo"/></template>`,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>foo</span><!--]--><!--slot-->`,
+ )
+
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>bar</span><!--]--><!--slot-->`,
+ )
+ })
+
+ test('named slot with v-if', async () => {
+ const { data, container } = await testHydration(
+ `<template>
+ <components.Child>
+ <template #foo v-if="data">
+ <span>{{data}}</span>
+ </template>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><slot name="foo"/></template>`,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[--><span>foo</span><!--]--><!--slot-->`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toBe(`<!--[--><!--]--><!--slot-->`)
+ })
+
+ test('named slot with v-if and v-for', async () => {
+ const data = reactive({
+ show: true,
+ items: ['a', 'b', 'c'],
+ })
+ const { container } = await testHydration(
+ `<template>
+ <components.Child>
+ <template #foo v-if="data.show">
+ <span v-for="item in data.items" :key="item">{{item}}</span>
+ </template>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><slot name="foo"/></template>`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<!--[--><span>a</span><span>b</span><span>c</span><!--]-->` +
+ `<!--]-->` +
+ `<!--slot-->`,
+ )
+
+ data.show = false
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--[--><!--[--><!--]--><!--]--><!--slot-->`,
+ )
+ })
+
+ test('with anchor insertion', async () => {
+ const { data, container } = await testHydration(
+ `<template>
+ <components.Child>
+ <span/>
+ <span>{{data}}</span>
+ <span/>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><slot/></template>`,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<span></span>` +
+ `<span>foo</span>` +
+ `<span></span>` +
+ `<!--]-->` +
+ `<!--slot-->`,
+ )
+
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<span></span>` +
+ `<span>bar</span>` +
+ `<span></span>` +
+ `<!--]-->` +
+ `<!--slot-->`,
+ )
+ })
+
+ test('with multi level anchor insertion', async () => {
+ const { data, container } = await testHydration(
+ `<template>
+ <components.Child>
+ <span/>
+ <span>{{data}}</span>
+ <span/>
+ </components.Child>
+ </template>`,
+ {
+ Child: `
+ <template>
+ <div/>
+ <div/>
+ <slot/>
+ <div/>
+ </div>
+ </template>`,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<div></div>` +
+ `<div></div>` +
+ `<!--[-->` +
+ `<span></span>` +
+ `<span>foo</span>` +
+ `<span></span>` +
+ `<!--]-->` +
+ `<!--slot-->` +
+ `<div></div>` +
+ `<!--]-->`,
+ )
+
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<div></div>` +
+ `<div></div>` +
+ `<!--[-->` +
+ `<span></span>` +
+ `<span>bar</span>` +
+ `<span></span>` +
+ `<!--]-->` +
+ `<!--slot-->` +
+ `<div></div>` +
+ `<!--]-->`,
+ )
+ })
+
+ // problem is next child is incorrect after slot
+ test.todo('mixed slot and text node', async () => {
+ const data = reactive({
+ text: 'foo',
+ msg: 'hi',
+ })
+ const { container } = await testHydration(
+ `<template>
+ <components.Child>
+ <span>{{data.text}}</span>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><div><slot/>{{data.msg}}</div></template>`,
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><!--[--><span>foo</span><!--]--><!--slot-->hi</div>"`,
+ )
+ })
+
+ test.todo('mixed slot and element', async () => {
+ const data = reactive({
+ text: 'foo',
+ msg: 'hi',
+ })
+ const { container } = await testHydration(
+ `<template>
+ <components.Child>
+ <span>{{data.text}}</span>
+ </components.Child>
+ </template>`,
+ {
+ Child: `<template><div><slot/><div>{{data.msg}}</div></div></template>`,
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<div><!--hi--><span>foo</span><!--]--><!--slot--><div>hi</div></div>"`,
+ )
+ })
+
+ // mixed slot and component
+ // mixed slot and fragment component
+ // mixed slot and v-if
+ // mixed slot and v-for
+ })
// test('element with ref', () => {
// const el = ref()
-import { isArray, isVaporFragmentEndAnchor } from '@vue/shared'
+import { isArray } from '@vue/shared'
import {
type VaporComponentInstance,
isVaporComponent,
} else {
// find next sibling dynamic fragment end anchor
const anchor = nextVaporFragmentAnchor(currentHydrationNode!, label)!
- if (anchor && isVaporFragmentEndAnchor(anchor)) {
+ if (anchor) {
this.anchor = anchor
} else if (__DEV__) {
- // TODO warning
+ // TODO warning, should not happen
warn(`DynamicFragment anchor not found...`)
}
}
-import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
+import {
+ EMPTY_OBJ,
+ NO,
+ SLOT_ANCHOR_LABEL,
+ hasOwn,
+ isArray,
+ isFunction,
+} from '@vue/shared'
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
import { rawPropsProxyHandlers } from './componentProps'
import { currentInstance, isRef } from '@vue/runtime-dom'
import type { LooseRawProps, VaporComponentInstance } from './component'
import { renderEffect } from './renderEffect'
-import { insertionAnchor, insertionParent } from './insertionState'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+ insertionAnchor,
+ insertionParent,
+ resetInsertionState,
+} from './insertionState'
+import { isHydrating } from './dom/hydration'
export type RawSlots = Record<string, VaporSlot> & {
$?: DynamicSlotSource[]
): Block {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
- if (isHydrating) {
- locateHydrationNode()
- }
+ if (!isHydrating) resetInsertionState()
const instance = currentInstance as VaporComponentInstance
const rawSlots = instance.rawSlots
fallback,
)
} else {
- fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
+ fragment = new DynamicFragment(SLOT_ANCHOR_LABEL)
const isDynamicName = isFunction(name)
const renderSlot = () => {
const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
}
export let adoptTemplate: (node: Node, template: string) => Node | null
-export let locateHydrationNode: (isFragment?: boolean) => void
+export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void
type Anchor = Comment & {
// cached matching fragment start to avoid repeated traversal
} else {
node = insertionParent ? insertionParent.lastChild : currentHydrationNode
+ // if current node is fragment start anchor, find the next one
+ if (node && isComment(node, '[')) {
+ node = node.nextSibling
+ }
// if the last child is a vapor fragment end anchor, find the previous one
- if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
- let previous = node.previousSibling
- if (previous) node = previous
+ else if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
+ node = node.previousSibling
+ if (__DEV__ && !node) {
+ // TODO warning, should not happen
+ }
}
if (node && isComment(node, ']')) {
).toBe(
`<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` +
- `</div></div>`,
+ `<!--slot--></div></div>`,
)
// test fallback
}),
),
).toBe(
- `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`,
+ `<div>parent<div class="child"><!--[-->fallback<!--]--><!--slot--></div></div>`,
)
})
).toBe(
`<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` +
- `</div></div>`,
+ `<!--slot--></div></div>`,
)
})
expect(await render(app)).toBe(
`<div>parent<div class="child">` +
`<!--[--><span>from slot</span><!--]-->` +
- `</div></div>`,
+ `<!--slot--></div></div>`,
)
})
})
expect(await render(app)).toBe(
- `<div><!--[--><!--[-->hello<!--]--><!--]--></div>`,
+ `<div><!--[--><!--[-->hello<!--]--><!--slot--><!--]--><!--slot--></div>`,
)
})
expect(await render(app)).toBe(
// should only have a single fragment
- `<div><!--[--><!--]--></div>`,
+ `<div><!--[--><!--]--><!--slot--></div>`,
)
})
expect(await render(app)).toBe(
// should only have a single fragment
- `<div><!--[-->fallback<!--]--></div>`,
+ `<div><!--[-->fallback<!--]--><!--slot--></div>`,
)
})
})
}),
),
).toBe(
- `<div><!--[--><span>slot</span><!--]--></div><!--dynamic-component-->`,
+ `<div><!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
)
})
}),
),
).toBe(
- `<div>test<!--[--><span>slot</span><!--]--></div><!--dynamic-component-->`,
+ `<div>test<!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
)
})
}
const result = await renderToString(createApp(Comp))
- expect(result).toBe(`<!--[--><div parent wrapper-s child></div><!--]-->`)
+ expect(result).toBe(
+ `<!--[--><div parent wrapper-s child></div><!--]--><!--slot-->`,
+ )
})
// #2892
const result = await renderToString(createApp(Root))
expect(result).toBe(
`<div class="wrapper" root slotted wrapper>` +
- `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
- `</div>`,
+ `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
+ `<!--slot--></div>`,
)
})
const result = await renderToString(createApp(Root))
expect(result).toBe(
`<div class="wrapper" root slotted wrapper>` +
- `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
- `</div>`,
+ `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
+ `<!--slot--></div>`,
)
})
})
template: `<one>hello</one>`,
}),
),
- ).toBe(`<div><!--[-->hello<!--]--></div>`)
+ ).toBe(`<div><!--[-->hello<!--]--><!--slot--></div>`)
})
test('element slot', async () => {
template: `<one><div>hi</div></one>`,
}),
),
- ).toBe(`<div><!--[--><div>hi</div><!--]--></div>`)
+ ).toBe(`<div><!--[--><div>hi</div><!--]--><!--slot--></div>`)
})
test('empty slot', async () => {
template: `<one><template v-if="false"/></one>`,
}),
),
- ).toBe(`<div><!--[--><!--]--></div>`)
+ ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
})
test('empty slot (manual comments)', async () => {
template: `<one><!--hello--></one>`,
}),
),
- ).toBe(`<div><!--[--><!--]--></div>`)
+ ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
})
test('empty slot (multi-line comments)', async () => {
template: `<one><!--he\nllo--></one>`,
}),
),
- ).toBe(`<div><!--[--><!--]--></div>`)
+ ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
})
test('multiple elements', async () => {
template: `<one><div>one</div><div>two</div></one>`,
}),
),
- ).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--></div>`)
+ ).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--><!--slot--></div>`)
})
test('fragment slot (template v-if)', async () => {
template: `<one><template v-if="true">hello</template></one>`,
}),
),
- ).toBe(`<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--></div>`)
+ ).toBe(
+ `<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--><!--slot--></div>`,
+ )
})
test('fragment slot (template v-if + multiple elements)', async () => {
}),
),
).toBe(
- `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--if--><!--]--></div>`,
+ `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--if--><!--]--><!--slot--></div>`,
)
})
}),
),
).toBe(
- `<button><!--[--><div><!--[--><!--]--></div><!--]--></button><!--dynamic-component-->`,
+ `<button><!--[--><div><!--[--><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
)
expect(
}),
),
).toBe(
- `<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button><!--dynamic-component-->`,
+ `<button><!--[--><div><!--[--><div>hello</div><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
)
expect(
type SSRBufferItem,
renderVNodeChildren,
} from '../render'
-import { isArray } from '@vue/shared'
+import { SLOT_ANCHOR_LABEL, isArray } from '@vue/shared'
const { ensureValidVNode } = ssrUtils
parentComponent,
slotScopeId,
)
- push(`<!--]-->`)
+ push(`<!--]--><!--${SLOT_ANCHOR_LABEL}-->`)
}
export function ssrRenderSlotInner(