Skip to content

Commit

Permalink
fix(teleport): skip teleported nodes when locating patch anchor
Browse files Browse the repository at this point in the history
close #9071
close #9134
close #9313

Tests reused from #9313
  • Loading branch information
yyx990803 committed Jul 16, 2024
1 parent 50ddafe commit 8655ced
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 7 deletions.
69 changes: 68 additions & 1 deletion packages/runtime-core/__tests__/components/Teleport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
serializeInner,
withDirectives,
} from '@vue/runtime-test'
import { Fragment, createVNode } from '../../src/vnode'
import { Fragment, createCommentVNode, createVNode } from '../../src/vnode'
import { compile, render as domRender } from 'vue'

describe('renderer: teleport', () => {
Expand Down Expand Up @@ -553,4 +553,71 @@ describe('renderer: teleport', () => {
`"<div>teleported</div>"`,
)
})

//#9071
test('toggle sibling node inside target node', async () => {
const root = document.createElement('div')
const show = ref(false)
const App = defineComponent({
setup() {
return () => {
return show.value
? h(Teleport, { to: root }, [h('div', 'teleported')])
: h('div', 'foo')
}
},
})

domRender(h(App), root)
expect(root.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')

show.value = true
await nextTick()

expect(root.innerHTML).toMatchInlineSnapshot(
'"<!--teleport start--><!--teleport end--><div>teleported</div>"',
)

show.value = false
await nextTick()

expect(root.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')
})

test('unmount previous sibling node inside target node', async () => {
const root = document.createElement('div')
const parentShow = ref(false)
const childShow = ref(true)

const Comp = {
setup() {
return () => h(Teleport, { to: root }, [h('div', 'foo')])
},
}

const App = defineComponent({
setup() {
return () => {
return parentShow.value
? h(Fragment, { key: 0 }, [
childShow.value ? h(Comp) : createCommentVNode('v-if'),
])
: createCommentVNode('v-if')
}
},
})

domRender(h(App), root)
expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')

parentShow.value = true
await nextTick()
expect(root.innerHTML).toMatchInlineSnapshot(
'"<!--teleport start--><!--teleport end--><div>foo</div>"',
)

parentShow.value = false
await nextTick()
expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
})
})
23 changes: 20 additions & 3 deletions packages/runtime-core/src/components/Teleport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface TeleportProps {
disabled?: boolean
}

export const TeleportEndKey = Symbol('_vte')

export const isTeleport = (type: any): boolean => type.__isTeleport

const isTeleportDisabled = (props: VNode['props']): boolean =>
Expand Down Expand Up @@ -105,11 +107,16 @@ export const TeleportImpl = {
const mainAnchor = (n2.anchor = __DEV__
? createComment('teleport end')
: createText(''))
insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor)
const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetStart = (n2.targetStart = createText(''))
const targetAnchor = (n2.targetAnchor = createText(''))
insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor)
// attach a special property so we can skip teleported content in
// renderer's nextSibling search
targetStart[TeleportEndKey] = targetAnchor
if (target) {
insert(targetStart, target)
insert(targetAnchor, target)
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
if (namespace === 'svg' || isTargetSVG(target)) {
Expand Down Expand Up @@ -146,6 +153,7 @@ export const TeleportImpl = {
} else {
// update content
n2.el = n1.el
n2.targetStart = n1.targetStart
const mainAnchor = (n2.anchor = n1.anchor)!
const target = (n2.target = n1.target)!
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
Expand Down Expand Up @@ -253,9 +261,18 @@ export const TeleportImpl = {
{ um: unmount, o: { remove: hostRemove } }: RendererInternals,
doRemove: boolean,
) {
const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode
const {
shapeFlag,
children,
anchor,
targetStart,
targetAnchor,
target,
props,
} = vnode

if (target) {
hostRemove(targetStart!)
hostRemove(targetAnchor!)
}

Expand Down
15 changes: 12 additions & 3 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ import {
type SuspenseImpl,
queueEffectWithSuspense,
} from './components/Suspense'
import type { TeleportImpl, TeleportVNode } from './components/Teleport'
import {
TeleportEndKey,
type TeleportImpl,
type TeleportVNode,
} from './components/Teleport'
import { type KeepAliveContext, isKeepAlive } from './components/KeepAlive'
import { isHmrUpdating, registerHMR, unregisterHMR } from './hmr'
import { type RootHydrateFunction, createHydrationFunctions } from './hydration'
Expand Down Expand Up @@ -140,7 +144,7 @@ export interface RendererOptions<
// functions provided via options, so the internal constraint is really just
// a generic object.
export interface RendererNode {
[key: string]: any
[key: string | symbol]: any
}

export interface RendererElement extends RendererNode {}
Expand Down Expand Up @@ -2368,7 +2372,12 @@ function baseCreateRenderer(
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
return vnode.suspense!.next()
}
return hostNextSibling((vnode.anchor || vnode.el)!)
const el = hostNextSibling((vnode.anchor || vnode.el)!)
// #9071, #9313
// teleported content can mess up nextSibling searches during patch so
// we need to skip them during nextSibling search
const teleportEnd = el && el[TeleportEndKey]
return teleportEnd ? hostNextSibling(teleportEnd) : el
}

let isFlushing = false
Expand Down
3 changes: 3 additions & 0 deletions packages/runtime-core/src/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export interface VNode<
el: HostNode | null
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetStart: HostNode | null // teleport target start anchor
targetAnchor: HostNode | null // teleport target anchor
/**
* number of elements contained in a static vnode
Expand Down Expand Up @@ -477,6 +478,7 @@ function createBaseVNode(
el: null,
anchor: null,
target: null,
targetStart: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
Expand Down Expand Up @@ -677,6 +679,7 @@ export function cloneVNode<T, U>(
? (children as VNode[]).map(deepCloneVNode)
: children,
target: vnode.target,
targetStart: vnode.targetStart,
targetAnchor: vnode.targetAnchor,
staticCount: vnode.staticCount,
shapeFlag: vnode.shapeFlag,
Expand Down

0 comments on commit 8655ced

Please sign in to comment.