mirror of https://github.com/vuejs/core.git
wip: hydrate vapor teleport (#13864)
This commit is contained in:
parent
2802c0f4dc
commit
12e41691c3
|
@ -157,9 +157,9 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
|
|||
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))
|
||||
|
@ -212,6 +212,30 @@ export function render(_ctx) {
|
|||
}"
|
||||
`;
|
||||
|
||||
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)
|
||||
|
@ -219,7 +243,7 @@ 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)
|
||||
|
@ -234,9 +258,9 @@ const t0 = _template("<div><span> </span> <br> </div>", true)
|
|||
|
||||
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(() => {
|
||||
|
|
|
@ -231,6 +231,23 @@ describe('compile', () => {
|
|||
)
|
||||
})
|
||||
|
||||
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>
|
||||
|
|
|
@ -47,7 +47,7 @@ const t0 = _template("<div> </div>", true)
|
|||
|
||||
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
|
||||
|
@ -86,7 +86,7 @@ const t0 = _template("<div> </div>", true)
|
|||
|
||||
export function render(_ctx) {
|
||||
const n1 = t0()
|
||||
const n0 = _child(n1)
|
||||
const n0 = _child(n1, 0)
|
||||
const x1 = _txt(n1)
|
||||
_renderEffect(() => {
|
||||
const _String = String
|
||||
|
|
|
@ -7,7 +7,7 @@ const t1 = _template("<div><div></div><!><div></div></div>", true)
|
|||
|
||||
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()
|
||||
|
@ -23,9 +23,9 @@ const t0 = _template("<div><p> </p> <p> </p></div>", true)
|
|||
|
||||
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(() => {
|
||||
|
@ -43,7 +43,7 @@ const t0 = _template("<div><div>x</div><div>x</div><div> </div></div>", true)
|
|||
|
||||
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
|
||||
|
@ -56,12 +56,12 @@ const t0 = _template("<div><div>x</div><div><span> </span></div><div><span> </sp
|
|||
|
||||
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)
|
||||
|
|
|
@ -243,7 +243,7 @@ export function render(_ctx) {
|
|||
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)
|
||||
|
@ -259,7 +259,7 @@ export function render(_ctx) {
|
|||
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)
|
||||
|
|
|
@ -17,8 +17,8 @@ const t0 = _template("<div> <span></span></div>", true)
|
|||
|
||||
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
|
||||
|
@ -54,7 +54,7 @@ const t0 = _template("<div><div></div></div>", true)
|
|||
|
||||
export function render(_ctx) {
|
||||
const n1 = t0()
|
||||
const n0 = _child(n1)
|
||||
const n0 = _child(n1, 0)
|
||||
_setProp(n0, "id", _ctx.foo)
|
||||
return n1
|
||||
}"
|
||||
|
|
|
@ -56,7 +56,7 @@ describe('compiler: children transform', () => {
|
|||
<div>{{ msg }}</div>
|
||||
</div>`,
|
||||
)
|
||||
expect(code).contains(`const n0 = _nthChild(n1, 2)`)
|
||||
expect(code).contains(`const n0 = _nthChild(n1, 2, 2)`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
|
@ -69,7 +69,7 @@ describe('compiler: children transform', () => {
|
|||
</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()
|
||||
})
|
||||
|
|
|
@ -71,7 +71,7 @@ export function genBlockContent(
|
|||
}
|
||||
for (const child of dynamic.children) {
|
||||
if (!child.hasDynamicChild) {
|
||||
push(...genChildren(child, context, `n${child.id!}`))
|
||||
push(...genChildren(child, context, push, `n${child.id!}`))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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'
|
||||
|
@ -36,7 +40,7 @@ export function genSelf(
|
|||
}
|
||||
|
||||
if (hasDynamicChild) {
|
||||
push(...genChildren(dynamic, context, `n${id}`))
|
||||
push(...genChildren(dynamic, context, push, `n${id}`))
|
||||
}
|
||||
|
||||
return frag
|
||||
|
@ -45,6 +49,7 @@ export function genSelf(
|
|||
export function genChildren(
|
||||
dynamic: IRDynamicInfo,
|
||||
context: CodegenContext,
|
||||
pushBlock: (...items: CodeFragment[]) => number,
|
||||
from: string = `n${dynamic.id}`,
|
||||
): CodeFragment[] {
|
||||
const { helper } = context
|
||||
|
@ -53,10 +58,20 @@ export function genChildren(
|
|||
|
||||
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 =
|
||||
|
@ -72,29 +87,41 @@ export function genChildren(
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,7 +134,7 @@ export function genChildren(
|
|||
}
|
||||
|
||||
prev = [variable, elementIndex]
|
||||
push(...genChildren(child, context, variable))
|
||||
push(...genChildren(child, context, pushBlock, variable))
|
||||
}
|
||||
|
||||
return frag
|
||||
|
|
|
@ -272,6 +272,7 @@ export interface IRDynamicInfo {
|
|||
hasDynamicChild?: boolean
|
||||
operation?: OperationNode
|
||||
needsKey?: boolean
|
||||
ifBranch?: boolean
|
||||
}
|
||||
|
||||
export interface IREffect {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
isConstantExpression,
|
||||
isStaticExpression,
|
||||
} from '../utils'
|
||||
import { escapeHtml } from '@vue/shared'
|
||||
|
||||
type TextLike = TextNode | InterpolationNode
|
||||
const seen = new WeakMap<
|
||||
|
@ -82,7 +83,7 @@ export const transformText: NodeTransform = (node, context) => {
|
|||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,7 +144,7 @@ function processTextContainer(
|
|||
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({
|
||||
|
|
|
@ -59,6 +59,7 @@ export function processIf(
|
|||
} 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
|
||||
|
|
|
@ -394,7 +394,7 @@ function moveTeleport(
|
|||
}
|
||||
}
|
||||
|
||||
interface TeleportTargetElement extends Element {
|
||||
export interface TeleportTargetElement extends Element {
|
||||
// last teleport target
|
||||
_lpa?: Node | null
|
||||
}
|
||||
|
|
|
@ -659,6 +659,17 @@ export function createHydrationFunctions(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
@ -584,6 +584,13 @@ export {
|
|||
isTeleportDisabled,
|
||||
isTeleportDeferred,
|
||||
} from './components/Teleport'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type { TeleportTargetElement } from './components/Teleport'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export {
|
||||
createAsyncComponentContext,
|
||||
useAsyncComponentState,
|
||||
|
|
|
@ -5,6 +5,8 @@ import * as runtimeVapor from '../src'
|
|||
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
|
||||
|
@ -77,22 +79,34 @@ async function testWithVDOMApp(
|
|||
})
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -298,7 +312,7 @@ describe('Vapor Mode hydration', () => {
|
|||
<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 () => {
|
||||
|
@ -331,7 +345,7 @@ describe('Vapor Mode hydration', () => {
|
|||
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 () => {
|
||||
|
@ -1588,7 +1602,8 @@ describe('Vapor Mode hydration', () => {
|
|||
expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
|
||||
`
|
||||
"
|
||||
<!--[--><span>a</span><span>b</span><span>c</span><!--for-->"
|
||||
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
|
||||
"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1597,7 +1612,35 @@ describe('Vapor Mode hydration', () => {
|
|||
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><!--]-->
|
||||
"
|
||||
`,
|
||||
)
|
||||
})
|
||||
|
@ -1619,7 +1662,8 @@ describe('Vapor Mode hydration', () => {
|
|||
`
|
||||
"
|
||||
<!--[--><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><!--]-->
|
||||
"
|
||||
`,
|
||||
)
|
||||
|
@ -1630,7 +1674,8 @@ describe('Vapor Mode hydration', () => {
|
|||
`
|
||||
"
|
||||
<!--[--><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><!--]-->
|
||||
"
|
||||
`,
|
||||
)
|
||||
|
@ -1651,7 +1696,8 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1660,7 +1706,8 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1669,7 +1716,8 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
})
|
||||
|
@ -1690,8 +1738,9 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1700,8 +1749,9 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1710,8 +1760,9 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
})
|
||||
|
@ -1732,7 +1783,8 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1741,7 +1793,8 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
})
|
||||
|
@ -1767,7 +1820,8 @@ describe('Vapor Mode hydration', () => {
|
|||
<!--[--><span>a</span><!--]-->
|
||||
<!--[--><span>b</span><!--]-->
|
||||
<!--[--><span>c</span><!--]-->
|
||||
<!--for--></div>"
|
||||
<!--]-->
|
||||
</div>"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1780,7 +1834,8 @@ describe('Vapor Mode hydration', () => {
|
|||
<!--[--><span>a</span><!--]-->
|
||||
<!--[--><span>b</span><!--]-->
|
||||
<!--[--><span>c</span><!--]-->
|
||||
<span>d</span><!--slot--><!--for--></div>"
|
||||
<span>d</span><!--slot--><!--]-->
|
||||
</div>"
|
||||
`,
|
||||
)
|
||||
})
|
||||
|
@ -1803,7 +1858,8 @@ describe('Vapor Mode hydration', () => {
|
|||
<!--[-->
|
||||
<!--[--><div>foo</div>-bar-<!--]-->
|
||||
<!--[--><div>foo</div>-bar-<!--]-->
|
||||
<!--[--><div>foo</div>-bar-<!--for--><!--]-->
|
||||
<!--[--><div>foo</div>-bar-<!--]-->
|
||||
<!--]-->
|
||||
</div>"
|
||||
`,
|
||||
)
|
||||
|
@ -1816,7 +1872,8 @@ describe('Vapor Mode hydration', () => {
|
|||
<!--[-->
|
||||
<!--[--><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>"
|
||||
`,
|
||||
)
|
||||
|
@ -1845,7 +1902,8 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1854,7 +1912,8 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`,
|
||||
)
|
||||
|
||||
|
@ -1862,14 +1921,16 @@ describe('Vapor Mode hydration', () => {
|
|||
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>"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
@ -2005,7 +2066,8 @@ describe('Vapor Mode hydration', () => {
|
|||
`
|
||||
"
|
||||
<!--[-->
|
||||
<!--[--><span>a</span><span>b</span><span>c</span><!--for--><!--]-->
|
||||
<!--[--><span>a</span><span>b</span><span>c</span><!--]-->
|
||||
<!--]-->
|
||||
"
|
||||
`,
|
||||
)
|
||||
|
@ -2437,9 +2499,10 @@ describe('Vapor Mode hydration', () => {
|
|||
`
|
||||
"
|
||||
<!--[-->
|
||||
<!--[--><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><!--]-->
|
||||
<!--]-->
|
||||
"
|
||||
`,
|
||||
)
|
||||
|
@ -2450,9 +2513,10 @@ describe('Vapor Mode hydration', () => {
|
|||
`
|
||||
"
|
||||
<!--[-->
|
||||
<!--[--><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><!--]-->
|
||||
<!--]-->
|
||||
"
|
||||
`,
|
||||
)
|
||||
|
@ -2920,7 +2984,587 @@ describe('Vapor Mode hydration', () => {
|
|||
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')
|
||||
})
|
||||
|
|
|
@ -27,8 +27,9 @@ import { renderEffect } from './renderEffect'
|
|||
import { VaporVForFlags } from '../../shared/src/vaporFlags'
|
||||
import {
|
||||
advanceHydrationNode,
|
||||
currentHydrationNode,
|
||||
isComment,
|
||||
isHydrating,
|
||||
locateFragmentEndAnchor,
|
||||
locateHydrationNode,
|
||||
setCurrentHydrationNode,
|
||||
} from './dom/hydration'
|
||||
|
@ -145,12 +146,12 @@ export const createFor = (
|
|||
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 {
|
||||
|
|
|
@ -19,7 +19,8 @@ import {
|
|||
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
|
||||
|
@ -68,11 +69,11 @@ export function isValidBlock(block: Block): boolean {
|
|||
|
||||
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
|
||||
|
@ -182,12 +183,12 @@ export function normalizeBlock(block: Block): Node[] {
|
|||
} 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
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ import {
|
|||
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 {
|
||||
|
@ -73,7 +73,7 @@ import {
|
|||
locateHydrationNode,
|
||||
setCurrentHydrationNode,
|
||||
} from './dom/hydration'
|
||||
import { isVaporTeleport } from './components/Teleport'
|
||||
import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
|
||||
import {
|
||||
insertionAnchor,
|
||||
insertionParent,
|
||||
|
@ -445,8 +445,9 @@ export class VaporComponentInstance implements GenericComponentInstance {
|
|||
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
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
MismatchTypes,
|
||||
type TeleportProps,
|
||||
currentInstance,
|
||||
type TeleportTargetElement,
|
||||
isMismatchAllowed,
|
||||
isTeleportDeferred,
|
||||
isTeleportDisabled,
|
||||
queuePostFlushCb,
|
||||
|
@ -12,16 +14,21 @@ import { createComment, createTextNode, querySelector } from '../dom/node'
|
|||
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',
|
||||
|
@ -29,102 +36,90 @@ export const VaporTeleportImpl = {
|
|||
__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(
|
||||
|
@ -136,7 +131,7 @@ export class TeleportFragment extends VaporFragment {
|
|||
|
||||
const mountToTarget = () => {
|
||||
const target = (this.target = resolveTeleportTarget(
|
||||
this.props!,
|
||||
this.resolvedProps!,
|
||||
querySelector,
|
||||
))
|
||||
if (target) {
|
||||
|
@ -161,12 +156,12 @@ export class TeleportFragment extends VaporFragment {
|
|||
}
|
||||
|
||||
// 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()
|
||||
|
@ -175,20 +170,21 @@ export class TeleportFragment extends VaporFragment {
|
|||
}
|
||||
|
||||
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 = []
|
||||
}
|
||||
|
||||
|
@ -200,19 +196,99 @@ export class TeleportFragment extends VaporFragment {
|
|||
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!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,24 +298,14 @@ export function isVaporTeleport(
|
|||
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
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
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'
|
||||
|
@ -22,6 +22,15 @@ const isHydratingStack = [] as boolean[]
|
|||
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>(
|
||||
|
@ -37,12 +46,9 @@ 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
|
||||
}
|
||||
|
@ -120,7 +126,6 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
|
|||
) {
|
||||
const parent = parentNode(node)!
|
||||
node = parent.insertBefore(createTextNode(), node)
|
||||
incrementIndexOffset(parent)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -143,19 +148,16 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
|
|||
|
||||
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) {
|
||||
|
@ -166,11 +168,13 @@ function locateHydrationNodeImpl(): void {
|
|||
// 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
|
||||
|
@ -180,22 +184,16 @@ function locateHydrationNodeImpl(): void {
|
|||
}
|
||||
// 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
|
||||
|
@ -246,15 +244,6 @@ export function locateEndAnchor(
|
|||
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__) &&
|
||||
|
@ -297,7 +286,7 @@ function handleMismatch(node: Node, template: string): Node {
|
|||
// 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)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* @__NO_SIDE_EFFECTS__ */
|
||||
|
||||
import type { ChildItem, InsertionParent } from '../insertionState'
|
||||
import { isComment, locateEndAnchor } from './hydration'
|
||||
|
||||
export function createElement(tagName: string): HTMLElement {
|
||||
return document.createElement(tagName)
|
||||
|
@ -30,14 +31,14 @@ export function parentNode(node: Node): ParentNode | null {
|
|||
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())
|
||||
}
|
||||
|
@ -47,74 +48,44 @@ const __txt: typeof __child = (node: ParentNode): Node => {
|
|||
|
||||
/* @__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 & {
|
||||
|
@ -122,26 +93,26 @@ 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
|
||||
|
||||
|
@ -155,9 +126,9 @@ 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 {
|
||||
|
@ -166,3 +137,29 @@ 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
|
||||
}
|
||||
|
|
|
@ -386,8 +386,7 @@ export function optimizePropertyLookup(): void {
|
|||
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 =
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
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,
|
||||
|
@ -18,8 +11,6 @@ export function template(
|
|||
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)!
|
||||
|
@ -34,7 +25,7 @@ export function template(
|
|||
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
|
||||
|
|
|
@ -9,13 +9,11 @@ import {
|
|||
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 {
|
||||
|
@ -24,7 +22,6 @@ 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
|
||||
|
@ -42,7 +39,6 @@ export class VaporFragment<T extends Block = Block>
|
|||
remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
|
||||
fallback?: BlockFn
|
||||
|
||||
getNodes?: () => Block
|
||||
setRef?: (comp: VaporComponentInstance) => void
|
||||
|
||||
constructor(nodes: T) {
|
||||
|
@ -149,7 +145,7 @@ export class DynamicFragment extends VaporFragment {
|
|||
|
||||
// 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 {
|
||||
|
@ -172,7 +168,7 @@ export class DynamicFragment extends VaporFragment {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
|
@ -182,13 +178,14 @@ export class DynamicFragment extends VaporFragment {
|
|||
|
||||
// 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,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,7 +238,7 @@ export function findLastChild(node: Block): Node | undefined | null {
|
|||
} 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!)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,19 +12,19 @@ import {
|
|||
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)
|
||||
|
@ -39,7 +39,8 @@ export function hmrReload(
|
|||
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,
|
||||
|
@ -48,5 +49,59 @@ export function hmrReload(
|
|||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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
|
||||
|
@ -7,20 +6,19 @@ export type ChildItem = ChildNode & {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -31,7 +29,7 @@ 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
|
||||
|
@ -39,119 +37,26 @@ export function setInsertionState(
|
|||
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)
|
||||
|
||||
// track the first child for potential future use
|
||||
if (anchor === 0 && !parent.$prependAnchor) {
|
||||
parent.$prependAnchor = parent.firstChild
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export function resetInsertionState(): void {
|
||||
insertionParent = insertionAnchor = undefined
|
||||
}
|
||||
|
||||
export function incrementIndexOffset(parent: InsertionParent): void {
|
||||
if (parent.$indexOffset !== undefined) {
|
||||
parent.$indexOffset++
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
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
|
||||
|
@ -41,6 +41,9 @@ class RenderEffect extends ReactiveEffect {
|
|||
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
|
||||
}
|
||||
|
@ -82,14 +85,10 @@ class RenderEffect extends ReactiveEffect {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -59,7 +59,6 @@ import {
|
|||
currentHydrationNode,
|
||||
isComment,
|
||||
isHydrating,
|
||||
locateFragmentEndAnchor,
|
||||
locateHydrationNode,
|
||||
setCurrentHydrationNode,
|
||||
hydrateNode as vaporHydrateNode,
|
||||
|
@ -75,9 +74,7 @@ const vaporInteropImpl: Omit<
|
|||
> = {
|
||||
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)
|
||||
|
||||
|
@ -119,12 +116,6 @@ const vaporInteropImpl: Omit<
|
|||
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
|
||||
|
@ -198,8 +189,7 @@ const vaporInteropImpl: Omit<
|
|||
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`)
|
||||
|
|
Loading…
Reference in New Issue