Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Suspense): mount runs too early when used in Transition #9388

Merged
merged 2 commits into from
Oct 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/runtime-core/src/components/Suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,17 +491,20 @@ function createSuspenseBoundary(
container
} = suspense

// if there's a transition happening we need to wait it to finish.
let delayEnter: boolean | null = false
if (suspense.isHydrating) {
suspense.isHydrating = false
} else if (!resume) {
const delayEnter =
delayEnter =
activeBranch &&
pendingBranch!.transition &&
pendingBranch!.transition.mode === 'out-in'
if (delayEnter) {
activeBranch!.transition!.afterLeave = () => {
if (pendingId === suspense.pendingId) {
move(pendingBranch!, container, anchor, MoveType.ENTER)
queuePostFlushCb(effects)
}
}
}
Expand Down Expand Up @@ -538,8 +541,8 @@ function createSuspenseBoundary(
}
parent = parent.parent
}
// no pending parent suspense, flush all jobs
if (!hasUnresolvedAncestor) {
// no pending parent suspense nor transition, flush all jobs
if (!hasUnresolvedAncestor && !delayEnter) {
queuePostFlushCb(effects)
}
suspense.effects = []
Expand Down
88 changes: 88 additions & 0 deletions packages/vue/__tests__/e2e/Transition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,94 @@ describe('e2e: Transition', () => {
},
E2E_TIMEOUT
)

// #5844
test('children mount should be called after html changes', async () => {
const fooMountSpy = vi.fn()
const barMountSpy = vi.fn()

await page().exposeFunction('fooMountSpy', fooMountSpy)
await page().exposeFunction('barMountSpy', barMountSpy)

await page().evaluate(() => {
const { fooMountSpy, barMountSpy } = window as any
const { createApp, ref, h, onMounted } = (window as any).Vue
createApp({
template: `
<div id="container">
<transition mode="out-in">
<Suspense>
<Foo v-if="toggle" />
<Bar v-else />
</Suspense>
</transition>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
components: {
Foo: {
setup() {
const el = ref(null)
onMounted(() => {
fooMountSpy(
!!el.value,
!!document.getElementById('foo'),
!!document.getElementById('bar')
)
})

return () => h('div', { ref: el, id: 'foo' }, 'Foo')
}
},
Bar: {
setup() {
const el = ref(null)
onMounted(() => {
barMountSpy(
!!el.value,
!!document.getElementById('foo'),
!!document.getElementById('bar')
)
})

return () => h('div', { ref: el, id: 'bar' }, 'Bar')
}
}
},
setup: () => {
const toggle = ref(true)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
}
}).mount('#app')
})

await nextFrame()
expect(await html('#container')).toBe('<div id="foo">Foo</div>')
await transitionFinish()

expect(fooMountSpy).toBeCalledTimes(1)
expect(fooMountSpy).toHaveBeenNthCalledWith(1, true, true, false)

await page().evaluate(async () => {
;(document.querySelector('#toggleBtn') as any)!.click()
// nextTrick for patch start
await Promise.resolve()
// nextTrick for Suspense resolve
await Promise.resolve()
// nextTrick for dom transition start
await Promise.resolve()
return document.querySelector('#container div')!.className.split(/\s+/g)
})

await nextFrame()
await transitionFinish()

expect(await html('#container')).toBe('<div id="bar" class="">Bar</div>')

expect(barMountSpy).toBeCalledTimes(1)
expect(barMountSpy).toHaveBeenNthCalledWith(1, true, false, true)
})
})

describe('transition with v-show', () => {
Expand Down