const _component_Comp = _resolveComponent("Comp")
const n0 = t0()
const n3 = t1()
+ const n2 = _child(n3, 1)
_setInsertionState(n3, 0)
const n1 = _createComponentWithFallback(_component_Comp)
- const n2 = _child(n3)
_renderEffect(() => {
_setProp(n3, "id", _ctx.foo)
_setText(n2, _toDisplayString(_ctx.bar))
}"
`;
+exports[`compile > execution order > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = `
+"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<div><div></div><!><div></div><!><div><button></button></div></div>", true)
+
+export function render(_ctx) {
+ const _component_Comp = _resolveComponent("Comp")
+ const n6 = t1()
+ const n5 = _next(_child(n6), 1)
+ const n7 = _nthChild(n6, 3, 3)
+ const p0 = _next(n7, 4)
+ const n4 = _child(p0, 0)
+ _setInsertionState(n6, n5)
+ const n0 = _createComponentWithFallback(_component_Comp)
+ _setInsertionState(n6, n7)
+ const n1 = _createIf(() => (true), () => {
+ const n3 = t0()
+ return n3
+ })
+ _renderEffect(() => _setProp(n4, "disabled", _ctx.foo))
+ return n6
+}"
+`;
+
exports[`compile > execution order > with insertionState 1`] = `
"import { resolveComponent as _resolveComponent, child as _child, setInsertionState as _setInsertionState, createSlot as _createSlot, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template("<div><div></div></div>", true)
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n3 = t0()
- const n1 = _child(n3)
+ const n1 = _child(n3, 0)
_setInsertionState(n1, null)
const n0 = _createSlot("default", null)
_setInsertionState(n3, 1)
export function render(_ctx) {
const n3 = t0()
- const n0 = _child(n3)
- const n1 = _next(n0)
- const n2 = _nthChild(n3, 3)
+ const n0 = _child(n3, 0)
+ const n1 = _next(n0, 1)
+ const n2 = _nthChild(n3, 3, 3)
const x0 = _txt(n0)
_setText(x0, _toDisplayString(_ctx.foo))
_renderEffect(() => {
)
})
+ describe('setInsertionState', () => {
+ test('next, child and nthChild should be above the setInsertionState', () => {
+ const code = compile(`
+ <div>
+ <div />
+ <Comp />
+ <div />
+ <div v-if="true" />
+ <div>
+ <button :disabled="foo" />
+ </div>
+ </div>
+ `)
+ expect(code).toMatchSnapshot()
+ })
+ })
+
test('with v-once', () => {
const code = compile(
`<div>
export function render(_ctx) {
const n1 = t0()
- const n0 = _child(n1)
+ const n0 = _child(n1, 0)
const x1 = _txt(n1)
_renderEffect(() => {
const _foo = _ctx.foo
export function render(_ctx) {
const n1 = t0()
- const n0 = _child(n1)
+ const n0 = _child(n1, 0)
const x1 = _txt(n1)
_renderEffect(() => {
const _String = String
export function render(_ctx) {
const n4 = t1()
- const n3 = _next(_child(n4))
+ const n3 = _next(_child(n4), 1)
_setInsertionState(n4, n3)
const n0 = _createIf(() => (1), () => {
const n2 = t0()
export function render(_ctx) {
const n3 = t0()
- const n0 = _child(n3)
- const n1 = _next(n0)
- const n2 = _next(n1)
+ const n0 = _child(n3, 0)
+ const n1 = _next(n0, 1)
+ const n2 = _next(n1, 2)
const x0 = _txt(n0)
const x2 = _txt(n2)
_renderEffect(() => {
export function render(_ctx) {
const n1 = t0()
- const n0 = _nthChild(n1, 2)
+ const n0 = _nthChild(n1, 2, 2)
const x0 = _txt(n0)
_renderEffect(() => _setText(x0, _toDisplayString(_ctx.msg)))
return n1
export function render(_ctx) {
const n3 = t0()
- const p0 = _next(_child(n3))
- const n0 = _child(p0)
- const p1 = _next(p0)
- const n1 = _child(p1)
- const p2 = _next(p1)
- const n2 = _child(p2)
+ const p0 = _next(_child(n3), 1)
+ const n0 = _child(p0, 0)
+ const p1 = _next(p0, 2)
+ const n1 = _child(p1, 0)
+ const p2 = _next(p1, 3)
+ const n2 = _child(p2, 0)
const x0 = _txt(n0)
const x1 = _txt(n1)
const x2 = _txt(n2)
const _component_Comp = _resolveComponent("Comp")
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n3 = _createComponentWithFallback(_component_Comp)
- const n2 = _child(n3)
+ const n2 = _child(n3, 0)
_renderEffect(() => _setText(n2, _toDisplayString(_for_item0.value)))
return [n2, n3]
}, undefined, 2)
const _component_Comp = _resolveComponent("Comp")
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n3 = _createComponentWithFallback(_component_Comp)
- const n2 = _child(n3)
+ const n2 = _child(n3, 0)
_renderEffect(() => _setText(n2, _toDisplayString(_for_item0.value)))
return [n2, n3]
}, undefined, 2)
export function render(_ctx, $props, $emit, $attrs, $slots) {
const n2 = t0()
- const n0 = _child(n2)
- const n1 = _next(n0)
+ const n0 = _child(n2, 0)
+ const n1 = _next(n0, 1)
_setText(n0, _toDisplayString(_ctx.msg) + " ")
_setClass(n1, _ctx.clz)
return n2
export function render(_ctx) {
const n1 = t0()
- const n0 = _child(n1)
+ const n0 = _child(n1, 0)
_setProp(n0, "id", _ctx.foo)
return n1
}"
<div>{{ msg }}</div>
</div>`,
)
- expect(code).contains(`const n0 = _nthChild(n1, 2)`)
+ expect(code).contains(`const n0 = _nthChild(n1, 2, 2)`)
expect(code).toMatchSnapshot()
})
</div>`,
)
// ensure the insertion anchor is generated before the insertion statement
- expect(code).toMatch(`const n3 = _next(_child(n4))`)
+ expect(code).toMatch(`const n3 = _next(_child(n4), 1)`)
expect(code).toMatch(`_setInsertionState(n4, n3)`)
expect(code).toMatchSnapshot()
})
}
for (const child of dynamic.children) {
if (!child.hasDynamicChild) {
- push(...genChildren(child, context, `n${child.id!}`))
+ push(...genChildren(child, context, push, `n${child.id!}`))
}
}
import type { CodegenContext } from '../generate'
-import { DynamicFlag, type IRDynamicInfo } from '../ir'
+import {
+ DynamicFlag,
+ type IRDynamicInfo,
+ type InsertionStateTypes,
+} from '../ir'
import { genDirectivesForElement } from './directive'
import { genOperationWithInsertionState } from './operation'
import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
}
if (hasDynamicChild) {
- push(...genChildren(dynamic, context, `n${id}`))
+ push(...genChildren(dynamic, context, push, `n${id}`))
}
return frag
export function genChildren(
dynamic: IRDynamicInfo,
context: CodegenContext,
+ pushBlock: (...items: CodeFragment[]) => number,
from: string = `n${dynamic.id}`,
): CodeFragment[] {
const { helper } = context
let offset = 0
let prev: [variable: string, elementIndex: number] | undefined
+ let ifBranchCount = 0
+ let prependCount = 0
for (const [index, child] of children.entries()) {
+ if (
+ child.operation &&
+ (child.operation as InsertionStateTypes).anchor === -1
+ ) {
+ prependCount++
+ }
if (child.flags & DynamicFlag.NON_TEMPLATE) {
offset--
+ } else if (child.ifBranch) {
+ ifBranchCount++
}
const id =
}
const elementIndex = index + offset
+ const logicalIndex = elementIndex - ifBranchCount + prependCount
// p for "placeholder" variables that are meant for possible reuse by
// other access paths
const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
- push(NEWLINE, `const ${variable} = `)
-
+ pushBlock(NEWLINE, `const ${variable} = `)
if (prev) {
if (elementIndex - prev[1] === 1) {
- push(...genCall(helper('next'), prev[0]))
+ pushBlock(...genCall(helper('next'), prev[0], String(logicalIndex)))
} else {
- push(...genCall(helper('nthChild'), from, String(elementIndex)))
+ pushBlock(
+ ...genCall(
+ helper('nthChild'),
+ from,
+ String(elementIndex),
+ String(logicalIndex),
+ ),
+ )
}
} else {
if (elementIndex === 0) {
- push(...genCall(helper('child'), from))
+ pushBlock(...genCall(helper('child'), from, String(logicalIndex)))
} else {
// check if there's a node that we can reuse from
let init = genCall(helper('child'), from)
if (elementIndex === 1) {
- init = genCall(helper('next'), init)
+ init = genCall(helper('next'), init, String(logicalIndex))
} else if (elementIndex > 1) {
- init = genCall(helper('nthChild'), from, String(elementIndex))
+ init = genCall(
+ helper('nthChild'),
+ from,
+ String(elementIndex),
+ String(logicalIndex),
+ )
}
- push(...init)
+ pushBlock(...init)
}
}
}
prev = [variable, elementIndex]
- push(...genChildren(child, context, variable))
+ push(...genChildren(child, context, pushBlock, variable))
}
return frag
hasDynamicChild?: boolean
operation?: OperationNode
needsKey?: boolean
+ ifBranch?: boolean
}
export interface IREffect {
isConstantExpression,
isStaticExpression,
} from '../utils'
+import { escapeHtml } from '@vue/shared'
type TextLike = TextNode | InterpolationNode
const seen = new WeakMap<
} else if (node.type === NodeTypes.INTERPOLATION) {
processInterpolation(context as TransformContext<InterpolationNode>)
} else if (node.type === NodeTypes.TEXT) {
- context.template += node.content
+ context.template += escapeHtml(node.content)
}
}
const literals = values.map(getLiteralExpressionValue)
if (literals.every(l => l != null)) {
- context.childrenTemplate = literals.map(l => String(l))
+ context.childrenTemplate = literals.map(l => escapeHtml(String(l)))
} else {
context.childrenTemplate = [' ']
context.registerOperation({
} else {
// check the adjacent v-if
const siblingIf = getSiblingIf(context, true)
+ context.dynamic.ifBranch = true
const siblings = context.parent && context.parent.dynamic.children
let lastIfNode
}
}
-interface TeleportTargetElement extends Element {
+export interface TeleportTargetElement extends Element {
// last teleport target
_lpa?: Node | null
}
)
}
}
+
+ // the server output does not contain blank text nodes. It appears here that
+ // it is a dynamically inserted anchor, and needs to be skipped.
+ // e.g. vaporInteropImpl.mount() > selfAnchor
+ if (
+ node &&
+ node.nodeType === DOMNodeTypes.TEXT &&
+ !(node as Text).data.trim()
+ ) {
+ node = nextSibling(node)
+ }
return node
}
isTeleportDisabled,
isTeleportDeferred,
} from './components/Teleport'
+/**
+ * @internal
+ */
+export type { TeleportTargetElement } from './components/Teleport'
+/**
+ * @internal
+ */
export {
createAsyncComponentContext,
useAsyncComponentState,
import * as runtimeDom from '@vue/runtime-dom'
import * as VueServerRenderer from '@vue/server-renderer'
import { isString } from '@vue/shared'
+import type { VaporComponentInstance } from '../src/component'
+import type { TeleportFragment } from '../src/components/Teleport'
const formatHtml = (raw: string) => {
return raw
})
}
+function compileVaporComponent(
+ code: string,
+ data: runtimeDom.Ref<any> = ref({}),
+ components?: Record<string, any>,
+ ssr = false,
+) {
+ return compile(`<template>${code}</template>`, data, components, {
+ vapor: true,
+ ssr,
+ })
+}
+
async function mountWithHydration(
html: string,
code: string,
- data: runtimeDom.Ref<any>,
+ data: runtimeDom.Ref<any> = ref({}),
+ components?: Record<string, any>,
) {
const container = document.createElement('div')
container.innerHTML = html
+ document.body.appendChild(container)
- const clientComp = compile(`<template>${code}</template>`, data, undefined, {
- vapor: true,
- ssr: false,
- })
+ const clientComp = compileVaporComponent(code, data, components)
const app = createVaporSSRApp(clientComp)
app.mount(container)
return {
+ block: (app._instance! as VaporComponentInstance).block,
container,
}
}
<template><!----></template>
`)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"<!---->"`)
- expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
+ expect(`mismatch in <div>`).not.toHaveBeenWarned()
})
test('root with mixed element and text', async () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`"<div></div>"`,
)
- expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
+ expect(`mismatch in <div>`).not.toHaveBeenWarned()
})
test('element with binding and text children', async () => {
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- <!--[--><span>a</span><span>b</span><span>c</span><!--for-->"
+ <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+ "
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"
- <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for-->"
+ <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+ "
+ `,
+ )
+ })
+
+ test('empty v-for', async () => {
+ const { container, data } = await testHydration(
+ `<template>
+ <span v-for="item in data" :key="item">{{ item }}</span>
+ </template>`,
+ undefined,
+ ref([]),
+ )
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><!--]-->
+ "
+ `,
+ )
+
+ data.value.push('a')
+ await nextTick()
+ expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+ `
+ "
+ <!--[--><span>a</span><!--]-->
+ "
`,
)
})
`
"
<!--[--><div>
- <!--[--><span>a</span><span>b</span><span>c</span><!--for--></div><div>3</div><!--]-->
+ <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+ </div><div>3</div><!--]-->
"
`,
)
`
"
<!--[--><div>
- <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--></div><div>4</div><!--]-->
+ <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+ </div><div>4</div><!--]-->
"
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
- <!--[--><span>a</span><span>b</span><span>c</span><!--for--><span></span></div>"
+ <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+ <span></span></div>"
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
- <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
+ <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+ <span></span></div>"
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
- <!--[--><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
+ <!--[--><span>b</span><span>c</span><span>d</span><!--]-->
+ <span></span></div>"
`,
)
})
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
- <!--[--><span>a</span><span>b</span><span>c</span><!--for-->
- <!--[--><span>a</span><span>b</span><span>c</span><!--for--><span></span></div>"
+ <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+ <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+ <span></span></div>"
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
- <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for-->
- <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--for--><span></span></div>"
+ <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+ <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+ <span></span></div>"
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div><span></span>
- <!--[--><span>c</span><span>d</span><!--for-->
- <!--[--><span>c</span><span>d</span><!--for--><span></span></div>"
+ <!--[--><span>c</span><span>d</span><!--]-->
+ <!--[--><span>c</span><span>d</span><!--]-->
+ <span></span></div>"
`,
)
})
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
- <!--[--><div>comp</div><div>comp</div><div>comp</div><!--for--></div>"
+ <!--[--><div>comp</div><div>comp</div><div>comp</div><!--]-->
+ </div>"
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
- <!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--for--></div>"
+ <!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--]-->
+ </div>"
`,
)
})
<!--[--><span>a</span><!--]-->
<!--[--><span>b</span><!--]-->
<!--[--><span>c</span><!--]-->
- <!--for--></div>"
+ <!--]-->
+ </div>"
`,
)
<!--[--><span>a</span><!--]-->
<!--[--><span>b</span><!--]-->
<!--[--><span>c</span><!--]-->
- <span>d</span><!--slot--><!--for--></div>"
+ <span>d</span><!--slot--><!--]-->
+ </div>"
`,
)
})
<!--[-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<!--]-->
- <!--[--><div>foo</div>-bar-<!--for--><!--]-->
+ <!--[--><div>foo</div>-bar-<!--]-->
+ <!--]-->
</div>"
`,
)
<!--[-->
<!--[--><div>foo</div>-bar-<!--]-->
<!--[--><div>foo</div>-bar-<!--]-->
- <!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--for--><!--]-->
+ <!--[--><div>foo</div>-bar-<div>foo</div>-bar-<!--]-->
+ <!--]-->
</div>"
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
- <!--[--><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><!--for--></div>"
+ <!--[--><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+ </div>"
`,
)
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
`
"<div>
- <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--for--></div>"
+ <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+ </div>"
`,
)
await nextTick()
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
"<div>
- <!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--for--></div>"
+ <!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--]-->
+ </div>"
`)
data.value.show = true
await nextTick()
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
"<div>
- <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--for--></div>"
+ <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+ </div>"
`)
})
})
`
"
<!--[-->
- <!--[--><span>a</span><span>b</span><span>c</span><!--for--><!--]-->
+ <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+ <!--]-->
"
`,
)
`
"
<!--[-->
- <!--[--><div>a</div><div>b</div><div>c</div><!--for-->
+ <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
<!--[--><span>foo</span><!--]-->
- <!--[--><div>a</div><div>b</div><div>c</div><!--for--><!--]-->
+ <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
+ <!--]-->
"
`,
)
`
"
<!--[-->
- <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--for-->
+ <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
<!--[--><span>foo</span><!--]-->
- <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--for--><!--]-->
+ <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
+ <!--]-->
"
`,
)
test.todo('force hydrate custom element with dynamic props', () => {})
})
- describe.todo('Teleport')
+ describe('Teleport', () => {
+ test('basic', async () => {
+ const data = ref({
+ msg: ref('foo'),
+ disabled: ref(false),
+ fn: vi.fn(),
+ })
+
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = 'teleport'
+ teleportContainer.innerHTML =
+ `<!--teleport start anchor-->` +
+ `<span>foo</span>` +
+ `<span class="foo"></span>` +
+ `<!--teleport anchor-->`
+ document.body.appendChild(teleportContainer)
+
+ const { block, container } = await mountWithHydration(
+ '<!--teleport start--><!--teleport end-->',
+ `<teleport to="#teleport" :disabled="data.disabled">
+ <span>{{data.msg}}</span>
+ <span :class="data.msg" @click="data.fn"></span>
+ </teleport>`,
+ data,
+ )
+
+ const teleport = block as TeleportFragment
+ expect(teleport.anchor).toBe(container.lastChild)
+ expect(teleport.target).toBe(teleportContainer)
+ expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
+ expect((teleport.nodes as Node[])[0]).toBe(
+ teleportContainer.childNodes[1],
+ )
+ expect((teleport.nodes as Node[])[1]).toBe(
+ teleportContainer.childNodes[2],
+ )
+ expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--teleport start--><!--teleport end-->"`,
+ )
+
+ // event handler
+ triggerEvent('click', teleportContainer.querySelector('.foo')!)
+ expect(data.value.fn).toHaveBeenCalled()
+
+ data.value.msg = 'bar'
+ await nextTick()
+ expect(formatHtml(teleportContainer.innerHTML)).toBe(
+ `<!--teleport start anchor-->` +
+ `<span>bar</span>` +
+ `<span class="bar"></span>` +
+ `<!--teleport anchor-->`,
+ )
+
+ data.value.disabled = true
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--teleport start-->` +
+ `<span>bar</span>` +
+ `<span class="bar"></span>` +
+ `<!--teleport end-->`,
+ )
+ expect(formatHtml(teleportContainer.innerHTML)).toMatchInlineSnapshot(
+ `"<!--teleport start anchor--><!--teleport anchor-->"`,
+ )
+
+ data.value.msg = 'baz'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--teleport start-->` +
+ `<span>baz</span>` +
+ `<span class="baz"></span>` +
+ `<!--teleport end-->`,
+ )
+
+ data.value.disabled = false
+ await nextTick()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--teleport start--><!--teleport end-->"`,
+ )
+ expect(formatHtml(teleportContainer.innerHTML)).toBe(
+ `<!--teleport start anchor-->` +
+ `<span>baz</span>` +
+ `<span class="baz"></span>` +
+ `<!--teleport anchor-->`,
+ )
+ })
+
+ test('multiple + integration', async () => {
+ const data = ref({
+ msg: ref('foo'),
+ fn1: vi.fn(),
+ fn2: vi.fn(),
+ })
+
+ const code = `
+ <teleport to="#teleport2">
+ <span>{{data.msg}}</span>
+ <span :class="data.msg" @click="data.fn1"></span>
+ </teleport>
+ <teleport to="#teleport2">
+ <span>{{data.msg}}2</span>
+ <span :class="data.msg + 2" @click="data.fn2"></span>
+ </teleport>`
+
+ const SSRComp = compileVaporComponent(code, data, undefined, true)
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = 'teleport2'
+ const ctx = {} as any
+ const mainHtml = await VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRComp),
+ ctx,
+ )
+ expect(mainHtml).toBe(
+ `<!--[-->` +
+ `<!--teleport start--><!--teleport end-->` +
+ `<!--teleport start--><!--teleport end-->` +
+ `<!--]-->`,
+ )
+
+ const teleportHtml = ctx.teleports!['#teleport2']
+ expect(teleportHtml).toBe(
+ `<!--teleport start anchor-->` +
+ `<span>foo</span><span class="foo"></span>` +
+ `<!--teleport anchor-->` +
+ `<!--teleport start anchor-->` +
+ `<span>foo2</span><span class="foo2"></span>` +
+ `<!--teleport anchor-->`,
+ )
+
+ teleportContainer.innerHTML = teleportHtml
+ document.body.appendChild(teleportContainer)
+
+ const { block, container } = await mountWithHydration(
+ mainHtml,
+ code,
+ data,
+ )
+
+ const teleports = block as any as TeleportFragment[]
+ const teleport1 = teleports[0]
+ const teleport2 = teleports[1]
+ expect(teleport1.anchor).toBe(container.childNodes[2])
+ expect(teleport2.anchor).toBe(container.childNodes[4])
+
+ expect(teleport1.target).toBe(teleportContainer)
+ expect(teleport1.targetStart).toBe(teleportContainer.childNodes[0])
+ expect((teleport1.nodes as Node[])[0]).toBe(
+ teleportContainer.childNodes[1],
+ )
+ expect(teleport1.targetAnchor).toBe(teleportContainer.childNodes[3])
+
+ expect(teleport2.target).toBe(teleportContainer)
+ expect(teleport2.targetStart).toBe(teleportContainer.childNodes[4])
+ expect((teleport2.nodes as Node[])[0]).toBe(
+ teleportContainer.childNodes[5],
+ )
+ expect(teleport2.targetAnchor).toBe(teleportContainer.childNodes[7])
+
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<!--teleport start--><!--teleport end-->` +
+ `<!--teleport start--><!--teleport end-->` +
+ `<!--]-->`,
+ )
+
+ // event handler
+ triggerEvent('click', teleportContainer.querySelector('.foo')!)
+ expect(data.value.fn1).toHaveBeenCalled()
+
+ triggerEvent('click', teleportContainer.querySelector('.foo2')!)
+ expect(data.value.fn2).toHaveBeenCalled()
+
+ data.value.msg = 'bar'
+ await nextTick()
+ expect(teleportContainer.innerHTML).toBe(
+ `<!--teleport start anchor-->` +
+ `<span>bar</span>` +
+ `<span class="bar"></span>` +
+ `<!--teleport anchor-->` +
+ `<!--teleport start anchor-->` +
+ `<span>bar2</span>` +
+ `<span class="bar2"></span>` +
+ `<!--teleport anchor-->`,
+ )
+ })
+
+ test('disabled', async () => {
+ const data = ref({
+ msg: ref('foo'),
+ fn1: vi.fn(),
+ fn2: vi.fn(),
+ })
+
+ const code = `
+ <div>foo</div>
+ <teleport to="#teleport3" disabled="true">
+ <span>{{data.msg}}</span>
+ <span :class="data.msg" @click="data.fn1"></span>
+ </teleport>
+ <div :class="data.msg + 2" @click="data.fn2">bar</div>
+ `
+
+ const SSRComp = compileVaporComponent(code, data, undefined, true)
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = 'teleport3'
+ const ctx = {} as any
+ const mainHtml = await VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRComp),
+ ctx,
+ )
+ expect(mainHtml).toBe(
+ `<!--[-->` +
+ `<div>foo</div>` +
+ `<!--teleport start-->` +
+ `<span>foo</span>` +
+ `<span class="foo"></span>` +
+ `<!--teleport end-->` +
+ `<div class="foo2">bar</div>` +
+ `<!--]-->`,
+ )
+
+ const teleportHtml = ctx.teleports!['#teleport3']
+ expect(teleportHtml).toMatchInlineSnapshot(
+ `"<!--teleport start anchor--><!--teleport anchor-->"`,
+ )
+
+ teleportContainer.innerHTML = teleportHtml
+ document.body.appendChild(teleportContainer)
+
+ const { block, container } = await mountWithHydration(
+ mainHtml,
+ code,
+ data,
+ )
+
+ const blocks = block as any[]
+ expect(blocks[0]).toBe(container.childNodes[1])
+
+ const teleport = blocks[1] as TeleportFragment
+ expect((teleport.nodes as Node[])[0]).toBe(container.childNodes[3])
+ expect((teleport.nodes as Node[])[1]).toBe(container.childNodes[4])
+ expect(teleport.anchor).toBe(container.childNodes[5])
+ expect(teleport.target).toBe(teleportContainer)
+ expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
+ expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[1])
+ expect(blocks[2]).toBe(container.childNodes[6])
+
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<div>foo</div>` +
+ `<!--teleport start-->` +
+ `<span>foo</span>` +
+ `<span class="foo"></span>` +
+ `<!--teleport end-->` +
+ `<div class="foo2">bar</div>` +
+ `<!--]-->`,
+ )
+
+ // event handler
+ triggerEvent('click', container.querySelector('.foo')!)
+ expect(data.value.fn1).toHaveBeenCalled()
+
+ triggerEvent('click', container.querySelector('.foo2')!)
+ expect(data.value.fn2).toHaveBeenCalled()
+
+ data.value.msg = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<div>foo</div>` +
+ `<!--teleport start-->` +
+ `<span>bar</span>` +
+ `<span class="bar"></span>` +
+ `<!--teleport end-->` +
+ `<div class="bar2">bar</div>` +
+ `<!--]-->`,
+ )
+ })
+
+ test('disabled + as component root', async () => {
+ const { container } = await mountWithHydration(
+ `<!--[-->` +
+ `<div>Parent fragment</div>` +
+ `<!--teleport start--><div>Teleport content</div><!--teleport end-->` +
+ `<!--]-->`,
+ `
+ <div>Parent fragment</div>
+ <teleport to="body" disabled>
+ <div>Teleport content</div>
+ </teleport>
+ `,
+ )
+ expect(container.innerHTML).toBe(
+ `<!--[-->` +
+ `<div>Parent fragment</div>` +
+ `<!--teleport start-->` +
+ `<div>Teleport content</div>` +
+ `<!--teleport end-->` +
+ `<!--]-->`,
+ )
+ expect(`mismatch`).not.toHaveBeenWarned()
+ })
+
+ test('as component root', async () => {
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = 'teleport4'
+ teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
+ document.body.appendChild(teleportContainer)
+
+ const { block, container } = await mountWithHydration(
+ '<!--teleport start--><!--teleport end-->',
+ `<components.Wrapper></components.Wrapper>`,
+ undefined,
+ {
+ Wrapper: compileVaporComponent(
+ `<teleport to="#teleport4">hello</teleport>`,
+ ),
+ },
+ )
+
+ const teleport = (block as VaporComponentInstance)
+ .block as TeleportFragment
+ expect(teleport.anchor).toBe(container.childNodes[1])
+ expect(teleport.target).toBe(teleportContainer)
+ expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
+ expect(teleport.nodes).toBe(teleportContainer.childNodes[1])
+ expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[2])
+ })
+
+ test('nested', async () => {
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = 'teleport5'
+ teleportContainer.innerHTML =
+ `<!--teleport start anchor-->` +
+ `<!--teleport start--><!--teleport end-->` +
+ `<!--teleport anchor-->` +
+ `<!--teleport start anchor-->` +
+ `<div>child</div>` +
+ `<!--teleport anchor-->`
+ document.body.appendChild(teleportContainer)
+
+ const { block, container } = await mountWithHydration(
+ '<!--teleport start--><!--teleport end-->',
+ `<teleport to="#teleport5">
+ <teleport to="#teleport5"><div>child</div></teleport>
+ </teleport>`,
+ )
+
+ const teleport = block as TeleportFragment
+ expect(teleport.anchor).toBe(container.childNodes[1])
+ expect(teleport.targetStart).toBe(teleportContainer.childNodes[0])
+ expect(teleport.targetAnchor).toBe(teleportContainer.childNodes[3])
+
+ const childTeleport = teleport.nodes as TeleportFragment
+ expect(childTeleport.anchor).toBe(teleportContainer.childNodes[2])
+ expect(childTeleport.targetStart).toBe(teleportContainer.childNodes[4])
+ expect(childTeleport.targetAnchor).toBe(teleportContainer.childNodes[6])
+ expect(childTeleport.nodes).toBe(teleportContainer.childNodes[5])
+ })
+
+ test('unmount (full integration)', async () => {
+ const targetId = 'teleport6'
+ const data = ref({
+ toggle: ref(true),
+ })
+
+ const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
+ const Comp1 = compileVaporComponent(template1)
+ const SSRComp1 = compileVaporComponent(
+ template1,
+ undefined,
+ undefined,
+ true,
+ )
+
+ const template2 = `<div>Comp2</div>`
+ const Comp2 = compileVaporComponent(template2)
+ const SSRComp2 = compileVaporComponent(
+ template2,
+ undefined,
+ undefined,
+ true,
+ )
+
+ const appCode = `
+ <div>
+ <components.Comp1 v-if="data.toggle"/>
+ <components.Comp2 v-else/>
+ </div>
+ `
+
+ const SSRApp = compileVaporComponent(
+ appCode,
+ data,
+ {
+ Comp1: SSRComp1,
+ Comp2: SSRComp2,
+ },
+ true,
+ )
+
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = targetId
+ document.body.appendChild(teleportContainer)
+
+ const ctx = {} as any
+ const mainHtml = await VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRApp),
+ ctx,
+ )
+ expect(mainHtml).toBe(
+ '<div><!--teleport start--><!--teleport end--></div>',
+ )
+ teleportContainer.innerHTML = ctx.teleports![`#${targetId}`]
+
+ const { container } = await mountWithHydration(mainHtml, appCode, data, {
+ Comp1,
+ Comp2,
+ })
+
+ expect(container.innerHTML).toBe(
+ '<div><!--teleport start--><!--teleport end--><!--if--></div>',
+ )
+ expect(teleportContainer.innerHTML).toBe(
+ `<!--teleport start anchor-->` +
+ `<span>Teleported Comp1</span>` +
+ `<!--teleport anchor-->`,
+ )
+ expect(`mismatch`).not.toHaveBeenWarned()
+
+ data.value.toggle = false
+ await nextTick()
+ expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
+ expect(teleportContainer.innerHTML).toBe('')
+ })
+
+ test('unmount (mismatch + full integration)', async () => {
+ const targetId = 'teleport7'
+ const data = ref({
+ toggle: ref(true),
+ })
+
+ const template1 = `<Teleport to="#${targetId}"><span>Teleported Comp1</span></Teleport>`
+ const Comp1 = compileVaporComponent(template1)
+ const SSRComp1 = compileVaporComponent(
+ template1,
+ undefined,
+ undefined,
+ true,
+ )
+
+ const template2 = `<div>Comp2</div>`
+ const Comp2 = compileVaporComponent(template2)
+ const SSRComp2 = compileVaporComponent(
+ template2,
+ undefined,
+ undefined,
+ true,
+ )
+
+ const appCode = `
+ <div>
+ <components.Comp1 v-if="data.toggle"/>
+ <components.Comp2 v-else/>
+ </div>
+ `
+
+ const SSRApp = compileVaporComponent(
+ appCode,
+ data,
+ {
+ Comp1: SSRComp1,
+ Comp2: SSRComp2,
+ },
+ true,
+ )
+
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = targetId
+ document.body.appendChild(teleportContainer)
+
+ const mainHtml = await VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRApp),
+ )
+ expect(mainHtml).toBe(
+ '<div><!--teleport start--><!--teleport end--></div>',
+ )
+ expect(teleportContainer.innerHTML).toBe('')
+
+ const { container } = await mountWithHydration(mainHtml, appCode, data, {
+ Comp1,
+ Comp2,
+ })
+
+ expect(container.innerHTML).toBe(
+ '<div><!--teleport start--><!--teleport end--><!--if--></div>',
+ )
+ expect(teleportContainer.innerHTML).toBe(`<span>Teleported Comp1</span>`)
+ expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+ data.value.toggle = false
+ await nextTick()
+ expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
+ expect(teleportContainer.innerHTML).toBe('')
+ })
+
+ test('target change (mismatch + full integration)', async () => {
+ const targetId1 = 'teleport8-1'
+ const targetId2 = 'teleport8-2'
+ const data = ref({
+ target: ref(targetId1),
+ msg: ref('foo'),
+ })
+
+ const template = `<Teleport :to="'#' + data.target"><span>{{data.msg}}</span></Teleport>`
+ const Comp = compileVaporComponent(template, data)
+ const SSRComp = compileVaporComponent(template, data, undefined, true)
+
+ const teleportContainer1 = document.createElement('div')
+ teleportContainer1.id = targetId1
+ const teleportContainer2 = document.createElement('div')
+ teleportContainer2.id = targetId2
+ document.body.appendChild(teleportContainer1)
+ document.body.appendChild(teleportContainer2)
+
+ // server render
+ const mainHtml = await VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRComp),
+ )
+ expect(mainHtml).toBe(`<!--teleport start--><!--teleport end-->`)
+ expect(teleportContainer1.innerHTML).toBe('')
+ expect(teleportContainer2.innerHTML).toBe('')
+
+ // hydrate
+ const { container } = await mountWithHydration(mainHtml, template, data, {
+ Comp,
+ })
+
+ expect(container.innerHTML).toBe(
+ `<!--teleport start--><!--teleport end-->`,
+ )
+ expect(teleportContainer1.innerHTML).toBe(`<span>foo</span>`)
+ expect(teleportContainer2.innerHTML).toBe('')
+ expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+ data.value.target = targetId2
+ data.value.msg = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--teleport start--><!--teleport end-->`,
+ )
+ expect(teleportContainer1.innerHTML).toBe('')
+ expect(teleportContainer2.innerHTML).toBe(`<span>bar</span>`)
+ })
+
+ test('with disabled teleport + undefined target', async () => {
+ const data = ref({
+ msg: ref('foo'),
+ })
+
+ const { container } = await mountWithHydration(
+ '<!--teleport start--><span>foo</span><!--teleport end-->',
+ `<teleport :to="undefined" :disabled="true">
+ <span>{{data.msg}}</span>
+ </teleport>`,
+ data,
+ )
+
+ expect(container.innerHTML).toBe(
+ `<!--teleport start--><span>foo</span><!--teleport end-->`,
+ )
+
+ data.value.msg = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<!--teleport start--><span>bar</span><!--teleport end-->`,
+ )
+ })
+ })
describe.todo('Suspense')
})
import { VaporVForFlags } from '../../shared/src/vaporFlags'
import {
advanceHydrationNode,
+ currentHydrationNode,
+ isComment,
isHydrating,
- locateFragmentEndAnchor,
locateHydrationNode,
setCurrentHydrationNode,
} from './dom/hydration'
findLastChild(newBlocks[newLength - 1].nodes)!.nextSibling,
)
}
- parentAnchor = locateFragmentEndAnchor()!
- if (__DEV__) {
- if (!parentAnchor) {
- throw new Error(`v-for fragment anchor node was not found.`)
- }
- ;(parentAnchor as Comment).data = 'for'
+ parentAnchor =
+ newLength === 0
+ ? currentHydrationNode!.nextSibling!
+ : currentHydrationNode!
+ if (!parentAnchor || (parentAnchor && !isComment(parentAnchor, ']'))) {
+ throw new Error(`v-for fragment anchor node was not found.`)
}
}
} else {
type VaporFragment,
isFragment,
} from './fragment'
-import { child } from './dom/node'
+import { _child } from './dom/node'
+import { TeleportFragment } from './components/Teleport'
export interface TransitionOptions {
$key?: any
export function insert(
block: Block,
- parent: ParentNode & { $anchor?: Node | null },
+ parent: ParentNode & { $prependAnchor?: Node | null },
anchor: Node | null | 0 = null, // 0 means prepend
parentSuspense?: any, // TODO Suspense
): void {
- anchor = anchor === 0 ? child(parent) : anchor
+ anchor = anchor === 0 ? parent.$prependAnchor || _child(parent) : anchor
if (block instanceof Node) {
if (!isHydrating) {
// only apply transition on Element nodes
} else if (isVaporComponent(block)) {
nodes.push(...normalizeBlock(block.block!))
} else {
- if (block.getNodes) {
- nodes.push(...normalizeBlock(block.getNodes()))
+ if (block instanceof TeleportFragment) {
+ nodes.push(block.placeholder!, block.anchor!)
} else {
nodes.push(...normalizeBlock(block.nodes))
+ block.anchor && nodes.push(block.anchor)
}
- block.anchor && nodes.push(block.anchor)
}
return nodes
}
resolveDynamicProps,
setupPropsValidation,
} from './componentProps'
-import { renderEffect } from './renderEffect'
+import { type RenderEffect, renderEffect } from './renderEffect'
import { emit, normalizeEmitsOptions } from './componentEmits'
import { setDynamicProps } from './dom/prop'
import {
locateHydrationNode,
setCurrentHydrationNode,
} from './dom/hydration'
-import { isVaporTeleport } from './components/Teleport'
+import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
import {
insertionAnchor,
insertionParent,
setupState?: Record<string, any>
devtoolsRawSetupState?: any
hmrRerender?: () => void
- hmrRerenderEffects?: (() => void)[]
hmrReload?: (newComp: VaporComponent) => void
+ renderEffects?: RenderEffect[]
+ parentTeleport?: TeleportFragment | null
propsOptions?: NormalizedPropsOptions
emitsOptions?: ObjectEmitsOptions | null
isSingleRoot?: boolean
import {
+ MismatchTypes,
type TeleportProps,
- currentInstance,
+ type TeleportTargetElement,
+ isMismatchAllowed,
isTeleportDeferred,
isTeleportDisabled,
queuePostFlushCb,
import {
type LooseRawProps,
type LooseRawSlots,
- type VaporComponentInstance,
isVaporComponent,
} from '../component'
import { rawPropsProxyHandlers } from '../componentProps'
import { renderEffect } from '../renderEffect'
import { extend, isArray } from '@vue/shared'
import { VaporFragment } from '../fragment'
-
-const instanceToTeleportMap: WeakMap<VaporComponentInstance, TeleportFragment> =
- __DEV__ ? new WeakMap() : (undefined as any)
+import {
+ advanceHydrationNode,
+ currentHydrationNode,
+ isComment,
+ isHydrating,
+ logMismatchError,
+ runWithoutHydration,
+ setCurrentHydrationNode,
+} from '../dom/hydration'
export const VaporTeleportImpl = {
name: 'VaporTeleport',
__vapor: true,
process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment {
- const frag = new TeleportFragment()
- const updateChildrenEffect = renderEffect(() =>
- frag.updateChildren(slots.default && (slots.default as BlockFn)()),
- )
-
- const updateEffect = renderEffect(() => {
- // access the props to trigger tracking
- frag.props = extend(
- {},
- new Proxy(props, rawPropsProxyHandlers) as any as TeleportProps,
- )
- frag.update()
- })
-
- if (__DEV__) {
- // used in `normalizeBlock` to get nodes of TeleportFragment during
- // HMR updates. returns empty array if content is mounted in target
- // container to prevent incorrect parent node lookup.
- frag.getNodes = () => {
- return frag.parent !== frag.currentParent ? [] : frag.nodes
- }
-
- // for HMR rerender
- const instance = currentInstance as VaporComponentInstance
- ;(
- instance!.hmrRerenderEffects || (instance!.hmrRerenderEffects = [])
- ).push(() => {
- // remove the teleport content
- frag.remove()
-
- // stop effects
- updateChildrenEffect.stop()
- updateEffect.stop()
- })
-
- // for HMR reload
- const nodes = frag.nodes
- if (isVaporComponent(nodes)) {
- instanceToTeleportMap.set(nodes, frag)
- } else if (isArray(nodes)) {
- nodes.forEach(
- node =>
- isVaporComponent(node) && instanceToTeleportMap.set(node, frag),
- )
- }
- }
-
- return frag
+ return new TeleportFragment(props, slots)
},
}
export class TeleportFragment extends VaporFragment {
+ anchor?: Node
+ private rawProps?: LooseRawProps
+ private resolvedProps?: TeleportProps
+ private rawSlots?: LooseRawSlots
+
target?: ParentNode | null
targetAnchor?: Node | null
- anchor: Node
- props?: TeleportProps
+ targetStart?: Node | null
- private targetStart?: Node
- private mainAnchor?: Node
- private placeholder?: Node
- private mountContainer?: ParentNode | null
- private mountAnchor?: Node | null
+ placeholder?: Node
+ mountContainer?: ParentNode | null
+ mountAnchor?: Node | null
- constructor() {
+ constructor(props: LooseRawProps, slots: LooseRawSlots) {
super([])
- this.anchor = createTextNode()
- }
+ this.rawProps = props
+ this.rawSlots = slots
+ this.anchor = isHydrating
+ ? undefined
+ : __DEV__
+ ? createComment('teleport end')
+ : createTextNode()
- get currentParent(): ParentNode {
- return (this.mountContainer || this.parent)!
- }
+ renderEffect(() => {
+ // access the props to trigger tracking
+ this.resolvedProps = extend(
+ {},
+ new Proxy(
+ this.rawProps!,
+ rawPropsProxyHandlers,
+ ) as any as TeleportProps,
+ )
+ this.handlePropsUpdate()
+ })
- get currentAnchor(): Node | null {
- return this.mountAnchor || this.anchor
+ if (!isHydrating) {
+ this.initChildren()
+ }
}
get parent(): ParentNode | null {
- return this.anchor && this.anchor.parentNode
+ return this.anchor ? this.anchor.parentNode : null
}
- updateChildren(children: Block): void {
+ private initChildren(): void {
+ renderEffect(() => {
+ this.handleChildrenUpdate(
+ this.rawSlots!.default && (this.rawSlots!.default as BlockFn)(),
+ )
+ })
+
+ if (__DEV__) {
+ const nodes = this.nodes
+ if (isVaporComponent(nodes)) {
+ nodes.parentTeleport = this
+ } else if (isArray(nodes)) {
+ nodes.forEach(
+ node => isVaporComponent(node) && (node.parentTeleport = this),
+ )
+ }
+ }
+ }
+
+ private handleChildrenUpdate(children: Block): void {
// not mounted yet
- if (!this.parent) {
+ if (!this.parent || isHydrating) {
this.nodes = children
return
}
// teardown previous nodes
- remove(this.nodes, this.currentParent)
+ remove(this.nodes, this.mountContainer!)
// mount new nodes
- insert((this.nodes = children), this.currentParent, this.currentAnchor)
+ insert((this.nodes = children), this.mountContainer!, this.mountAnchor!)
}
- update(): void {
+ private handlePropsUpdate(): void {
// not mounted yet
- if (!this.parent) return
+ if (!this.parent || isHydrating) return
const mount = (parent: ParentNode, anchor: Node | null) => {
insert(
const mountToTarget = () => {
const target = (this.target = resolveTeleportTarget(
- this.props!,
+ this.resolvedProps!,
querySelector,
))
if (target) {
}
// mount into main container
- if (isTeleportDisabled(this.props!)) {
- mount(this.parent, this.mainAnchor!)
+ if (isTeleportDisabled(this.resolvedProps!)) {
+ mount(this.parent, this.anchor!)
}
// mount into target container
else {
- if (isTeleportDeferred(this.props!)) {
+ if (isTeleportDeferred(this.resolvedProps!)) {
queuePostFlushCb(mountToTarget)
} else {
mountToTarget()
}
insert = (container: ParentNode, anchor: Node | null): void => {
+ if (isHydrating) return
+
// insert anchors in the main view
this.placeholder = __DEV__
? createComment('teleport start')
: createTextNode()
- this.mainAnchor = __DEV__ ? createComment('teleport end') : createTextNode()
insert(this.placeholder, container, anchor)
- insert(this.mainAnchor, container, anchor)
- this.update()
+ insert(this.anchor!, container, anchor)
+ this.handlePropsUpdate()
}
remove = (parent: ParentNode | undefined = this.parent!): void => {
// remove nodes
if (this.nodes) {
- remove(this.nodes, this.currentParent)
+ remove(this.nodes, this.mountContainer!)
this.nodes = []
}
this.targetAnchor = undefined
}
+ if (this.anchor) {
+ remove(this.anchor, this.anchor.parentNode!)
+ this.anchor = undefined
+ }
+
if (this.placeholder) {
remove(this.placeholder!, parent)
this.placeholder = undefined
- remove(this.mainAnchor!, parent)
- this.mainAnchor = undefined
}
this.mountContainer = undefined
this.mountAnchor = undefined
}
+ private hydrateDisabledTeleport(targetNode: Node | null): void {
+ let nextNode = this.placeholder!.nextSibling!
+ setCurrentHydrationNode(nextNode)
+ this.mountAnchor = this.anchor = locateTeleportEndAnchor(nextNode)!
+ this.mountContainer = this.anchor.parentNode
+ this.targetStart = targetNode
+ this.targetAnchor = targetNode && targetNode.nextSibling
+ this.initChildren()
+ }
+
+ private mount(target: Node): void {
+ target.appendChild((this.targetStart = createTextNode('')))
+ target.appendChild(
+ (this.mountAnchor = this.targetAnchor = createTextNode('')),
+ )
+
+ if (!isMismatchAllowed(target as Element, MismatchTypes.CHILDREN)) {
+ if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
+ warn(
+ `Hydration children mismatch on`,
+ target,
+ `\nServer rendered element contains fewer child nodes than client nodes.`,
+ )
+ }
+ logMismatchError()
+ }
+
+ runWithoutHydration(this.initChildren.bind(this))
+ }
+
hydrate = (): void => {
- // TODO
+ const target = (this.target = resolveTeleportTarget(
+ this.resolvedProps!,
+ querySelector,
+ ))
+ const disabled = isTeleportDisabled(this.resolvedProps!)
+ this.placeholder = currentHydrationNode!
+ if (target) {
+ const targetNode =
+ (target as TeleportTargetElement)._lpa || target.firstChild
+ if (disabled) {
+ this.hydrateDisabledTeleport(targetNode)
+ } else {
+ this.anchor = locateTeleportEndAnchor()!
+ this.mountContainer = target
+ let targetAnchor = targetNode
+ while (targetAnchor) {
+ if (targetAnchor && targetAnchor.nodeType === 8) {
+ if ((targetAnchor as Comment).data === 'teleport start anchor') {
+ this.targetStart = targetAnchor
+ } else if ((targetAnchor as Comment).data === 'teleport anchor') {
+ this.mountAnchor = this.targetAnchor = targetAnchor
+ ;(target as TeleportTargetElement)._lpa =
+ this.targetAnchor && this.targetAnchor.nextSibling
+ break
+ }
+ }
+ targetAnchor = targetAnchor.nextSibling
+ }
+
+ if (targetNode) {
+ setCurrentHydrationNode(targetNode.nextSibling)
+ }
+
+ // if the HTML corresponding to Teleport is not embedded in the
+ // correct position on the final page during SSR. the targetAnchor will
+ // always be null, we need to manually add targetAnchor to ensure
+ // Teleport it can properly unmount or move
+ if (!this.targetAnchor) {
+ this.mount(target)
+ } else {
+ this.initChildren()
+ }
+ }
+ } else if (disabled) {
+ this.hydrateDisabledTeleport(currentHydrationNode!)
+ }
+
+ advanceHydrationNode(this.anchor!)
}
}
return value === VaporTeleportImpl
}
-/**
- * dev only
- * during root component HMR reload, since the old component will be unmounted
- * and a new one will be mounted, we need to update the teleport's nodes
- * to ensure they are up to date.
- */
-export function handleTeleportRootComponentHmrReload(
- instance: VaporComponentInstance,
- newInstance: VaporComponentInstance,
-): void {
- const teleport = instanceToTeleportMap.get(instance)
- if (teleport) {
- instanceToTeleportMap.set(newInstance, teleport)
- if (teleport.nodes === instance) {
- teleport.nodes = newInstance
- } else if (isArray(teleport.nodes)) {
- const i = teleport.nodes.indexOf(instance)
- if (i !== -1) teleport.nodes[i] = newInstance
+function locateTeleportEndAnchor(
+ node: Node = currentHydrationNode!,
+): Node | null {
+ while (node) {
+ if (isComment(node, 'teleport end')) {
+ return node
}
+ node = node.nextSibling as Node
}
+ return null
}
import { MismatchTypes, isMismatchAllowed, warn } from '@vue/runtime-dom'
import {
type ChildItem,
- incrementIndexOffset,
insertionAnchor,
insertionParent,
resetInsertionState,
setInsertionState,
} from '../insertionState'
import {
+ _child,
_next,
- child,
createElement,
createTextNode,
disableHydrationNodeLookup,
enableHydrationNodeLookup,
+ locateChildByLogicalIndex,
parentNode,
} from './node'
import { remove } from '../block'
export let isHydrating = false
export let currentHydrationNode: Node | null = null
+export function runWithoutHydration(fn: () => any): any {
+ try {
+ isHydrating = false
+ return fn()
+ } finally {
+ isHydrating = true
+ }
+}
+
let isOptimized = false
function performHydration<T>(
;(Node.prototype as any).$pns = undefined
;(Node.prototype as any).$uc = undefined
;(Node.prototype as any).$idx = undefined
- ;(Node.prototype as any).$children = undefined
- ;(Node.prototype as any).$idxMap = undefined
;(Node.prototype as any).$prevDynamicCount = undefined
;(Node.prototype as any).$anchorCount = undefined
;(Node.prototype as any).$appendIndex = undefined
- ;(Node.prototype as any).$indexOffset = undefined
isOptimized = true
}
) {
const parent = parentNode(node)!
node = parent.insertBefore(createTextNode(), node)
- incrementIndexOffset(parent)
break
}
}
function locateHydrationNodeImpl(): void {
let node: Node | null
- let idxMap: number[] | undefined
- if (insertionAnchor !== undefined && (idxMap = insertionParent!.$idxMap)) {
+ if (insertionAnchor !== undefined) {
const {
$prevDynamicCount: prevDynamicCount = 0,
$appendIndex: appendIndex,
- $indexOffset: indexOffset = 0,
$anchorCount: anchorCount = 0,
} = insertionParent!
// prepend
if (insertionAnchor === 0) {
// use prevDynamicCount as logical index to locate the hydration node
- const realIndex = idxMap![prevDynamicCount] + indexOffset
- node = insertionParent!.childNodes[realIndex]
+ node = locateChildByLogicalIndex(insertionParent!, prevDynamicCount)!
}
// insert
else if (insertionAnchor instanceof Node) {
// consecutive insert operations locate the correct hydration node.
let { $idx, $uc: usedCount } = insertionAnchor as ChildItem
if (usedCount !== undefined) {
- const realIndex = idxMap![$idx + usedCount + 1] + indexOffset
- node = insertionParent!.childNodes[realIndex]
+ node = locateChildByLogicalIndex(
+ insertionParent!,
+ ($idx || 0) + usedCount + 1,
+ )!
usedCount++
} else {
- node = insertionAnchor
+ insertionParent!.$lastLogicalChild = node = insertionAnchor
// first use of this anchor: it doesn't consume the next child
// so we track unique anchor appearances for later offset correction
insertionParent!.$anchorCount = anchorCount + 1
}
// append
else {
- let realIndex: number
if (appendIndex !== null && appendIndex !== undefined) {
- realIndex = idxMap![appendIndex + 1] + indexOffset
- node = insertionParent!.childNodes[realIndex]
+ node = locateChildByLogicalIndex(insertionParent!, appendIndex + 1)!
} else {
if (insertionAnchor === null) {
- // insertionAnchor is null, indicates no previous static nodes
- // use the first child as hydration node
- realIndex = idxMap![0] + indexOffset
- node = insertionParent!.childNodes[realIndex]
+ node = locateChildByLogicalIndex(insertionParent!, 0)!
} else {
- // insertionAnchor is a number > 0
- // indicates how many static nodes precede the node to append
- // use it as index to locate the hydration node
- realIndex = idxMap![prevDynamicCount + insertionAnchor] + indexOffset
- node = insertionParent!.childNodes[realIndex]
+ node = locateChildByLogicalIndex(
+ insertionParent!,
+ prevDynamicCount + insertionAnchor,
+ )!
}
}
insertionParent!.$appendIndex = (node as ChildItem).$idx
return null
}
-export function locateFragmentEndAnchor(label: string = ']'): Comment | null {
- let node = currentHydrationNode!
- while (node) {
- if (isComment(node, label)) return node
- node = node.nextSibling!
- }
- return null
-}
-
function handleMismatch(node: Node, template: string): Node {
if (!isMismatchAllowed(node.parentElement!, MismatchTypes.CHILDREN)) {
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
// element node
const t = createElement('template') as HTMLTemplateElement
t.innerHTML = template
- const newNode = child(t.content).cloneNode(true) as Element
+ const newNode = _child(t.content).cloneNode(true) as Element
newNode.innerHTML = (node as Element).innerHTML
Array.from((node as Element).attributes).forEach(attr => {
newNode.setAttribute(attr.name, attr.value)
/* @__NO_SIDE_EFFECTS__ */
import type { ChildItem, InsertionParent } from '../insertionState'
+import { isComment, locateEndAnchor } from './hydration'
export function createElement(tagName: string): HTMLElement {
return document.createElement(tagName)
const _txt: typeof _child = _child
/**
- * Hydration-specific version of `child`.
+ * Hydration-specific version of `txt`.
*/
/* @__NO_SIDE_EFFECTS__ */
-const __txt: typeof __child = (node: ParentNode): Node => {
+const __txt = (node: ParentNode): Node => {
let n = node.firstChild!
- // since SSR doesn't generate whitespace placeholder text nodes, if firstChild
- // is null, manually insert a text node as the first child
+ // since SSR doesn't generate blank text nodes,
+ // manually insert a text node as the first child
if (!n) {
return node.appendChild(createTextNode())
}
/* @__NO_SIDE_EFFECTS__ */
export function _child(node: InsertionParent): Node {
- const children = node.$children
- return children ? children[0] : node.firstChild!
+ return node.firstChild!
}
/**
* Hydration-specific version of `child`.
*/
/* @__NO_SIDE_EFFECTS__ */
-export function __child(node: ParentNode): Node {
- return __nthChild(node, 0)!
+export function __child(node: ParentNode, logicalIndex: number = 0): Node {
+ return locateChildByLogicalIndex(node as InsertionParent, logicalIndex)!
}
/* @__NO_SIDE_EFFECTS__ */
export function _nthChild(node: InsertionParent, i: number): Node {
- const children = node.$children
- return children ? children[i] : node.childNodes[i]
+ return node.childNodes[i]
}
/**
* Hydration-specific version of `nthChild`.
*/
/* @__NO_SIDE_EFFECTS__ */
-export function __nthChild(node: Node, i: number): Node {
- const parent = node as InsertionParent
- if (parent.$idxMap) {
- const {
- $prevDynamicCount: prevDynamicCount = 0,
- $anchorCount: anchorCount = 0,
- $idxMap: idxMap,
- $indexOffset: indexOffset = 0,
- } = parent
- // prevDynamicCount tracks how many dynamic nodes have been processed
- // so far (prepend/insert/append).
- // For anchor-based insert, the first time an anchor is used we adopt the
- // anchor node itself and do NOT consume the next child in `idxMap`,
- // yet prevDynamicCount is still incremented. This overcounts the base
- // offset by 1 per unique anchor that has appeared.
- // anchorCount equals the number of unique anchors seen, so we
- // subtract it to neutralize those "first-use doesn't consume" cases:
- // base = prevDynamicCount - anchorCount
- // Then index from this base: idxMap[base + i] + indexOffset.
- const logicalIndex = prevDynamicCount - anchorCount + i
- const realIndex = idxMap[logicalIndex] + indexOffset
- return node.childNodes[realIndex]
- }
- return node.childNodes[i]
+export function __nthChild(node: Node, logicalIndex: number): Node {
+ return locateChildByLogicalIndex(node as InsertionParent, logicalIndex)!
}
/* @__NO_SIDE_EFFECTS__ */
export function _next(node: Node): Node {
- const children = (node.parentNode! as InsertionParent).$children
- return children ? children[(node as ChildItem).$idx + 1] : node.nextSibling!
+ return node.nextSibling!
}
/**
* Hydration-specific version of `next`.
*/
/* @__NO_SIDE_EFFECTS__ */
-export function __next(node: Node): Node {
- const parent = node.parentNode! as InsertionParent
- if (parent.$idxMap) {
- const { $idxMap: idxMap, $indexOffset: indexOffset = 0 } = parent
- const { $idx, $uc: usedCount = 0 } = node as ChildItem
- const logicalIndex = $idx + usedCount + 1
- const realIndex = idxMap[logicalIndex] + indexOffset
- return node.parentNode!.childNodes[realIndex]
- }
- return node.nextSibling!
+export function __next(node: Node, logicalIndex: number): Node {
+ return locateChildByLogicalIndex(
+ node.parentNode! as InsertionParent,
+ logicalIndex,
+ )!
}
type DelegatedFunction<T extends (...args: any[]) => any> = T & {
}
/* @__NO_SIDE_EFFECTS__ */
-export const txt: DelegatedFunction<typeof _txt> = node => {
- return txt.impl(node)
+export const txt: DelegatedFunction<typeof _txt> = (...args) => {
+ return txt.impl(...args)
}
-txt.impl = _child
+txt.impl = _txt
/* @__NO_SIDE_EFFECTS__ */
-export const child: DelegatedFunction<typeof _child> = node => {
- return child.impl(node)
+export const child: DelegatedFunction<typeof _child> = (...args) => {
+ return child.impl(...args)
}
child.impl = _child
/* @__NO_SIDE_EFFECTS__ */
-export const next: DelegatedFunction<typeof _next> = node => {
- return next.impl(node)
+export const next: DelegatedFunction<typeof _next> = (...args) => {
+ return next.impl(...args)
}
next.impl = _next
/* @__NO_SIDE_EFFECTS__ */
-export const nthChild: DelegatedFunction<typeof _nthChild> = (node, i) => {
- return nthChild.impl(node, i)
+export const nthChild: DelegatedFunction<typeof _nthChild> = (...args) => {
+ return nthChild.impl(...args)
}
nthChild.impl = _nthChild
*/
export function enableHydrationNodeLookup(): void {
txt.impl = __txt
- child.impl = __child
- next.impl = __next
- nthChild.impl = __nthChild
+ child.impl = __child as typeof _child
+ next.impl = __next as typeof _next
+ nthChild.impl = __nthChild as any as typeof _nthChild
}
export function disableHydrationNodeLookup(): void {
next.impl = _next
nthChild.impl = _nthChild
}
+
+export function locateChildByLogicalIndex(
+ node: InsertionParent,
+ logicalIndex: number,
+): Node | null {
+ let child = (node.$lastLogicalChild || node.firstChild) as ChildItem
+ let fromIndex = child.$idx || 0
+
+ while (child) {
+ if (fromIndex === logicalIndex) {
+ child.$idx = logicalIndex
+ return (node.$lastLogicalChild = child)
+ }
+
+ child = (
+ isComment(child, '[')
+ ? // fragment start: jump to the node after the matching end anchor
+ locateEndAnchor(child)!.nextSibling
+ : child.nextSibling
+ ) as ChildItem
+
+ fromIndex++
+ }
+
+ return null
+}
const proto = Element.prototype as any
proto.$transition = undefined
proto.$key = undefined
- proto.$evtclick = undefined
- proto.$children = undefined
+ proto.$prependAnchor = proto.$evtclick = undefined
proto.$idx = undefined
proto.$root = false
proto.$html =
import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
-import { child, createElement, createTextNode } from './node'
+import { _child, createElement, createTextNode } from './node'
let t: HTMLTemplateElement
-export let currentTemplateFn: (Function & { $idxMap?: number[] }) | undefined =
- undefined
-
-export function resetTemplateFn(): void {
- currentTemplateFn = undefined
-}
-
/*! #__NO_SIDE_EFFECTS__ */
export function template(
html: string,
let node: Node
const fn = () => {
if (isHydrating) {
- currentTemplateFn = fn
-
// do not cache the adopted node in node because it contains child nodes
// this avoids duplicate rendering of children
const adopted = adoptTemplate(currentHydrationNode!, html)!
if (!node) {
t = t || createElement('template')
t.innerHTML = html
- node = child(t.content)
+ node = _child(t.content)
}
const ret = node.cloneNode(true)
if (root) (ret as any).$root = true
isValidBlock,
remove,
} from './block'
-import type { TransitionHooks } from '@vue/runtime-dom'
+import { type TransitionHooks, queuePostFlushCb } from '@vue/runtime-dom'
import {
- advanceHydrationNode,
currentHydrationNode,
isComment,
isHydrating,
- locateFragmentEndAnchor,
locateHydrationNode,
} from './dom/hydration'
import {
} from './components/Transition'
import { type VaporComponentInstance, isVaporComponent } from './component'
import { isArray } from '@vue/shared'
-import { incrementIndexOffset } from './insertionState'
export class VaporFragment<T extends Block = Block>
implements TransitionOptions
remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
fallback?: BlockFn
- getNodes?: () => Block
setRef?: (comp: VaporComponentInstance) => void
constructor(nodes: T) {
// reuse the empty comment node as the anchor for empty if
if (this.anchorLabel === 'if' && isEmpty) {
- this.anchor = locateFragmentEndAnchor('')!
+ this.anchor = currentHydrationNode!
if (!this.anchor) {
throw new Error('Failed to locate if anchor')
} else {
}
// reuse the vdom fragment end anchor for slots
- this.anchor = locateFragmentEndAnchor()!
+ this.anchor = currentHydrationNode!
if (!this.anchor) {
throw new Error('Failed to locate slot anchor')
} else {
// create an anchor
const { parentNode, nextSibling } = findLastChild(this)!
- parentNode!.insertBefore(
- (this.anchor = createComment(this.anchorLabel!)),
- nextSibling,
- )
- // increment index offset since we dynamically inserted a comment node
- incrementIndexOffset(parentNode!)
- advanceHydrationNode(this.anchor)
+ queuePostFlushCb(() => {
+ parentNode!.insertBefore(
+ (this.anchor = __DEV__
+ ? createComment(this.anchorLabel!)
+ : createTextNode()),
+ nextSibling,
+ )
+ })
}
}
} else if (isVaporComponent(node)) {
return findLastChild(node.block!)
} else {
- if (node instanceof DynamicFragment && node.anchor) return node.anchor
+ if (node.anchor) return node.anchor
return findLastChild(node.nodes!)
}
}
mountComponent,
unmountComponent,
} from './component'
-import { handleTeleportRootComponentHmrReload } from './components/Teleport'
+import { isArray } from '@vue/shared'
export function hmrRerender(instance: VaporComponentInstance): void {
const normalized = normalizeBlock(instance.block)
const parent = normalized[0].parentNode!
const anchor = normalized[normalized.length - 1].nextSibling
remove(instance.block, parent)
- if (instance.hmrRerenderEffects) {
- instance.hmrRerenderEffects.forEach(e => e())
- instance.hmrRerenderEffects.length = 0
- }
const prev = setCurrentInstance(instance)
pushWarningContext(instance)
+ if (instance.renderEffects) {
+ instance.renderEffects.forEach(e => e.stop())
+ instance.renderEffects = []
+ }
devRender(instance)
popWarningContext()
setCurrentInstance(...prev)
const parent = normalized[0].parentNode!
const anchor = normalized[normalized.length - 1].nextSibling
unmountComponent(instance, parent)
- const prev = setCurrentInstance(instance.parent)
+ const parentInstance = instance.parent as VaporComponentInstance | null
+ const prev = setCurrentInstance(parentInstance)
const newInstance = createComponent(
newComp,
instance.rawProps,
)
setCurrentInstance(...prev)
mountComponent(newInstance, parent, anchor)
- handleTeleportRootComponentHmrReload(instance, newInstance)
+
+ updateParentBlockOnHmrReload(parentInstance, instance, newInstance)
+ updateParentTeleportOnHmrReload(instance, newInstance)
+}
+
+/**
+ * dev only
+ * update parentInstance.block to ensure that the correct parent and
+ * anchor are found during parentInstance HMR rerender/reload, as
+ * `normalizeBlock` relies on the current instance.block
+ */
+function updateParentBlockOnHmrReload(
+ parentInstance: VaporComponentInstance | null,
+ instance: VaporComponentInstance,
+ newInstance: VaporComponentInstance,
+): void {
+ if (parentInstance) {
+ if (parentInstance.block === instance) {
+ parentInstance.block = newInstance
+ } else if (isArray(parentInstance.block)) {
+ for (let i = 0; i < parentInstance.block.length; i++) {
+ if (parentInstance.block[i] === instance) {
+ parentInstance.block[i] = newInstance
+ break
+ }
+ }
+ }
+ }
+}
+
+/**
+ * dev only
+ * during root component HMR reload, since the old component will be unmounted
+ * and a new one will be mounted, we need to update the teleport's nodes
+ * to ensure that the correct parent and anchor are found during parentInstance
+ * HMR rerender/reload, as `normalizeBlock` relies on the current instance.block
+ */
+export function updateParentTeleportOnHmrReload(
+ instance: VaporComponentInstance,
+ newInstance: VaporComponentInstance,
+): void {
+ const teleport = instance.parentTeleport
+ if (teleport) {
+ newInstance.parentTeleport = teleport
+ if (teleport.nodes === instance) {
+ teleport.nodes = newInstance
+ } else if (isArray(teleport.nodes)) {
+ for (let i = 0; i < teleport.nodes.length; i++) {
+ if (teleport.nodes[i] === instance) {
+ teleport.nodes[i] = newInstance
+ break
+ }
+ }
+ }
+ }
}
-import { isComment, isHydrating } from './dom/hydration'
-import { currentTemplateFn, resetTemplateFn } from './dom/template'
+import { isHydrating } from './dom/hydration'
export type ChildItem = ChildNode & {
$idx: number
// used count as an anchor
}
export type InsertionParent = ParentNode & {
- $children?: ChildItem[]
+ $prependAnchor?: Node | null
+
/**
* hydration-specific properties
*/
- // mapping from logical index to real index in childNodes
- $idxMap?: number[]
// hydrated dynamic children count so far
$prevDynamicCount?: number
// number of unique insertion anchors that have appeared
$anchorCount?: number
// last append index
$appendIndex?: number | null
- // number of dynamically inserted nodes (e.g., comment anchors)
- $indexOffset?: number
+ // last located logical child
+ $lastLogicalChild?: Node | null
}
export let insertionParent: InsertionParent | undefined
export let insertionAnchor: Node | 0 | undefined | null
* insertion on client-side render, and used for node adoption during hydration.
*/
export function setInsertionState(
- parent: ParentNode,
+ parent: ParentNode & { $prependAnchor?: Node | null },
anchor?: Node | 0 | null | number,
): void {
insertionParent = parent
if (anchor !== undefined) {
if (isHydrating) {
insertionAnchor = anchor as Node
- initializeHydrationState(parent)
- resetTemplateFn()
+ // when the setInsertionState is called for the first time, reset $lastLogicalChild,
+ // in order to reuse it in locateChildByLogicalIndex
+ if (insertionParent.$prevDynamicCount === undefined) {
+ insertionParent!.$lastLogicalChild = null
+ }
} else {
// special handling append anchor value to null
insertionAnchor =
typeof anchor === 'number' && anchor > 0 ? null : (anchor as Node)
- cacheTemplateChildren(parent)
- }
- } else {
- insertionAnchor = undefined
- }
-}
-
-function initializeHydrationState(parent: InsertionParent) {
- if (!parent.$idxMap) {
- const childNodes = parent.childNodes
- const len = childNodes.length
-
- // fast path for single child case. use first child as hydration node
- // no need to build logical index map
- if (
- len === 1 ||
- (len === 3 &&
- isComment(childNodes[0], '[') &&
- isComment(childNodes[2], ']'))
- ) {
- insertionAnchor = undefined
- return
- }
-
- if (currentTemplateFn) {
- if (currentTemplateFn.$idxMap) {
- const idxMap = (parent.$idxMap = currentTemplateFn.$idxMap)
- // set $idx to childNodes
- for (let i = 0; i < idxMap.length; i++) {
- ;(childNodes[idxMap[i]] as ChildItem).$idx = i
- }
- } else {
- parent.$idxMap = currentTemplateFn.$idxMap = buildLogicalIndexMap(
- len,
- childNodes,
- )
- }
- } else {
- parent.$idxMap = buildLogicalIndexMap(len, childNodes)
- }
- parent.$prevDynamicCount = 0
- parent.$anchorCount = 0
- parent.$appendIndex = null
- parent.$indexOffset = 0
- }
-}
-function buildLogicalIndexMap(len: number, childNodes: NodeListOf<ChildNode>) {
- const idxMap = new Array() as number[]
- // Build logical index map:
- // - static node: map logical index to real index
- // - fragment: map logical index to start anchor's real index
- let logicalIndex = 0
- for (let i = 0; i < len; i++) {
- const n = childNodes[i] as ChildItem
- n.$idx = logicalIndex
- if (n.nodeType === 8) {
- const data = (n as any as Comment).data
- // vdom fragment
- if (data === '[') {
- idxMap[logicalIndex++] = i
- // find matching end anchor, accounting for nested fragments
- let depth = 1
- let j = i + 1
- for (; j < len; j++) {
- const c = childNodes[j] as Comment
- if (c.nodeType === 8) {
- const d = c.data
- if (d === '[') depth++
- else if (d === ']') {
- depth--
- if (depth === 0) break
- }
- }
- }
- // jump i to the end anchor
- i = j
- continue
+ // track the first child for potential future use
+ if (anchor === 0 && !parent.$prependAnchor) {
+ parent.$prependAnchor = parent.firstChild
}
}
- idxMap[logicalIndex++] = i
- }
- return idxMap
-}
-
-function cacheTemplateChildren(parent: InsertionParent) {
- if (!parent.$children) {
- const nodes = parent.childNodes
- const len = nodes.length
- if (len === 0) return
-
- const children = new Array(len) as ChildItem[]
- for (let i = 0; i < len; i++) {
- const node = nodes[i] as ChildItem
- node.$idx = i
- children[i] = node
- }
- parent.$children = children
+ } else {
+ insertionAnchor = undefined
}
}
export function resetInsertionState(): void {
insertionParent = insertionAnchor = undefined
}
-
-export function incrementIndexOffset(parent: InsertionParent): void {
- if (parent.$indexOffset !== undefined) {
- parent.$indexOffset++
- }
-}
import { type VaporComponentInstance, isVaporComponent } from './component'
import { invokeArrayFns } from '@vue/shared'
-class RenderEffect extends ReactiveEffect {
+export class RenderEffect extends ReactiveEffect {
i: VaporComponentInstance | null
job: SchedulerJob
updateJob: SchedulerJob
this.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
+
+ // register effect for stopping them during HMR rerender
+ ;(instance.renderEffects || (instance.renderEffects = [])).push(this)
}
job.i = instance
}
}
}
-export function renderEffect(
- fn: () => void,
- noLifecycle = false,
-): RenderEffect {
+export function renderEffect(fn: () => void, noLifecycle = false): void {
const effect = new RenderEffect(fn)
if (noLifecycle) {
effect.fn = fn
}
effect.run()
- return effect
}
currentHydrationNode,
isComment,
isHydrating,
- locateFragmentEndAnchor,
locateHydrationNode,
setCurrentHydrationNode,
hydrateNode as vaporHydrateNode,
> = {
mount(vnode, container, anchor, parentComponent) {
let selfAnchor = (vnode.el = vnode.anchor = createTextNode())
- if (!isHydrating) {
- container.insertBefore(selfAnchor, anchor)
- }
+ container.insertBefore(selfAnchor, anchor)
const prev = currentInstance
simpleSetCurrentInstance(parentComponent)
vnode.transition as VaporTransitionHooks,
)
}
- if (isHydrating) {
- // insert self anchor after hydration completed to avoid mismatching
- ;(instance.m || (instance.m = [])).push(() => {
- container.insertBefore(selfAnchor, anchor)
- })
- }
mountComponent(instance, container, selfAnchor)
simpleSetCurrentInstance(prev)
return instance
const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
vaporHydrateNode(node, () => {
vnode.vb = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
- vnode.el = currentHydrationNode!
- vnode.anchor = locateFragmentEndAnchor()
+ vnode.anchor = vnode.el = currentHydrationNode!
if (__DEV__ && !vnode.anchor) {
throw new Error(`Failed to locate slot anchor`)