Skip to content

Commit 32b44f1

Browse files
authored
fix(teleport): handle updates before deferred mount (#14642)
close #14640
1 parent f166353 commit 32b44f1

4 files changed

Lines changed: 491 additions & 84 deletions

File tree

packages/runtime-core/__tests__/components/Suspense.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2476,6 +2476,100 @@ describe('Suspense', () => {
24762476
expect(serializeInner(target)).toBe(``)
24772477
})
24782478

2479+
test('should not mount discarded teleport after suspense is resolved', async () => {
2480+
const target = nodeOps.createElement('div')
2481+
const showTeleport = ref(true)
2482+
2483+
const Async = defineAsyncComponent({
2484+
render() {
2485+
return h('div', 'async')
2486+
},
2487+
})
2488+
2489+
const Comp = {
2490+
setup() {
2491+
return () => {
2492+
const children = [h(Async)]
2493+
if (showTeleport.value) {
2494+
children.push(h(Teleport, { to: target }, h('div', 'teleported')))
2495+
}
2496+
return h(Suspense, null, {
2497+
default: h('div', null, children),
2498+
fallback: h('div', 'fallback'),
2499+
})
2500+
}
2501+
},
2502+
}
2503+
2504+
const root = nodeOps.createElement('div')
2505+
render(h(Comp), root)
2506+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2507+
expect(serializeInner(target)).toBe(``)
2508+
2509+
showTeleport.value = false
2510+
await nextTick()
2511+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2512+
expect(serializeInner(target)).toBe(``)
2513+
2514+
await Promise.all(deps)
2515+
await nextTick()
2516+
expect(serializeInner(root)).toBe(`<div><div>async</div></div>`)
2517+
expect(serializeInner(target)).toBe(``)
2518+
})
2519+
2520+
test('should not process discarded disabled teleport update after suspense is resolved', async () => {
2521+
const target = nodeOps.createElement('div')
2522+
const showTeleport = ref(true)
2523+
const disabled = ref(false)
2524+
2525+
const Async = defineAsyncComponent({
2526+
render() {
2527+
return h('div', 'async')
2528+
},
2529+
})
2530+
2531+
const Comp = {
2532+
setup() {
2533+
return () => {
2534+
const children = [h(Async)]
2535+
if (showTeleport.value) {
2536+
children.push(
2537+
h(
2538+
Teleport,
2539+
{ to: target, disabled: disabled.value },
2540+
h('div', 'teleported'),
2541+
),
2542+
)
2543+
}
2544+
return h(Suspense, null, {
2545+
default: h('div', null, children),
2546+
fallback: h('div', 'fallback'),
2547+
})
2548+
}
2549+
},
2550+
}
2551+
2552+
const root = nodeOps.createElement('div')
2553+
render(h(Comp), root)
2554+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2555+
expect(serializeInner(target)).toBe(``)
2556+
2557+
disabled.value = true
2558+
await nextTick()
2559+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2560+
expect(serializeInner(target)).toBe(``)
2561+
2562+
showTeleport.value = false
2563+
await nextTick()
2564+
expect(serializeInner(root)).toBe(`<div>fallback</div>`)
2565+
expect(serializeInner(target)).toBe(``)
2566+
2567+
await Promise.all(deps)
2568+
await nextTick()
2569+
expect(serializeInner(root)).toBe(`<div><div>async</div></div>`)
2570+
expect(serializeInner(target)).toBe(``)
2571+
})
2572+
24792573
//#11617
24802574
test('update async component before resolve then update again', async () => {
24812575
const arr: boolean[] = []

packages/runtime-core/__tests__/components/Teleport.spec.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,200 @@ describe('renderer: teleport', () => {
146146
)
147147
})
148148

149+
test('should keep the mounted vnode as the patch base across deferred updates', async () => {
150+
const root = document.createElement('div')
151+
document.body.appendChild(root)
152+
153+
const show = ref(false)
154+
const disabled = ref(false)
155+
const text = ref('A')
156+
const phase = ref(0)
157+
158+
const Step1 = {
159+
setup() {
160+
disabled.value = true
161+
text.value = 'B'
162+
phase.value = 1
163+
return () => h('div', 'step1')
164+
},
165+
}
166+
167+
const Step2 = {
168+
setup() {
169+
disabled.value = false
170+
text.value = 'C'
171+
return () => h('div', 'step2')
172+
},
173+
}
174+
175+
createDOMApp({
176+
render() {
177+
return show.value
178+
? [
179+
h(
180+
Teleport,
181+
{ to: '#targetId2', defer: true, disabled: disabled.value },
182+
h('div', text.value),
183+
),
184+
phase.value === 0 ? h(Step1) : h(Step2),
185+
h('div', { id: 'targetId2' }),
186+
]
187+
: [h('div')]
188+
},
189+
}).mount(root)
190+
191+
show.value = true
192+
await nextTick()
193+
194+
expect(root.innerHTML).toMatchInlineSnapshot(
195+
`"<!--teleport start--><!--teleport end--><div>step2</div><div id="targetId2"><div>C</div></div>"`,
196+
)
197+
})
198+
199+
test('should handle disabled teleport updates before deferred mount', async () => {
200+
const root = document.createElement('div')
201+
const target = document.createElement('div')
202+
target.id = 'targetId3'
203+
document.body.appendChild(root)
204+
document.body.appendChild(target)
205+
206+
const showTeleport = ref(false)
207+
const disabled = ref(false)
208+
209+
const Step = {
210+
setup() {
211+
disabled.value = true
212+
return () => h('div', 'step')
213+
},
214+
}
215+
216+
createDOMApp({
217+
render() {
218+
return showTeleport.value
219+
? [
220+
h(
221+
Teleport,
222+
{ to: '#targetId3', defer: true, disabled: disabled.value },
223+
h('div', 'teleported'),
224+
),
225+
h(Step),
226+
]
227+
: [h('div')]
228+
},
229+
}).mount(root)
230+
231+
expect(root.innerHTML).toMatchInlineSnapshot(`"<div></div>"`)
232+
expect(target.innerHTML).toBe(``)
233+
234+
showTeleport.value = true
235+
await nextTick()
236+
237+
expect(root.innerHTML).toMatchInlineSnapshot(
238+
`"<!--teleport start--><div>teleported</div><!--teleport end--><div>step</div>"`,
239+
)
240+
expect(target.innerHTML).toBe(``)
241+
})
242+
243+
test('should not mount discarded teleport after deferred updates', async () => {
244+
const root = document.createElement('div')
245+
const target = document.createElement('div')
246+
target.id = 'targetId4'
247+
document.body.appendChild(root)
248+
document.body.appendChild(target)
249+
250+
const showTeleport = ref(false)
251+
const phase = ref(0)
252+
253+
const Step1 = {
254+
setup() {
255+
phase.value = 1
256+
return () => h('div', 'step1')
257+
},
258+
}
259+
260+
const Step2 = {
261+
setup() {
262+
showTeleport.value = false
263+
return () => h('div', 'step2')
264+
},
265+
}
266+
267+
createDOMApp({
268+
render() {
269+
return showTeleport.value
270+
? [
271+
h(
272+
Teleport,
273+
{ to: '#targetId4', defer: true },
274+
h('div', 'teleported'),
275+
),
276+
phase.value === 0 ? h(Step1) : h(Step2),
277+
]
278+
: [h('div', 'done')]
279+
},
280+
}).mount(root)
281+
282+
expect(root.innerHTML).toMatchInlineSnapshot(`"<div>done</div>"`)
283+
expect(target.innerHTML).toBe(``)
284+
285+
showTeleport.value = true
286+
await nextTick()
287+
288+
expect(root.innerHTML).toMatchInlineSnapshot(`"<div>done</div>"`)
289+
expect(target.innerHTML).toBe(``)
290+
})
291+
292+
test('should not mount discarded disabled teleport after deferred updates', async () => {
293+
const root = document.createElement('div')
294+
const target = document.createElement('div')
295+
target.id = 'targetId5'
296+
document.body.appendChild(root)
297+
document.body.appendChild(target)
298+
299+
const showTeleport = ref(false)
300+
const disabled = ref(false)
301+
const phase = ref(0)
302+
303+
const Step1 = {
304+
setup() {
305+
disabled.value = true
306+
phase.value = 1
307+
return () => h('div', 'step1')
308+
},
309+
}
310+
311+
const Step2 = {
312+
setup() {
313+
showTeleport.value = false
314+
return () => h('div', 'step2')
315+
},
316+
}
317+
318+
createDOMApp({
319+
render() {
320+
return showTeleport.value
321+
? [
322+
h(
323+
Teleport,
324+
{ to: '#targetId5', defer: true, disabled: disabled.value },
325+
h('div', 'teleported'),
326+
),
327+
phase.value === 0 ? h(Step1) : h(Step2),
328+
]
329+
: [h('div', 'done')]
330+
},
331+
}).mount(root)
332+
333+
expect(root.innerHTML).toMatchInlineSnapshot(`"<div>done</div>"`)
334+
expect(target.innerHTML).toBe(``)
335+
336+
showTeleport.value = true
337+
await nextTick()
338+
339+
expect(root.innerHTML).toMatchInlineSnapshot(`"<div>done</div>"`)
340+
expect(target.innerHTML).toBe(``)
341+
})
342+
149343
// #13349
150344
test('handle deferred teleport updates before and after mount', async () => {
151345
const root = document.createElement('div')

0 commit comments

Comments
 (0)