mirror of https://github.com/vuejs/core.git
Merge 196d551437
into c86bf7b11f
This commit is contained in:
commit
65ef847f25
|
@ -0,0 +1,62 @@
|
|||
import path from 'node:path'
|
||||
import {
|
||||
E2E_TIMEOUT,
|
||||
setupPuppeteer,
|
||||
} from '../../../packages/vue/__tests__/e2e/e2eUtils'
|
||||
import connect from 'connect'
|
||||
import sirv from 'sirv'
|
||||
import { nextTick } from 'vue'
|
||||
import { ports } from '../utils'
|
||||
const { page, click, html } = setupPuppeteer()
|
||||
|
||||
describe('vapor teleport', () => {
|
||||
let server: any
|
||||
const port = ports.teleport
|
||||
beforeAll(() => {
|
||||
server = connect()
|
||||
.use(sirv(path.resolve(import.meta.dirname, '../dist')))
|
||||
.listen(port)
|
||||
process.on('SIGTERM', () => server && server.close())
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const baseUrl = `http://localhost:${port}/teleport/`
|
||||
await page().goto(baseUrl)
|
||||
await page().waitForSelector('#app')
|
||||
})
|
||||
|
||||
test(
|
||||
'render vdom component',
|
||||
async () => {
|
||||
const targetSelector = '.target'
|
||||
const testSelector = '.interop-render-vdom-comp'
|
||||
const containerSelector = `${testSelector} > div`
|
||||
const btnSelector = `${testSelector} > button`
|
||||
|
||||
const tt = await html('#app')
|
||||
console.log(tt)
|
||||
|
||||
// teleport is disabled
|
||||
expect(await html(containerSelector)).toBe('<h1>vdom comp</h1>')
|
||||
expect(await html(targetSelector)).toBe('')
|
||||
|
||||
// enable teleport
|
||||
await click(btnSelector)
|
||||
await nextTick()
|
||||
|
||||
expect(await html(containerSelector)).toBe('')
|
||||
expect(await html(targetSelector)).toBe('<h1>vdom comp</h1>')
|
||||
|
||||
// disable teleport
|
||||
await click(btnSelector)
|
||||
await nextTick()
|
||||
expect(await html(containerSelector)).toBe('<h1>vdom comp</h1>')
|
||||
expect(await html(targetSelector)).toBe('')
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
})
|
|
@ -5,6 +5,7 @@ import {
|
|||
} from '../../../packages/vue/__tests__/e2e/e2eUtils'
|
||||
import connect from 'connect'
|
||||
import sirv from 'sirv'
|
||||
import { ports } from '../utils'
|
||||
|
||||
describe('e2e: todomvc', () => {
|
||||
const {
|
||||
|
@ -23,7 +24,7 @@ describe('e2e: todomvc', () => {
|
|||
} = setupPuppeteer()
|
||||
|
||||
let server: any
|
||||
const port = '8194'
|
||||
const port = ports.todomvc
|
||||
beforeAll(() => {
|
||||
server = connect()
|
||||
.use(sirv(path.resolve(import.meta.dirname, '../dist')))
|
||||
|
|
|
@ -0,0 +1,407 @@
|
|||
import path from 'node:path'
|
||||
import {
|
||||
E2E_TIMEOUT,
|
||||
setupPuppeteer,
|
||||
} from '../../../packages/vue/__tests__/e2e/e2eUtils'
|
||||
import connect from 'connect'
|
||||
import sirv from 'sirv'
|
||||
import { expect } from 'vitest'
|
||||
const { page, nextFrame, timeout, html, transitionStart } = setupPuppeteer()
|
||||
import { ports } from '../utils'
|
||||
|
||||
const duration = process.env.CI ? 200 : 50
|
||||
const buffer = process.env.CI ? 50 : 20
|
||||
const transitionFinish = (time = duration) => timeout(time + buffer)
|
||||
|
||||
describe('vapor transition-group', () => {
|
||||
let server: any
|
||||
const port = ports.transitionGroup
|
||||
beforeAll(() => {
|
||||
server = connect()
|
||||
.use(sirv(path.resolve(import.meta.dirname, '../dist')))
|
||||
.listen(port)
|
||||
process.on('SIGTERM', () => server && server.close())
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const baseUrl = `http://localhost:${port}/transition-group/`
|
||||
await page().goto(baseUrl)
|
||||
await page().waitForSelector('#app')
|
||||
})
|
||||
|
||||
test(
|
||||
'enter',
|
||||
async () => {
|
||||
const btnSelector = '.enter > button'
|
||||
const containerSelector = '.enter > div'
|
||||
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>`,
|
||||
)
|
||||
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test test-enter-from test-enter-active">d</div>` +
|
||||
`<div class="test test-enter-from test-enter-active">e</div>`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test test-enter-active test-enter-to">d</div>` +
|
||||
`<div class="test test-enter-active test-enter-to">e</div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test">d</div>` +
|
||||
`<div class="test">e</div>`,
|
||||
)
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
|
||||
test(
|
||||
'leave',
|
||||
async () => {
|
||||
const btnSelector = '.leave > button'
|
||||
const containerSelector = '.leave > div'
|
||||
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>`,
|
||||
)
|
||||
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="test test-leave-from test-leave-active">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test test-leave-from test-leave-active">c</div>`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test test-leave-active test-leave-to">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test test-leave-active test-leave-to">c</div>`,
|
||||
)
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(`<div class="test">b</div>`)
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
|
||||
test(
|
||||
'enter + leave',
|
||||
async () => {
|
||||
const btnSelector = '.enter-leave > button'
|
||||
const containerSelector = '.enter-leave > div'
|
||||
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>`,
|
||||
)
|
||||
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="test test-leave-from test-leave-active">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test test-enter-from test-enter-active">d</div>`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test test-leave-active test-leave-to">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test test-enter-active test-enter-to">d</div>`,
|
||||
)
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test">d</div>`,
|
||||
)
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
|
||||
test(
|
||||
'appear',
|
||||
async () => {
|
||||
const btnSelector = '.appear > button'
|
||||
const containerSelector = '.appear > div'
|
||||
|
||||
expect(await html('.appear')).toBe(`<button>appear button</button>`)
|
||||
|
||||
await page().evaluate(() => {
|
||||
return (window as any).setAppear()
|
||||
})
|
||||
|
||||
// appear
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test test-appear-from test-appear-active">a</div>` +
|
||||
`<div class="test test-appear-from test-appear-active">b</div>` +
|
||||
`<div class="test test-appear-from test-appear-active">c</div>`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test test-appear-active test-appear-to">a</div>` +
|
||||
`<div class="test test-appear-active test-appear-to">b</div>` +
|
||||
`<div class="test test-appear-active test-appear-to">c</div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>`,
|
||||
)
|
||||
|
||||
// enter
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test test-enter-from test-enter-active">d</div>` +
|
||||
`<div class="test test-enter-from test-enter-active">e</div>`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test test-enter-active test-enter-to">d</div>` +
|
||||
`<div class="test test-enter-active test-enter-to">e</div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test">d</div>` +
|
||||
`<div class="test">e</div>`,
|
||||
)
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
|
||||
test(
|
||||
'move',
|
||||
async () => {
|
||||
const btnSelector = '.move > button'
|
||||
const containerSelector = '.move > div'
|
||||
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>`,
|
||||
)
|
||||
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="test group-enter-from group-enter-active">d</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test group-move" style="">a</div>` +
|
||||
`<div class="test group-leave-from group-leave-active group-move" style="">c</div>`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test group-enter-active group-enter-to">d</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test group-move" style="">a</div>` +
|
||||
`<div class="test group-leave-active group-move group-leave-to" style="">c</div>`,
|
||||
)
|
||||
await transitionFinish(duration * 2)
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">d</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test" style="">a</div>`,
|
||||
)
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
|
||||
test('dynamic name', async () => {
|
||||
const btnSelector = '.dynamic-name button.toggleBtn'
|
||||
const btnChangeName = '.dynamic-name button.changeNameBtn'
|
||||
const containerSelector = '.dynamic-name > div'
|
||||
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div>a</div>` + `<div>b</div>` + `<div>c</div>`,
|
||||
)
|
||||
|
||||
// invalid name
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(`<div>b</div>` + `<div>c</div>` + `<div>a</div>`)
|
||||
|
||||
// change name
|
||||
expect(
|
||||
(await transitionStart(btnChangeName, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="group-move" style="">a</div>` +
|
||||
`<div class="group-move" style="">b</div>` +
|
||||
`<div class="group-move" style="">c</div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="" style="">a</div>` +
|
||||
`<div class="" style="">b</div>` +
|
||||
`<div class="" style="">c</div>`,
|
||||
)
|
||||
})
|
||||
|
||||
test('events', async () => {
|
||||
const btnSelector = '.events > button'
|
||||
const containerSelector = '.events > div'
|
||||
|
||||
expect(await html('.events')).toBe(`<button>events button</button>`)
|
||||
|
||||
await page().evaluate(() => {
|
||||
return (window as any).setAppear()
|
||||
})
|
||||
|
||||
// appear
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test test-appear-from test-appear-active">a</div>` +
|
||||
`<div class="test test-appear-from test-appear-active">b</div>` +
|
||||
`<div class="test test-appear-from test-appear-active">c</div>`,
|
||||
)
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test test-appear-active test-appear-to">a</div>` +
|
||||
`<div class="test test-appear-active test-appear-to">b</div>` +
|
||||
`<div class="test test-appear-active test-appear-to">c</div>`,
|
||||
)
|
||||
|
||||
let calls = await page().evaluate(() => {
|
||||
return (window as any).getCalls()
|
||||
})
|
||||
expect(calls).toContain('beforeAppear')
|
||||
expect(calls).toContain('onAppear')
|
||||
expect(calls).not.toContain('afterAppear')
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>`,
|
||||
)
|
||||
|
||||
expect(
|
||||
await page().evaluate(() => {
|
||||
return (window as any).getCalls()
|
||||
}),
|
||||
).toContain('afterAppear')
|
||||
|
||||
// enter + leave
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="test test-leave-from test-leave-active">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test test-enter-from test-enter-active">d</div>`,
|
||||
)
|
||||
|
||||
calls = await page().evaluate(() => {
|
||||
return (window as any).getCalls()
|
||||
})
|
||||
expect(calls).toContain('beforeLeave')
|
||||
expect(calls).toContain('onLeave')
|
||||
expect(calls).not.toContain('afterLeave')
|
||||
expect(calls).toContain('beforeEnter')
|
||||
expect(calls).toContain('onEnter')
|
||||
expect(calls).not.toContain('afterEnter')
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test test-leave-active test-leave-to">a</div>` +
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test test-enter-active test-enter-to">d</div>`,
|
||||
)
|
||||
calls = await page().evaluate(() => {
|
||||
return (window as any).getCalls()
|
||||
})
|
||||
expect(calls).not.toContain('afterLeave')
|
||||
expect(calls).not.toContain('afterEnter')
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test">b</div>` +
|
||||
`<div class="test">c</div>` +
|
||||
`<div class="test">d</div>`,
|
||||
)
|
||||
|
||||
calls = await page().evaluate(() => {
|
||||
return (window as any).getCalls()
|
||||
})
|
||||
expect(calls).toContain('afterLeave')
|
||||
expect(calls).toContain('afterEnter')
|
||||
})
|
||||
|
||||
test('interop: render vdom component', async () => {
|
||||
const btnSelector = '.interop > button'
|
||||
const containerSelector = '.interop > div'
|
||||
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div><div>a</div></div>` +
|
||||
`<div><div>b</div></div>` +
|
||||
`<div><div>c</div></div>`,
|
||||
)
|
||||
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="test-leave-from test-leave-active"><div>a</div></div>` +
|
||||
`<div class="test-move" style=""><div>b</div></div>` +
|
||||
`<div class="test-move" style=""><div>c</div></div>` +
|
||||
`<div class="test-enter-from test-enter-active"><div>d</div></div>`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="test-leave-active test-leave-to"><div>a</div></div>` +
|
||||
`<div class="test-move" style=""><div>b</div></div>` +
|
||||
`<div class="test-move" style=""><div>c</div></div>` +
|
||||
`<div class="test-enter-active test-enter-to"><div>d</div></div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="" style=""><div>b</div></div>` +
|
||||
`<div class="" style=""><div>c</div></div>` +
|
||||
`<div class=""><div>d</div></div>`,
|
||||
)
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load Diff
|
@ -5,12 +5,28 @@ import {
|
|||
} from '../../../packages/vue/__tests__/e2e/e2eUtils'
|
||||
import connect from 'connect'
|
||||
import sirv from 'sirv'
|
||||
const {
|
||||
page,
|
||||
click,
|
||||
text,
|
||||
enterValue,
|
||||
html,
|
||||
transitionStart,
|
||||
waitForElement,
|
||||
nextFrame,
|
||||
timeout,
|
||||
} = setupPuppeteer()
|
||||
|
||||
const duration = process.env.CI ? 200 : 50
|
||||
const buffer = process.env.CI ? 50 : 20
|
||||
const transitionFinish = (time = duration) => timeout(time + buffer)
|
||||
|
||||
import { ports } from '../utils'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
describe('vdom / vapor interop', () => {
|
||||
const { page, click, text, enterValue } = setupPuppeteer()
|
||||
|
||||
let server: any
|
||||
const port = '8193'
|
||||
const port = ports.vdomInterop
|
||||
beforeAll(() => {
|
||||
server = connect()
|
||||
.use(sirv(path.resolve(import.meta.dirname, '../dist')))
|
||||
|
@ -18,16 +34,25 @@ describe('vdom / vapor interop', () => {
|
|||
process.on('SIGTERM', () => server && server.close())
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const baseUrl = `http://localhost:${port}/interop/`
|
||||
await page().goto(baseUrl)
|
||||
await page().waitForSelector('#app')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const baseUrl = `http://localhost:${port}/interop/`
|
||||
await page().goto(baseUrl)
|
||||
await page().waitForSelector('#app')
|
||||
})
|
||||
|
||||
test(
|
||||
'should work',
|
||||
async () => {
|
||||
const baseUrl = `http://localhost:${port}/interop/`
|
||||
await page().goto(baseUrl)
|
||||
|
||||
expect(await text('.vapor > h2')).toContain('Vapor component in VDOM')
|
||||
|
||||
expect(await text('.vapor-prop')).toContain('hello')
|
||||
|
@ -81,4 +106,205 @@ describe('vdom / vapor interop', () => {
|
|||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
|
||||
describe('vdom transition', () => {
|
||||
test(
|
||||
'render vapor component',
|
||||
async () => {
|
||||
const btnSelector = '.trans-vapor > button'
|
||||
const containerSelector = '.trans-vapor > div'
|
||||
|
||||
expect(await html(containerSelector)).toBe(`<div>vapor compA</div>`)
|
||||
|
||||
// comp leave
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="v-leave-from v-leave-active">vapor compA</div><!---->`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="v-leave-active v-leave-to">vapor compA</div><!---->`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(`<!---->`)
|
||||
|
||||
// comp enter
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(`<div class="v-enter-from v-enter-active">vapor compA</div>`)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="v-enter-active v-enter-to">vapor compA</div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="">vapor compA</div>`,
|
||||
)
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
|
||||
test(
|
||||
'switch between vdom/vapor component (out-in mode)',
|
||||
async () => {
|
||||
const btnSelector = '.trans-vdom-vapor-out-in > button'
|
||||
const containerSelector = '.trans-vdom-vapor-out-in > div'
|
||||
const childSelector = `${containerSelector} > div`
|
||||
|
||||
expect(await html(containerSelector)).toBe(`<div>vdom comp</div>`)
|
||||
|
||||
// switch to vapor comp
|
||||
// vdom comp leave
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="fade-leave-from fade-leave-active">vdom comp</div><!---->`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="fade-leave-active fade-leave-to">vdom comp</div><!---->`,
|
||||
)
|
||||
|
||||
// vapor comp enter
|
||||
await waitForElement(childSelector, 'vapor compA', [
|
||||
'fade-enter-from',
|
||||
'fade-enter-active',
|
||||
])
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="fade-enter-active fade-enter-to">vapor compA</div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="">vapor compA</div>`,
|
||||
)
|
||||
|
||||
// switch to vdom comp
|
||||
// vapor comp leave
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div class="fade-leave-from fade-leave-active">vapor compA</div><!---->`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="fade-leave-active fade-leave-to">vapor compA</div><!---->`,
|
||||
)
|
||||
|
||||
// vdom comp enter
|
||||
await waitForElement(childSelector, 'vdom comp', [
|
||||
'fade-enter-from',
|
||||
'fade-enter-active',
|
||||
])
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="fade-enter-active fade-enter-to">vdom comp</div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div class="">vdom comp</div>`,
|
||||
)
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
})
|
||||
|
||||
describe('vdom transition-group', () => {
|
||||
test(
|
||||
'render vapor component',
|
||||
async () => {
|
||||
const btnSelector = '.trans-group-vapor > button'
|
||||
const containerSelector = '.trans-group-vapor > div'
|
||||
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div><div>a</div></div>` +
|
||||
`<div><div>b</div></div>` +
|
||||
`<div><div>c</div></div>`,
|
||||
)
|
||||
|
||||
// insert
|
||||
expect(
|
||||
(await transitionStart(btnSelector, containerSelector)).innerHTML,
|
||||
).toBe(
|
||||
`<div><div>a</div></div>` +
|
||||
`<div><div>b</div></div>` +
|
||||
`<div><div>c</div></div>` +
|
||||
`<div class="test-enter-from test-enter-active"><div>d</div></div>` +
|
||||
`<div class="test-enter-from test-enter-active"><div>e</div></div>`,
|
||||
)
|
||||
|
||||
await nextFrame()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div><div>a</div></div>` +
|
||||
`<div><div>b</div></div>` +
|
||||
`<div><div>c</div></div>` +
|
||||
`<div class="test-enter-active test-enter-to"><div>d</div></div>` +
|
||||
`<div class="test-enter-active test-enter-to"><div>e</div></div>`,
|
||||
)
|
||||
|
||||
await transitionFinish()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<div><div>a</div></div>` +
|
||||
`<div><div>b</div></div>` +
|
||||
`<div><div>c</div></div>` +
|
||||
`<div class=""><div>d</div></div>` +
|
||||
`<div class=""><div>e</div></div>`,
|
||||
)
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
describe('teleport', () => {
|
||||
const testSelector = '.teleport'
|
||||
test('render vapor component', async () => {
|
||||
const targetSelector = `${testSelector} .teleport-target`
|
||||
const containerSelector = `${testSelector} .render-vapor-comp`
|
||||
const buttonSelector = `${containerSelector} button`
|
||||
|
||||
// teleport is disabled by default
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<button>toggle</button><div>vapor comp</div>`,
|
||||
)
|
||||
expect(await html(targetSelector)).toBe('')
|
||||
|
||||
// disabled -> enabled
|
||||
await click(buttonSelector)
|
||||
await nextTick()
|
||||
expect(await html(containerSelector)).toBe(`<button>toggle</button>`)
|
||||
expect(await html(targetSelector)).toBe('<div>vapor comp</div>')
|
||||
|
||||
// enabled -> disabled
|
||||
await click(buttonSelector)
|
||||
await nextTick()
|
||||
expect(await html(containerSelector)).toBe(
|
||||
`<button>toggle</button><div>vapor comp</div>`,
|
||||
)
|
||||
expect(await html(targetSelector)).toBe('')
|
||||
})
|
||||
})
|
||||
describe('async component', () => {
|
||||
const container = '.async-component-interop'
|
||||
test(
|
||||
'with-vdom-inner-component',
|
||||
async () => {
|
||||
const testContainer = `${container} .with-vdom-component`
|
||||
expect(await html(testContainer)).toBe('<span>loading...</span>')
|
||||
|
||||
await timeout(duration)
|
||||
expect(await html(testContainer)).toBe('<div>foo</div>')
|
||||
},
|
||||
E2E_TIMEOUT,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,2 +1,12 @@
|
|||
<a href="/interop/">VDOM / Vapor interop</a>
|
||||
<a href="/todomvc/">Vapor TodoMVC</a>
|
||||
<a href="/transition/">Vapor Transition</a>
|
||||
<a href="/transition-group/">Vapor TransitionGroup</a>
|
||||
<a href="/teleport/">Vapor Teleport</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
display: block;
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,39 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import VaporComp from './VaporComp.vue'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import VaporComp from './components/VaporComp.vue'
|
||||
import VaporCompA from '../transition/components/VaporCompA.vue'
|
||||
import VdomComp from '../transition/components/VdomComp.vue'
|
||||
import VaporSlot from '../transition/components/VaporSlot.vue'
|
||||
import { defineVaporAsyncComponent, h } from 'vue'
|
||||
import VdomFoo from './components/VdomFoo.vue'
|
||||
|
||||
const msg = ref('hello')
|
||||
const passSlot = ref(true)
|
||||
|
||||
const toggleVapor = ref(true)
|
||||
const interopComponent = shallowRef(VdomComp)
|
||||
function toggleInteropComponent() {
|
||||
interopComponent.value =
|
||||
interopComponent.value === VaporCompA ? VdomComp : VaporCompA
|
||||
}
|
||||
|
||||
const items = ref(['a', 'b', 'c'])
|
||||
const enterClick = () => items.value.push('d', 'e')
|
||||
import SimpleVaporComp from './components/SimpleVaporComp.vue'
|
||||
|
||||
const disabled = ref(true)
|
||||
const duration = typeof process !== 'undefined' && process.env.CI ? 200 : 50
|
||||
|
||||
const AsyncVDomFoo = defineVaporAsyncComponent({
|
||||
loader: () => {
|
||||
return new Promise(r => {
|
||||
setTimeout(() => {
|
||||
r(VdomFoo as any)
|
||||
}, duration)
|
||||
})
|
||||
},
|
||||
loadingComponent: () => h('span', 'loading...'),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -19,4 +49,59 @@ const passSlot = ref(true)
|
|||
|
||||
<template #test v-if="passSlot">A test slot</template>
|
||||
</VaporComp>
|
||||
|
||||
<!-- transition interop -->
|
||||
<div>
|
||||
<div class="trans-vapor">
|
||||
<button @click="toggleVapor = !toggleVapor">
|
||||
toggle vapor component
|
||||
</button>
|
||||
<div>
|
||||
<Transition>
|
||||
<VaporCompA v-if="toggleVapor" />
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trans-vdom-vapor-out-in">
|
||||
<button @click="toggleInteropComponent">
|
||||
switch between vdom/vapor component out-in mode
|
||||
</button>
|
||||
<div>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<component :is="interopComponent"></component>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- transition-group interop -->
|
||||
<div>
|
||||
<div class="trans-group-vapor">
|
||||
<button @click="enterClick">insert items</button>
|
||||
<div>
|
||||
<transition-group name="test">
|
||||
<VaporSlot v-for="item in items" :key="item">
|
||||
<div>{{ item }}</div>
|
||||
</VaporSlot>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- teleport -->
|
||||
<div class="teleport">
|
||||
<div class="teleport-target"></div>
|
||||
<div class="render-vapor-comp">
|
||||
<button @click="disabled = !disabled">toggle</button>
|
||||
<Teleport to=".teleport-target" defer :disabled="disabled">
|
||||
<SimpleVaporComp />
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
<!-- teleport end-->
|
||||
<!-- async component -->
|
||||
<div class="async-component-interop">
|
||||
<div class="with-vdom-component">
|
||||
<AsyncVDomFoo />
|
||||
</div>
|
||||
</div>
|
||||
<!-- async component end -->
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<script setup vapor lang="ts">
|
||||
const msg = 'vapor comp'
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ msg }}</div>
|
||||
</template>
|
|
@ -27,7 +27,8 @@ const slotProp = ref('slot prop')
|
|||
change slot prop
|
||||
</button>
|
||||
<div class="vdom-slot-in-vapor-default">
|
||||
#default: <slot :foo="slotProp" />
|
||||
#default:
|
||||
<slot :foo="slotProp" />
|
||||
</div>
|
||||
<div class="vdom-slot-in-vapor-test">
|
||||
#test: <slot name="test">fallback content</slot>
|
||||
|
@ -40,7 +41,7 @@ const slotProp = ref('slot prop')
|
|||
>
|
||||
Toggle default slot to vdom
|
||||
</button>
|
||||
<VdomComp :msg="msg">
|
||||
<VdomComp :msg="msg" class="foo">
|
||||
<template #default="{ foo }" v-if="passSlot">
|
||||
<div>slot prop: {{ foo }}</div>
|
||||
<div>component prop: {{ msg }}</div>
|
|
@ -0,0 +1,5 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>foo</div>
|
||||
</template>
|
|
@ -1,4 +1,5 @@
|
|||
import { createApp, vaporInteropPlugin } from 'vue'
|
||||
import App from './App.vue'
|
||||
import '../transition/style.css'
|
||||
|
||||
createApp(App).use(vaporInteropPlugin).mount('#app')
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<script setup vapor>
|
||||
import { ref, Teleport } from 'vue'
|
||||
import VdomComp from './components/VdomComp.vue'
|
||||
const disabled = ref(true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="target"></div>
|
||||
<div class="interop-render-vdom-comp">
|
||||
<button @click="disabled = !disabled">toggle</button>
|
||||
<div>
|
||||
<Teleport to=".target" defer :disabled>
|
||||
<VdomComp />
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
const msg = 'vdom comp'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
</template>
|
|
@ -0,0 +1,2 @@
|
|||
<script type="module" src="./main.ts"></script>
|
||||
<div id="app"></div>
|
|
@ -0,0 +1,5 @@
|
|||
import { createVaporApp, vaporInteropPlugin } from 'vue'
|
||||
import App from './App.vue'
|
||||
import 'todomvc-app-css/index.css'
|
||||
|
||||
createVaporApp(App).use(vaporInteropPlugin).mount('#app')
|
|
@ -0,0 +1,145 @@
|
|||
<script setup vapor>
|
||||
import { ref } from 'vue'
|
||||
import VdomComp from './components/VdomComp.vue'
|
||||
|
||||
const items = ref(['a', 'b', 'c'])
|
||||
const enterClick = () => items.value.push('d', 'e')
|
||||
const leaveClick = () => (items.value = ['b'])
|
||||
const enterLeaveClick = () => (items.value = ['b', 'c', 'd'])
|
||||
const appear = ref(false)
|
||||
window.setAppear = () => (appear.value = true)
|
||||
const moveClick = () => (items.value = ['d', 'b', 'a'])
|
||||
|
||||
const name = ref('invalid')
|
||||
const dynamicClick = () => (items.value = ['b', 'c', 'a'])
|
||||
const changeName = () => {
|
||||
name.value = 'group'
|
||||
items.value = ['a', 'b', 'c']
|
||||
}
|
||||
|
||||
let calls = []
|
||||
window.getCalls = () => {
|
||||
const ret = calls.slice()
|
||||
calls = []
|
||||
return ret
|
||||
}
|
||||
const eventsClick = () => (items.value = ['b', 'c', 'd'])
|
||||
|
||||
const interopClick = () => (items.value = ['b', 'c', 'd'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="transition-group-container">
|
||||
<div class="enter">
|
||||
<button @click="enterClick">enter button</button>
|
||||
<div>
|
||||
<transition-group name="test">
|
||||
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="leave">
|
||||
<button @click="leaveClick">leave button</button>
|
||||
<div>
|
||||
<transition-group name="test">
|
||||
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="enter-leave">
|
||||
<button @click="enterLeaveClick">enter-leave button</button>
|
||||
<div>
|
||||
<transition-group name="test">
|
||||
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appear">
|
||||
<button @click="enterClick">appear button</button>
|
||||
<div v-if="appear">
|
||||
<transition-group
|
||||
appear
|
||||
appear-from-class="test-appear-from"
|
||||
appear-to-class="test-appear-to"
|
||||
appear-active-class="test-appear-active"
|
||||
name="test"
|
||||
>
|
||||
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="move">
|
||||
<button @click="moveClick">move button</button>
|
||||
<div>
|
||||
<transition-group name="group">
|
||||
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dynamic-name">
|
||||
<button class="toggleBtn" @click="dynamicClick">dynamic button</button>
|
||||
<button class="changeNameBtn" @click="changeName">change name</button>
|
||||
<div>
|
||||
<transition-group :name="name">
|
||||
<div v-for="item in items" :key="item">{{ item }}</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="events">
|
||||
<button @click="eventsClick">events button</button>
|
||||
<div v-if="appear">
|
||||
<transition-group
|
||||
name="test"
|
||||
appear
|
||||
appear-from-class="test-appear-from"
|
||||
appear-to-class="test-appear-to"
|
||||
appear-active-class="test-appear-active"
|
||||
@beforeEnter="() => calls.push('beforeEnter')"
|
||||
@enter="() => calls.push('onEnter')"
|
||||
@afterEnter="() => calls.push('afterEnter')"
|
||||
@beforeLeave="() => calls.push('beforeLeave')"
|
||||
@leave="() => calls.push('onLeave')"
|
||||
@afterLeave="() => calls.push('afterLeave')"
|
||||
@beforeAppear="() => calls.push('beforeAppear')"
|
||||
@appear="() => calls.push('onAppear')"
|
||||
@afterAppear="() => calls.push('afterAppear')"
|
||||
>
|
||||
<div v-for="item in items" :key="item" class="test">{{ item }}</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interop">
|
||||
<button @click="interopClick">interop button</button>
|
||||
<div>
|
||||
<transition-group name="test">
|
||||
<VdomComp v-for="item in items" :key="item">
|
||||
<div>{{ item }}</div>
|
||||
</VdomComp>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.transition-group-container > div {
|
||||
padding: 15px;
|
||||
border: 1px solid #f7f7f7;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.test-move,
|
||||
.test-enter-active,
|
||||
.test-leave-active {
|
||||
transition: all 50ms cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
|
||||
.test-enter-from,
|
||||
.test-leave-to {
|
||||
opacity: 0;
|
||||
transform: scaleY(0.01) translate(30px, 0);
|
||||
}
|
||||
|
||||
.test-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
<script vapor>
|
||||
const msg = 'vapor comp'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<script setup>
|
||||
const msg = 'vdom comp'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,2 @@
|
|||
<script type="module" src="./main.ts"></script>
|
||||
<div id="app"></div>
|
|
@ -0,0 +1,5 @@
|
|||
import { createVaporApp, vaporInteropPlugin } from 'vue'
|
||||
import App from './App.vue'
|
||||
import '../../../packages/vue/__tests__/e2e/style.css'
|
||||
|
||||
createVaporApp(App).use(vaporInteropPlugin).mount('#app')
|
|
@ -0,0 +1,528 @@
|
|||
<script vapor>
|
||||
import {
|
||||
createComponent,
|
||||
defineVaporComponent,
|
||||
ref,
|
||||
shallowRef,
|
||||
VaporTransition,
|
||||
createIf,
|
||||
template,
|
||||
} from 'vue'
|
||||
const show = ref(true)
|
||||
const toggle = ref(true)
|
||||
const count = ref(0)
|
||||
|
||||
const timeout = (fn, time) => setTimeout(fn, time)
|
||||
const duration = typeof process !== 'undefined' && process.env.CI ? 200 : 50
|
||||
|
||||
let calls = {
|
||||
basic: [],
|
||||
withoutAppear: [],
|
||||
withArgs: [],
|
||||
enterCancel: [],
|
||||
withAppear: [],
|
||||
cssFalse: [],
|
||||
ifInOut: [],
|
||||
|
||||
show: [],
|
||||
showLeaveCancel: [],
|
||||
showAppear: [],
|
||||
notEnter: [],
|
||||
}
|
||||
window.getCalls = key => calls[key]
|
||||
window.resetCalls = key => (calls[key] = [])
|
||||
|
||||
import VaporCompA from './components/VaporCompA.vue'
|
||||
import VaporCompB from './components/VaporCompB.vue'
|
||||
const activeComponent = shallowRef(VaporCompB)
|
||||
function toggleComponent() {
|
||||
activeComponent.value =
|
||||
activeComponent.value === VaporCompA ? VaporCompB : VaporCompA
|
||||
}
|
||||
|
||||
const toggleVdom = ref(true)
|
||||
import VDomComp from './components/VdomComp.vue'
|
||||
|
||||
const interopComponent = shallowRef(VDomComp)
|
||||
function toggleInteropComponent() {
|
||||
interopComponent.value =
|
||||
interopComponent.value === VaporCompA ? VDomComp : VaporCompA
|
||||
}
|
||||
|
||||
const name = ref('test')
|
||||
const MyTransition = defineVaporComponent((props, { slots }) => {
|
||||
return createComponent(VaporTransition, { name: () => 'test' }, slots)
|
||||
})
|
||||
|
||||
const MyTransitionFallthroughAttr = defineVaporComponent((props, { slots }) => {
|
||||
return createComponent(
|
||||
VaporTransition,
|
||||
{ foo: () => 1, name: () => 'test' },
|
||||
slots,
|
||||
)
|
||||
})
|
||||
|
||||
const One = defineVaporComponent({
|
||||
setup() {
|
||||
return createIf(
|
||||
() => false,
|
||||
() => template('<div>one</div>', true)(),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const Two = defineVaporComponent({
|
||||
setup() {
|
||||
return template('<div>two</div>', true)()
|
||||
},
|
||||
})
|
||||
const view = shallowRef(One)
|
||||
function changeView() {
|
||||
view.value = view.value === One ? Two : One
|
||||
}
|
||||
|
||||
const SimpleOne = defineVaporComponent({
|
||||
setup() {
|
||||
return template('<div>one</div>', true)()
|
||||
},
|
||||
})
|
||||
const viewInOut = shallowRef(SimpleOne)
|
||||
function changeViewInOut() {
|
||||
viewInOut.value = viewInOut.value === SimpleOne ? Two : SimpleOne
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="transition-container">
|
||||
<!-- work with vif -->
|
||||
<div class="if-basic">
|
||||
<div>
|
||||
<transition>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">basic toggle</button>
|
||||
</div>
|
||||
<div class="if-named">
|
||||
<div>
|
||||
<transition name="test">
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-custom-classes">
|
||||
<div>
|
||||
<transition
|
||||
enter-from-class="hello-from"
|
||||
enter-active-class="hello-active"
|
||||
enter-to-class="hello-to"
|
||||
leave-from-class="bye-from"
|
||||
leave-active-class="bye-active"
|
||||
leave-to-class="bye-to"
|
||||
>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-dynamic-name">
|
||||
<div>
|
||||
<transition :name="name">
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button class="toggle" @click="toggle = !toggle">button</button>
|
||||
<button class="change" @click="name = 'changed'">{{ name }}</button>
|
||||
</div>
|
||||
<div class="if-events-without-appear">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
@before-enter="() => calls.withoutAppear.push('beforeEnter')"
|
||||
@enter="() => calls.withoutAppear.push('onEnter')"
|
||||
@after-enter="() => calls.withoutAppear.push('afterEnter')"
|
||||
@beforeLeave="() => calls.withoutAppear.push('beforeLeave')"
|
||||
@leave="() => calls.withoutAppear.push('onLeave')"
|
||||
@afterLeave="() => calls.withoutAppear.push('afterLeave')"
|
||||
>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-events-with-args">
|
||||
<div>
|
||||
<transition
|
||||
:css="false"
|
||||
name="test"
|
||||
@before-enter="
|
||||
el => {
|
||||
calls.withArgs.push('beforeEnter')
|
||||
el.classList.add('before-enter')
|
||||
}
|
||||
"
|
||||
@enter="
|
||||
(el, done) => {
|
||||
calls.withArgs.push('onEnter')
|
||||
el.classList.add('enter')
|
||||
timeout(done, 200)
|
||||
}
|
||||
"
|
||||
@after-enter="
|
||||
el => {
|
||||
calls.withArgs.push('afterEnter')
|
||||
el.classList.add('after-enter')
|
||||
}
|
||||
"
|
||||
@before-leave="
|
||||
el => {
|
||||
calls.withArgs.push('beforeLeave')
|
||||
el.classList.add('before-leave')
|
||||
}
|
||||
"
|
||||
@leave="
|
||||
(el, done) => {
|
||||
calls.withArgs.push('onLeave')
|
||||
el.classList.add('leave')
|
||||
timeout(done, 200)
|
||||
}
|
||||
"
|
||||
@after-leave="
|
||||
() => {
|
||||
calls.withArgs.push('afterLeave')
|
||||
}
|
||||
"
|
||||
>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-enter-cancelled">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
@enter-cancelled="
|
||||
() => {
|
||||
calls.enterCancel.push('enterCancelled')
|
||||
}
|
||||
"
|
||||
>
|
||||
<div v-if="!toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">cancelled</button>
|
||||
</div>
|
||||
<div class="if-appear">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
appear
|
||||
appear-from-class="test-appear-from"
|
||||
appear-to-class="test-appear-to"
|
||||
appear-active-class="test-appear-active"
|
||||
>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-events-with-appear">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
appear
|
||||
appear-from-class="test-appear-from"
|
||||
appear-to-class="test-appear-to"
|
||||
appear-active-class="test-appear-active"
|
||||
@beforeEnter="() => calls.withAppear.push('beforeEnter')"
|
||||
@enter="() => calls.withAppear.push('onEnter')"
|
||||
@afterEnter="() => calls.withAppear.push('afterEnter')"
|
||||
@beforeLeave="() => calls.withAppear.push('beforeLeave')"
|
||||
@leave="() => calls.withAppear.push('onLeave')"
|
||||
@afterLeave="() => calls.withAppear.push('afterLeave')"
|
||||
@beforeAppear="() => calls.withAppear.push('beforeAppear')"
|
||||
@appear="() => calls.withAppear.push('onAppear')"
|
||||
@afterAppear="() => calls.withAppear.push('afterAppear')"
|
||||
>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-css-false">
|
||||
<div>
|
||||
<transition
|
||||
:css="false"
|
||||
name="test"
|
||||
@beforeEnter="() => calls.cssFalse.push('beforeEnter')"
|
||||
@enter="() => calls.cssFalse.push('onEnter')"
|
||||
@afterEnter="() => calls.cssFalse.push('afterEnter')"
|
||||
@beforeLeave="() => calls.cssFalse.push('beforeLeave')"
|
||||
@leave="() => calls.cssFalse.push('onLeave')"
|
||||
@afterLeave="() => calls.cssFalse.push('afterLeave')"
|
||||
>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle"></button>
|
||||
</div>
|
||||
<div class="if-no-trans">
|
||||
<div>
|
||||
<transition name="noop">
|
||||
<div v-if="toggle">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-ani">
|
||||
<div>
|
||||
<transition name="test-anim">
|
||||
<div v-if="toggle">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-ani-explicit-type">
|
||||
<div>
|
||||
<transition name="test-anim-long" type="animation">
|
||||
<div v-if="toggle">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-high-order">
|
||||
<div>
|
||||
<MyTransition>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</MyTransition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="if-empty-root">
|
||||
<div>
|
||||
<transition name="test">
|
||||
<component class="test" :is="view"></component>
|
||||
</transition>
|
||||
</div>
|
||||
<button class="toggle" @click="toggle = !toggle">button</button>
|
||||
<button class="change" @click="changeView">changeView button</button>
|
||||
</div>
|
||||
<div class="if-at-component-root-level">
|
||||
<div>
|
||||
<transition name="test" mode="out-in">
|
||||
<component class="test" :is="view"></component>
|
||||
</transition>
|
||||
</div>
|
||||
<button class="toggle" @click="toggle = !toggle">button</button>
|
||||
<button class="change" @click="changeView">changeView button</button>
|
||||
</div>
|
||||
<div class="if-fallthrough-attr">
|
||||
<div>
|
||||
<MyTransitionFallthroughAttr>
|
||||
<div v-if="toggle">content</div>
|
||||
</MyTransitionFallthroughAttr>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button fallthrough</button>
|
||||
</div>
|
||||
<div class="if-fallthrough-attr-in-out">
|
||||
<div>
|
||||
<transition
|
||||
foo="1"
|
||||
name="test"
|
||||
mode="in-out"
|
||||
@beforeEnter="() => calls.ifInOut.push('beforeEnter')"
|
||||
@enter="() => calls.ifInOut.push('onEnter')"
|
||||
@afterEnter="() => calls.ifInOut.push('afterEnter')"
|
||||
@beforeLeave="() => calls.ifInOut.push('beforeLeave')"
|
||||
@leave="() => calls.ifInOut.push('onLeave')"
|
||||
@afterLeave="() => calls.ifInOut.push('afterLeave')"
|
||||
>
|
||||
<component :is="viewInOut"></component>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="changeViewInOut">button</button>
|
||||
</div>
|
||||
<!-- work with vif end -->
|
||||
|
||||
<!-- work with vshow -->
|
||||
<div class="show-named">
|
||||
<div>
|
||||
<transition name="test">
|
||||
<div v-show="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="show-events">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
@beforeEnter="() => calls.show.push('beforeEnter')"
|
||||
@enter="() => calls.show.push('onEnter')"
|
||||
@afterEnter="() => calls.show.push('afterEnter')"
|
||||
@beforeLeave="() => calls.show.push('beforeLeave')"
|
||||
@leave="() => calls.show.push('onLeave')"
|
||||
@afterLeave="() => calls.show.push('afterLeave')"
|
||||
>
|
||||
<div v-show="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="show-leave-cancelled">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
@leave-cancelled="() => calls.showLeaveCancel.push('leaveCancelled')"
|
||||
>
|
||||
<div v-show="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">leave cancelled</button>
|
||||
</div>
|
||||
<div class="show-appear">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
appear
|
||||
appear-from-class="test-appear-from"
|
||||
appear-to-class="test-appear-to"
|
||||
appear-active-class="test-appear-active"
|
||||
@beforeEnter="() => calls.showAppear.push('beforeEnter')"
|
||||
@enter="() => calls.showAppear.push('onEnter')"
|
||||
@afterEnter="() => calls.showAppear.push('afterEnter')"
|
||||
>
|
||||
<div v-show="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="show-appear-not-enter">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
appear
|
||||
@beforeEnter="() => calls.notEnter.push('beforeEnter')"
|
||||
@enter="() => calls.notEnter.push('onEnter')"
|
||||
@afterEnter="() => calls.notEnter.push('afterEnter')"
|
||||
>
|
||||
<div v-show="!toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<!-- work with vshow end -->
|
||||
|
||||
<!-- explicit durations -->
|
||||
<div class="duration-single-value">
|
||||
<div>
|
||||
<transition name="test" :duration="duration * 2">
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="duration-enter">
|
||||
<div>
|
||||
<transition name="test" :duration="{ enter: duration * 2 }">
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="duration-leave">
|
||||
<div>
|
||||
<transition name="test" :duration="{ leave: duration * 2 }">
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<div class="duration-enter-leave">
|
||||
<div>
|
||||
<transition
|
||||
name="test"
|
||||
:duration="{ enter: duration * 4, leave: duration * 2 }"
|
||||
>
|
||||
<div v-if="toggle" class="test">content</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="toggle = !toggle">button</button>
|
||||
</div>
|
||||
<!-- explicit durations end -->
|
||||
|
||||
<!-- keyed fragment -->
|
||||
<div class="keyed">
|
||||
<button @click="count++">inc</button>
|
||||
<Transition>
|
||||
<h1 style="position: absolute" :key="count">{{ count }}</h1>
|
||||
</Transition>
|
||||
</div>
|
||||
<!-- keyed fragment end -->
|
||||
|
||||
<!-- mode -->
|
||||
<div class="out-in">
|
||||
<button @click="toggleComponent">toggle out-in</button>
|
||||
<div>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<component :is="activeComponent"></component>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="in-out">
|
||||
<button @click="toggleComponent">toggle in-out</button>
|
||||
<div>
|
||||
<Transition name="fade" mode="in-out">
|
||||
<component :is="activeComponent"></component>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- mode end -->
|
||||
|
||||
<!-- vdom interop -->
|
||||
<div class="vdom">
|
||||
<button @click="toggleVdom = !toggleVdom">toggle vdom component</button>
|
||||
<div>
|
||||
<Transition>
|
||||
<VDomComp v-if="toggleVdom" />
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vdom-vapor-out-in">
|
||||
<button @click="toggleInteropComponent">
|
||||
switch between vdom/vapor component out-in mode
|
||||
</button>
|
||||
<div>
|
||||
<Transition name="fade" mode="out-in">
|
||||
<component :is="interopComponent"></component>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vdom-vapor-in-out">
|
||||
<button @click="toggleVdom = !toggleVdom">
|
||||
switch between vdom/vapor component in-out mode
|
||||
</button>
|
||||
<div>
|
||||
<Transition name="fade" mode="in-out">
|
||||
<VaporCompA v-if="toggleVdom" />
|
||||
<VDomComp v-else></VDomComp>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- vdom interop end -->
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.keyed {
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.transition-container > div {
|
||||
padding: 15px;
|
||||
border: 1px solid #f7f7f7;
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,6 @@
|
|||
<script setup vapor lang="ts">
|
||||
const msg = 'vapor compA'
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ msg }}</div>
|
||||
</template>
|
|
@ -0,0 +1,6 @@
|
|||
<script setup vapor lang="ts">
|
||||
const msg = 'vapor compB'
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ msg }}</div>
|
||||
</template>
|
|
@ -0,0 +1,8 @@
|
|||
<script setup vapor lang="ts">
|
||||
const msg = 'vapor'
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
const msg = 'vdom comp'
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ msg }}</div>
|
||||
</template>
|
|
@ -0,0 +1,2 @@
|
|||
<script type="module" src="./main.ts"></script>
|
||||
<div id="app"></div>
|
|
@ -0,0 +1,6 @@
|
|||
import { createVaporApp, vaporInteropPlugin } from 'vue'
|
||||
import App from './App.vue'
|
||||
import '../../../packages/vue/__tests__/e2e/style.css'
|
||||
import './style.css'
|
||||
|
||||
createVaporApp(App).use(vaporInteropPlugin).mount('#app')
|
|
@ -0,0 +1,35 @@
|
|||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 50ms ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 50ms ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.test-move,
|
||||
.test-enter-active,
|
||||
.test-leave-active {
|
||||
transition: all 50ms cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
|
||||
.test-enter-from,
|
||||
.test-leave-to {
|
||||
opacity: 0;
|
||||
transform: scaleY(0.01) translate(30px, 0);
|
||||
}
|
||||
|
||||
.test-leave-active {
|
||||
position: absolute;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// make sure these ports are unique
|
||||
export const ports = {
|
||||
vdomInterop: 8193,
|
||||
todomvc: 8194,
|
||||
transition: 8195,
|
||||
transitionGroup: 8196,
|
||||
teleport: 8197,
|
||||
}
|
|
@ -14,6 +14,12 @@ export default defineConfig({
|
|||
input: {
|
||||
interop: resolve(import.meta.dirname, 'interop/index.html'),
|
||||
todomvc: resolve(import.meta.dirname, 'todomvc/index.html'),
|
||||
teleport: resolve(import.meta.dirname, 'teleport/index.html'),
|
||||
transition: resolve(import.meta.dirname, 'transition/index.html'),
|
||||
transitionGroup: resolve(
|
||||
import.meta.dirname,
|
||||
'transition-group/index.html',
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -76,4 +76,5 @@ export {
|
|||
} from './errors'
|
||||
export { resolveModifiers } from './transforms/vOn'
|
||||
export { isValidHTMLNesting } from './htmlNesting'
|
||||
export { postTransformTransition } from './transforms/Transition'
|
||||
export * from '@vue/compiler-core'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
type CompilerError,
|
||||
type ComponentNode,
|
||||
ElementTypes,
|
||||
type IfBranchNode,
|
||||
|
@ -15,47 +16,55 @@ export const transformTransition: NodeTransform = (node, context) => {
|
|||
) {
|
||||
const component = context.isBuiltInComponent(node.tag)
|
||||
if (component === TRANSITION) {
|
||||
return () => {
|
||||
if (!node.children.length) {
|
||||
return
|
||||
}
|
||||
return postTransformTransition(node, context.onError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// warn multiple transition children
|
||||
if (hasMultipleChildren(node)) {
|
||||
context.onError(
|
||||
createDOMCompilerError(
|
||||
DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN,
|
||||
{
|
||||
start: node.children[0].loc.start,
|
||||
end: node.children[node.children.length - 1].loc.end,
|
||||
source: '',
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
export function postTransformTransition(
|
||||
node: ComponentNode,
|
||||
onError: (error: CompilerError) => void,
|
||||
hasMultipleChildren: (
|
||||
node: ComponentNode,
|
||||
) => boolean = defaultHasMultipleChildren,
|
||||
): () => void {
|
||||
return () => {
|
||||
if (!node.children.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// check if it's s single child w/ v-show
|
||||
// if yes, inject "persisted: true" to the transition props
|
||||
const child = node.children[0]
|
||||
if (child.type === NodeTypes.ELEMENT) {
|
||||
for (const p of child.props) {
|
||||
if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') {
|
||||
node.props.push({
|
||||
type: NodeTypes.ATTRIBUTE,
|
||||
name: 'persisted',
|
||||
nameLoc: node.loc,
|
||||
value: undefined,
|
||||
loc: node.loc,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (hasMultipleChildren(node)) {
|
||||
onError(
|
||||
createDOMCompilerError(DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN, {
|
||||
start: node.children[0].loc.start,
|
||||
end: node.children[node.children.length - 1].loc.end,
|
||||
source: '',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// check if it's s single child w/ v-show
|
||||
// if yes, inject "persisted: true" to the transition props
|
||||
const child = node.children[0]
|
||||
if (child.type === NodeTypes.ELEMENT) {
|
||||
for (const p of child.props) {
|
||||
if (p.type === NodeTypes.DIRECTIVE && p.name === 'show') {
|
||||
node.props.push({
|
||||
type: NodeTypes.ATTRIBUTE,
|
||||
name: 'persisted',
|
||||
nameLoc: node.loc,
|
||||
value: undefined,
|
||||
loc: node.loc,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
|
||||
function defaultHasMultipleChildren(
|
||||
node: ComponentNode | IfBranchNode,
|
||||
): boolean {
|
||||
// #1352 filter out potential comment nodes.
|
||||
const children = (node.children = node.children.filter(
|
||||
c =>
|
||||
|
@ -66,6 +75,7 @@ function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
|
|||
return (
|
||||
children.length !== 1 ||
|
||||
child.type === NodeTypes.FOR ||
|
||||
(child.type === NodeTypes.IF && child.branches.some(hasMultipleChildren))
|
||||
(child.type === NodeTypes.IF &&
|
||||
child.branches.some(defaultHasMultipleChildren))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -984,7 +984,7 @@ export function compileScript(
|
|||
ctx.s.prependLeft(
|
||||
startOffset,
|
||||
`\n${genDefaultAs} /*@__PURE__*/${ctx.helper(
|
||||
vapor ? `defineVaporComponent` : `defineComponent`,
|
||||
vapor && !ssr ? `defineVaporComponent` : `defineComponent`,
|
||||
)}({${def}${runtimeOptions}\n ${
|
||||
hasAwait ? `async ` : ``
|
||||
}setup(${args}) {\n${exposeCall}`,
|
||||
|
|
|
@ -39,6 +39,7 @@ describe('ssr: components', () => {
|
|||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent("foo"), _mergeProps({ prop: "b" }, _attrs), null), _parent)
|
||||
_push(\`<!--dynamic-component-->\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
|
@ -49,6 +50,7 @@ describe('ssr: components', () => {
|
|||
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
_ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: "b" }, _attrs), null), _parent)
|
||||
_push(\`<!--dynamic-component-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -244,7 +246,8 @@ describe('ssr: components', () => {
|
|||
_ssrRenderList(list, (i) => {
|
||||
_push(\`<span\${_scopeId}></span>\`)
|
||||
})
|
||||
_push(\`<!--]--></div>\`)
|
||||
_push(\`<!--]--><!--for--></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -267,7 +270,8 @@ describe('ssr: components', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<span\${_scopeId}></span>\`)
|
||||
})
|
||||
_push(\`<!--]--></div>\`)
|
||||
_push(\`<!--]--><!--for--></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -361,6 +365,7 @@ describe('ssr: components', () => {
|
|||
_push(\`\`)
|
||||
if (false) {
|
||||
_push(\`<div\${_scopeId}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
|
|
@ -396,4 +396,50 @@ describe('ssr: element', () => {
|
|||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic anchor', () => {
|
||||
test('two consecutive components', () => {
|
||||
expect(
|
||||
getCompiledString(`
|
||||
<div>
|
||||
<div/>
|
||||
<Comp1/>
|
||||
<Comp2/>
|
||||
<div/>
|
||||
</div>
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"\`<div><div></div>\`)
|
||||
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
|
||||
_push(\`<!--[[-->\`)
|
||||
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
|
||||
_push(\`<!--]]--><div></div></div>\`"
|
||||
`)
|
||||
})
|
||||
|
||||
test('multiple consecutive components', () => {
|
||||
expect(
|
||||
getCompiledString(`
|
||||
<div>
|
||||
<div/>
|
||||
<Comp1/>
|
||||
<Comp2/>
|
||||
<Comp3/>
|
||||
<Comp4/>
|
||||
<div/>
|
||||
</div>
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"\`<div><div></div>\`)
|
||||
_push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
|
||||
_push(\`<!--[[-->\`)
|
||||
_push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
|
||||
_push(\`<!--]]--><!--[[-->\`)
|
||||
_push(_ssrRenderComponent(_component_Comp3, null, null, _parent))
|
||||
_push(\`<!--]]-->\`)
|
||||
_push(_ssrRenderComponent(_component_Comp4, null, null, _parent))
|
||||
_push(\`<div></div></div>\`"
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('ssr: attrs fallthrough', () => {
|
|||
_push(\`<!--[-->\`)
|
||||
if (true) {
|
||||
_push(\`<div></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ describe('ssr: inject <style vars>', () => {
|
|||
const _cssVars = { style: { color: _ctx.color }}
|
||||
if (_ctx.ok) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!--[--><div\${
|
||||
_ssrRenderAttrs(_cssVars)
|
||||
|
|
|
@ -153,6 +153,7 @@ describe('ssr: <slot>', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (true) {
|
||||
_ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--for--><!--]-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -33,7 +33,7 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`</ul>\`)
|
||||
_push(\`<!--for--></ul>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -52,8 +52,10 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--for-->\`)
|
||||
if (false) {
|
||||
_push(\`<div></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
}
|
||||
_push(\`</ul>\`)
|
||||
}"
|
||||
|
@ -74,7 +76,7 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`</ul>\`)
|
||||
_push(\`<!--for--></ul>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -96,7 +98,7 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`</\${_ctx.someTag}>\`)
|
||||
_push(\`<!--for--></\${_ctx.someTag}>\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -118,11 +120,14 @@ describe('transition-group', () => {
|
|||
_ssrRenderList(10, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--for-->\`)
|
||||
_ssrRenderList(10, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--for-->\`)
|
||||
if (_ctx.ok) {
|
||||
_push(\`<div>ok</div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
}
|
||||
_push(\`<!--]-->\`)
|
||||
}"
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -25,7 +25,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div>foo<span>bar</span></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -51,9 +51,9 @@ describe('ssr: v-for', () => {
|
|||
_ssrInterpolate(j)
|
||||
}</div>\`)
|
||||
})
|
||||
_push(\`<!--]--></div>\`)
|
||||
_push(\`<!--]--><!--for--></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -68,7 +68,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -85,7 +85,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<span>\${_ssrInterpolate(i)}</span>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -107,7 +107,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrInterpolate(i + 1)
|
||||
}</span><!--]-->\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
@ -127,7 +127,7 @@ describe('ssr: v-for', () => {
|
|||
_ssrRenderList(_ctx.list, ({ foo }, index) => {
|
||||
_push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
|
|
@ -8,6 +8,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -23,6 +24,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -38,6 +40,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||
}
|
||||
|
@ -53,8 +56,10 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else if (_ctx.bar) {
|
||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -70,8 +75,10 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else if (_ctx.bar) {
|
||||
_push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
|
||||
}
|
||||
|
@ -82,15 +89,16 @@ describe('ssr: v-if', () => {
|
|||
test('<template v-if> (text)', () => {
|
||||
expect(compile(`<template v-if="foo">hello</template>`).code)
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<!--[-->hello<!--]-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
}"
|
||||
`)
|
||||
"
|
||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<!--[-->hello<!--]-->\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('<template v-if> (single element)', () => {
|
||||
|
@ -102,6 +110,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -118,6 +127,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -137,7 +147,8 @@ describe('ssr: v-if', () => {
|
|||
_ssrRenderList(_ctx.list, (i) => {
|
||||
_push(\`<div></div>\`)
|
||||
})
|
||||
_push(\`<!--]-->\`)
|
||||
_push(\`<!--]--><!--for-->\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
@ -156,6 +167,7 @@ describe('ssr: v-if', () => {
|
|||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||
if (_ctx.foo) {
|
||||
_push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
|
|||
: _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
|
||||
}></option>\`)
|
||||
})
|
||||
_push(\`<!--]--></select></div>\`)
|
||||
_push(\`<!--]--><!--for--></select></div>\`)
|
||||
}"
|
||||
`)
|
||||
|
||||
|
@ -91,6 +91,7 @@ describe('ssr: v-model', () => {
|
|||
? _ssrLooseContain(_ctx.model, _ctx.i)
|
||||
: _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
|
||||
}></option>\`)
|
||||
_push(\`<!--if-->\`)
|
||||
} else {
|
||||
_push(\`<!---->\`)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
type IfStatement,
|
||||
type JSChildNode,
|
||||
NodeTypes,
|
||||
type PlainElementNode,
|
||||
type RootNode,
|
||||
type TemplateChildNode,
|
||||
type TemplateLiteral,
|
||||
|
@ -20,7 +21,12 @@ import {
|
|||
isText,
|
||||
processExpression,
|
||||
} from '@vue/compiler-dom'
|
||||
import { escapeHtml, isString } from '@vue/shared'
|
||||
import {
|
||||
DYNAMIC_END_ANCHOR_LABEL,
|
||||
DYNAMIC_START_ANCHOR_LABEL,
|
||||
escapeHtml,
|
||||
isString,
|
||||
} from '@vue/shared'
|
||||
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
|
||||
import { ssrProcessIf } from './transforms/ssrVIf'
|
||||
import { ssrProcessFor } from './transforms/ssrVFor'
|
||||
|
@ -157,13 +163,33 @@ export function processChildren(
|
|||
asFragment = false,
|
||||
disableNestedFragments = false,
|
||||
disableComment = false,
|
||||
asDynamic = false,
|
||||
): void {
|
||||
if (asDynamic) {
|
||||
context.pushStringPart(`<!--${DYNAMIC_START_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
if (asFragment) {
|
||||
context.pushStringPart(`<!--[-->`)
|
||||
}
|
||||
const { children } = parent
|
||||
|
||||
const { children, type, tagType } = parent as PlainElementNode
|
||||
const inElement =
|
||||
type === NodeTypes.ELEMENT && tagType === ElementTypes.ELEMENT
|
||||
if (inElement) processChildrenDynamicInfo(children)
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
if (inElement && shouldProcessChildAsDynamic(parent, child)) {
|
||||
processChildren(
|
||||
{ children: [child] },
|
||||
context,
|
||||
asFragment,
|
||||
disableNestedFragments,
|
||||
disableComment,
|
||||
true,
|
||||
)
|
||||
continue
|
||||
}
|
||||
switch (child.type) {
|
||||
case NodeTypes.ELEMENT:
|
||||
switch (child.tagType) {
|
||||
|
@ -237,6 +263,9 @@ export function processChildren(
|
|||
if (asFragment) {
|
||||
context.pushStringPart(`<!--]-->`)
|
||||
}
|
||||
if (asDynamic) {
|
||||
context.pushStringPart(`<!--${DYNAMIC_END_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
}
|
||||
|
||||
export function processChildrenAsStatement(
|
||||
|
@ -249,3 +278,147 @@ export function processChildrenAsStatement(
|
|||
processChildren(parent, childContext, asFragment)
|
||||
return createBlockStatement(childContext.body)
|
||||
}
|
||||
|
||||
const isStaticChildNode = (c: TemplateChildNode): boolean =>
|
||||
(c.type === NodeTypes.ELEMENT && c.tagType !== ElementTypes.COMPONENT) ||
|
||||
c.type === NodeTypes.TEXT ||
|
||||
c.type === NodeTypes.COMMENT
|
||||
|
||||
interface DynamicInfo {
|
||||
hasStaticPrevious: boolean
|
||||
hasStaticNext: boolean
|
||||
prevDynamicCount: number
|
||||
nextDynamicCount: number
|
||||
}
|
||||
|
||||
function processChildrenDynamicInfo(
|
||||
children: (TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo })[],
|
||||
): void {
|
||||
const filteredChildren = children.filter(
|
||||
child => !(child.type === NodeTypes.TEXT && !child.content.trim()),
|
||||
)
|
||||
|
||||
for (let i = 0; i < filteredChildren.length; i++) {
|
||||
const child = filteredChildren[i]
|
||||
if (
|
||||
isStaticChildNode(child) ||
|
||||
// fragment has it's own anchor, which can be used to distinguish the boundary
|
||||
isFragmentChild(child)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
child._ssrDynamicInfo = {
|
||||
hasStaticPrevious: false,
|
||||
hasStaticNext: false,
|
||||
prevDynamicCount: 0,
|
||||
nextDynamicCount: 0,
|
||||
}
|
||||
|
||||
const info = child._ssrDynamicInfo
|
||||
|
||||
// Calculate the previous static and dynamic node counts
|
||||
let foundStaticPrev = false
|
||||
let dynamicCountPrev = 0
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
const prevChild = filteredChildren[j]
|
||||
if (isStaticChildNode(prevChild)) {
|
||||
foundStaticPrev = true
|
||||
break
|
||||
}
|
||||
// if the previous child has dynamic info, use it
|
||||
else if (prevChild._ssrDynamicInfo) {
|
||||
foundStaticPrev = prevChild._ssrDynamicInfo.hasStaticPrevious
|
||||
dynamicCountPrev = prevChild._ssrDynamicInfo.prevDynamicCount + 1
|
||||
break
|
||||
}
|
||||
dynamicCountPrev++
|
||||
}
|
||||
info.hasStaticPrevious = foundStaticPrev
|
||||
info.prevDynamicCount = dynamicCountPrev
|
||||
|
||||
// Calculate the number of static and dynamic nodes afterwards
|
||||
let foundStaticNext = false
|
||||
let dynamicCountNext = 0
|
||||
for (let j = i + 1; j < filteredChildren.length; j++) {
|
||||
const nextChild = filteredChildren[j]
|
||||
if (isStaticChildNode(nextChild)) {
|
||||
foundStaticNext = true
|
||||
break
|
||||
}
|
||||
// if the next child has dynamic info, use it
|
||||
else if (nextChild._ssrDynamicInfo) {
|
||||
foundStaticNext = nextChild._ssrDynamicInfo.hasStaticNext
|
||||
dynamicCountNext = nextChild._ssrDynamicInfo.nextDynamicCount + 1
|
||||
break
|
||||
}
|
||||
dynamicCountNext++
|
||||
}
|
||||
info.hasStaticNext = foundStaticNext
|
||||
info.nextDynamicCount = dynamicCountNext
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node should be processed as dynamic child.
|
||||
* This is primarily used in Vapor mode hydration to wrap dynamic parts
|
||||
* with markers (`<!--[[-->` and `<!--]]-->`).
|
||||
* The purpose is to distinguish the boundaries of nodes during vapor hydration
|
||||
*
|
||||
* 1. two consecutive dynamic nodes should only wrap the second one
|
||||
* <element>
|
||||
* <element/> // Static node
|
||||
* <Comp/> // Dynamic node -> should NOT be wrapped
|
||||
* <Comp/> // Dynamic node -> should be wrapped
|
||||
* <element/> // Static node
|
||||
* </element>
|
||||
*
|
||||
* 2. three or more consecutive dynamic nodes should only wrap the
|
||||
* middle nodes, leaving the first and last static.
|
||||
* <element>
|
||||
* <element/> // Static node
|
||||
* <Comp/> // Dynamic node -> should NOT be wrapped
|
||||
* <Comp/> // Dynamic node -> should be wrapped
|
||||
* <Comp/> // Dynamic node -> should be wrapped
|
||||
* <Comp/> // Dynamic node -> should NOT be wrapped
|
||||
* <element/> // Static node
|
||||
* </element>
|
||||
*/
|
||||
function shouldProcessChildAsDynamic(
|
||||
parent: { tag?: string; children: TemplateChildNode[] },
|
||||
node: TemplateChildNode & { _ssrDynamicInfo?: DynamicInfo },
|
||||
): boolean {
|
||||
// must be inside a parent element
|
||||
if (!parent.tag) return false
|
||||
|
||||
// must has dynamic info
|
||||
const { _ssrDynamicInfo: info } = node
|
||||
if (!info) return false
|
||||
|
||||
const {
|
||||
hasStaticPrevious,
|
||||
hasStaticNext,
|
||||
prevDynamicCount,
|
||||
nextDynamicCount,
|
||||
} = info
|
||||
|
||||
// must have static nodes on both sides
|
||||
if (!hasStaticPrevious || !hasStaticNext) return false
|
||||
|
||||
const dynamicNodeCount = 1 + prevDynamicCount + nextDynamicCount
|
||||
|
||||
// For two consecutive dynamic nodes, mark the second one as dynamic
|
||||
if (dynamicNodeCount === 2) {
|
||||
return prevDynamicCount > 0
|
||||
}
|
||||
// For three or more dynamic nodes, mark the middle nodes as dynamic
|
||||
else if (dynamicNodeCount >= 3) {
|
||||
return prevDynamicCount > 0 && nextDynamicCount > 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isFragmentChild(child: TemplateChildNode): boolean {
|
||||
const { type } = child
|
||||
return type === NodeTypes.IF || type === NodeTypes.FOR
|
||||
}
|
||||
|
|
|
@ -55,7 +55,14 @@ import {
|
|||
ssrProcessTransitionGroup,
|
||||
ssrTransformTransitionGroup,
|
||||
} from './ssrTransformTransitionGroup'
|
||||
import { extend, isArray, isObject, isPlainObject, isSymbol } from '@vue/shared'
|
||||
import {
|
||||
DYNAMIC_COMPONENT_ANCHOR_LABEL,
|
||||
extend,
|
||||
isArray,
|
||||
isObject,
|
||||
isPlainObject,
|
||||
isSymbol,
|
||||
} from '@vue/shared'
|
||||
import { buildSSRProps } from './ssrTransformElement'
|
||||
import {
|
||||
ssrProcessTransition,
|
||||
|
@ -264,6 +271,8 @@ export function ssrProcessComponent(
|
|||
// dynamic component (`resolveDynamicComponent` call)
|
||||
// the codegen node is a `renderVNode` call
|
||||
context.pushStatement(node.ssrCodegenNode)
|
||||
// anchor for dynamic component for vapor hydration
|
||||
context.pushStringPart(`<!--${DYNAMIC_COMPONENT_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
processChildrenAsStatement,
|
||||
} from '../ssrCodegenTransform'
|
||||
import { SSR_RENDER_LIST } from '../runtimeHelpers'
|
||||
import { FOR_ANCHOR_LABEL } from '@vue/shared'
|
||||
|
||||
// Plugin for the first transform pass, which simply constructs the AST node
|
||||
export const ssrTransformFor: NodeTransform =
|
||||
|
@ -49,4 +50,6 @@ export function ssrProcessFor(
|
|||
if (!disableNestedFragments) {
|
||||
context.pushStringPart(`<!--]-->`)
|
||||
}
|
||||
// v-for anchor for vapor hydration
|
||||
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
type SSRTransformContext,
|
||||
processChildrenAsStatement,
|
||||
} from '../ssrCodegenTransform'
|
||||
import { IF_ANCHOR_LABEL } from '@vue/shared'
|
||||
|
||||
// Plugin for the first transform pass, which simply constructs the AST node
|
||||
export const ssrTransformIf: NodeTransform = createStructuralDirectiveTransform(
|
||||
|
@ -74,5 +75,16 @@ function processIfBranch(
|
|||
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
||||
// optimize away nested fragments when the only child is a ForNode
|
||||
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
||||
return processChildrenAsStatement(branch, context, needFragmentWrapper)
|
||||
const statement = processChildrenAsStatement(
|
||||
branch,
|
||||
context,
|
||||
needFragmentWrapper,
|
||||
)
|
||||
if (branch.condition) {
|
||||
// v-if/v-else-if anchor for vapor hydration
|
||||
statement.body.push(
|
||||
createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
|
||||
)
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
import { makeCompile } from './_utils'
|
||||
import {
|
||||
transformChildren,
|
||||
transformElement,
|
||||
transformText,
|
||||
transformVBind,
|
||||
transformVIf,
|
||||
transformVShow,
|
||||
transformVSlot,
|
||||
} from '@vue/compiler-vapor'
|
||||
import { transformTransition } from '../../src/transforms/transformTransition'
|
||||
import { DOMErrorCodes } from '@vue/compiler-dom'
|
||||
|
||||
const compileWithElementTransform = makeCompile({
|
||||
nodeTransforms: [
|
||||
transformText,
|
||||
transformVIf,
|
||||
transformElement,
|
||||
transformVSlot,
|
||||
transformChildren,
|
||||
transformTransition,
|
||||
],
|
||||
directiveTransforms: {
|
||||
bind: transformVBind,
|
||||
show: transformVShow,
|
||||
},
|
||||
})
|
||||
|
||||
describe('compiler: transition', () => {
|
||||
test('basic', () => {
|
||||
const { code } = compileWithElementTransform(
|
||||
`<Transition><h1 v-show="show">foo</h1></Transition>`,
|
||||
)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('v-show + appear', () => {
|
||||
const { code } = compileWithElementTransform(
|
||||
`<Transition appear><h1 v-show="show">foo</h1></Transition>`,
|
||||
)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('work with v-if', () => {
|
||||
const { code } = compileWithElementTransform(
|
||||
`<Transition><h1 v-if="show">foo</h1></Transition>`,
|
||||
)
|
||||
|
||||
expect(code).toMatchSnapshot()
|
||||
// n2 should have a key
|
||||
expect(code).contains('n2.$key = 2')
|
||||
})
|
||||
|
||||
test('work with dynamic keyed children', () => {
|
||||
const { code } = compileWithElementTransform(
|
||||
`<Transition>
|
||||
<h1 :key="key">foo</h1>
|
||||
</Transition>`,
|
||||
)
|
||||
|
||||
expect(code).toMatchSnapshot()
|
||||
expect(code).contains('_createKeyedFragment(() => _ctx.key')
|
||||
// should preserve key
|
||||
expect(code).contains('n0.$key = _ctx.key')
|
||||
})
|
||||
|
||||
function checkWarning(template: string, shouldWarn = true) {
|
||||
const onError = vi.fn()
|
||||
compileWithElementTransform(template, { onError })
|
||||
if (shouldWarn) {
|
||||
expect(onError).toHaveBeenCalled()
|
||||
expect(onError.mock.calls).toMatchObject([
|
||||
[{ code: DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN }],
|
||||
])
|
||||
} else {
|
||||
expect(onError).not.toHaveBeenCalled()
|
||||
}
|
||||
}
|
||||
|
||||
test('warns if multiple children', () => {
|
||||
checkWarning(
|
||||
`<Transition>
|
||||
<h1>foo</h1>
|
||||
<h2>bar</h2>
|
||||
</Transition>`,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('warns with v-for', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<div v-for="i in items">hey</div>
|
||||
</transition>
|
||||
`,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('warns with multiple v-if + v-for', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<div v-if="a" v-for="i in items">hey</div>
|
||||
<div v-else v-for="i in items">hey</div>
|
||||
</transition>
|
||||
`,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('warns with template v-if', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<template v-if="ok"></template>
|
||||
</transition>
|
||||
`,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('warns with multiple templates', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<template v-if="a"></template>
|
||||
<template v-else></template>
|
||||
</transition>
|
||||
`,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('warns if multiple children with v-if', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<div v-if="one">hey</div>
|
||||
<div v-if="other">hey</div>
|
||||
</transition>
|
||||
`,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('does not warn with regular element', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<div>hey</div>
|
||||
</transition>
|
||||
`,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('does not warn with one single v-if', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<div v-if="a">hey</div>
|
||||
</transition>
|
||||
`,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('does not warn with v-if v-else-if v-else', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<div v-if="a">hey</div>
|
||||
<div v-else-if="b">hey</div>
|
||||
<div v-else>hey</div>
|
||||
</transition>
|
||||
`,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('does not warn with v-if v-else', () => {
|
||||
checkWarning(
|
||||
`
|
||||
<transition>
|
||||
<div v-if="a">hey</div>
|
||||
<div v-else>hey</div>
|
||||
</transition>
|
||||
`,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('inject persisted when child has v-show', () => {
|
||||
expect(
|
||||
compileWithElementTransform(`
|
||||
<Transition>
|
||||
<div v-show="ok" />
|
||||
</Transition>
|
||||
`).code,
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('the v-if/else-if/else branches in Transition should ignore comments', () => {
|
||||
expect(
|
||||
compileWithElementTransform(`
|
||||
<transition>
|
||||
<div v-if="a">hey</div>
|
||||
<!-- this should be ignored -->
|
||||
<div v-else-if="b">hey</div>
|
||||
<!-- this should be ignored -->
|
||||
<div v-else>
|
||||
<p v-if="c"/>
|
||||
<!-- this should not be ignored -->
|
||||
<p v-else/>
|
||||
</div>
|
||||
</transition>
|
||||
`).code,
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,128 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`compiler: transition > basic 1`] = `
|
||||
"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue';
|
||||
const t0 = _template("<h1>foo</h1>")
|
||||
|
||||
export function render(_ctx) {
|
||||
const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, {
|
||||
"default": () => {
|
||||
const n0 = t0()
|
||||
_applyVShow(n0, () => (_ctx.show))
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n1
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transition > inject persisted when child has v-show 1`] = `
|
||||
"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>")
|
||||
|
||||
export function render(_ctx) {
|
||||
const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, {
|
||||
"default": () => {
|
||||
const n0 = t0()
|
||||
_applyVShow(n0, () => (_ctx.ok))
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n1
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transition > the v-if/else-if/else branches in Transition should ignore comments 1`] = `
|
||||
"import { VaporTransition as _VaporTransition, setInsertionState as _setInsertionState, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue';
|
||||
const t0 = _template("<div>hey</div>")
|
||||
const t1 = _template("<p></p>")
|
||||
const t2 = _template("<div></div>")
|
||||
|
||||
export function render(_ctx) {
|
||||
const n16 = _createComponent(_VaporTransition, null, {
|
||||
"default": () => {
|
||||
const n0 = _createIf(() => (_ctx.a), () => {
|
||||
const n2 = t0()
|
||||
n2.$key = 2
|
||||
return n2
|
||||
}, () => _createIf(() => (_ctx.b), () => {
|
||||
const n5 = t0()
|
||||
n5.$key = 5
|
||||
return n5
|
||||
}, () => {
|
||||
const n14 = t2()
|
||||
_setInsertionState(n14, 0)
|
||||
const n9 = _createIf(() => (_ctx.c), () => {
|
||||
const n11 = t1()
|
||||
return n11
|
||||
}, () => {
|
||||
const n13 = t1()
|
||||
return n13
|
||||
})
|
||||
n14.$key = 14
|
||||
return n14
|
||||
}))
|
||||
return [n0, n3, n7]
|
||||
}
|
||||
}, true)
|
||||
return n16
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transition > v-show + appear 1`] = `
|
||||
"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue';
|
||||
const t0 = _template("<h1>foo</h1>")
|
||||
|
||||
export function render(_ctx) {
|
||||
const deferredApplyVShows = []
|
||||
const n1 = _createComponent(_VaporTransition, {
|
||||
appear: () => (""),
|
||||
persisted: () => ("")
|
||||
}, {
|
||||
"default": () => {
|
||||
const n0 = t0()
|
||||
deferredApplyVShows.push(() => _applyVShow(n0, () => (_ctx.show)))
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
deferredApplyVShows.forEach(fn => fn())
|
||||
return n1
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transition > work with dynamic keyed children 1`] = `
|
||||
"import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, createComponent as _createComponent, template as _template } from 'vue';
|
||||
const t0 = _template("<h1>foo</h1>")
|
||||
|
||||
export function render(_ctx) {
|
||||
const n1 = _createComponent(_VaporTransition, null, {
|
||||
"default": () => {
|
||||
return _createKeyedFragment(() => _ctx.key, () => {
|
||||
const n0 = t0()
|
||||
n0.$key = _ctx.key
|
||||
return n0
|
||||
})
|
||||
}
|
||||
}, true)
|
||||
return n1
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transition > work with v-if 1`] = `
|
||||
"import { VaporTransition as _VaporTransition, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue';
|
||||
const t0 = _template("<h1>foo</h1>")
|
||||
|
||||
export function render(_ctx) {
|
||||
const n3 = _createComponent(_VaporTransition, null, {
|
||||
"default": () => {
|
||||
const n0 = _createIf(() => (_ctx.show), () => {
|
||||
const n2 = t0()
|
||||
n2.$key = 2
|
||||
return n2
|
||||
})
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n3
|
||||
}"
|
||||
`;
|
|
@ -63,6 +63,15 @@ export function render(_ctx) {
|
|||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: template ref transform > static ref (inline mode) 1`] = `
|
||||
"
|
||||
const _setTemplateRef = _createTemplateRefSetter()
|
||||
const n0 = t0()
|
||||
_setTemplateRef(n0, foo)
|
||||
return n0
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`compiler: template ref transform > static ref 1`] = `
|
||||
"import { createTemplateRefSetter as _createTemplateRefSetter, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>", true)
|
||||
|
|
|
@ -113,6 +113,35 @@ export function render(_ctx) {
|
|||
}"
|
||||
`;
|
||||
|
||||
exports[`cache multiple access > object property name substring cases 1`] = `
|
||||
"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>", true)
|
||||
|
||||
export function render(_ctx) {
|
||||
const n0 = t0()
|
||||
_renderEffect(() => {
|
||||
const _p = _ctx.p
|
||||
const _p_title = _p.title
|
||||
_setProp(n0, "id", _p_title + _p.titles + _p_title)
|
||||
})
|
||||
return n0
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`cache multiple access > optional chaining 1`] = `
|
||||
"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>", true)
|
||||
|
||||
export function render(_ctx) {
|
||||
const n0 = t0()
|
||||
_renderEffect(() => {
|
||||
const _obj = _ctx.obj
|
||||
_setProp(n0, "id", _obj?.foo + _obj?.bar)
|
||||
})
|
||||
return n0
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`cache multiple access > repeated expression in expressions 1`] = `
|
||||
"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>")
|
||||
|
@ -180,6 +209,20 @@ export function render(_ctx) {
|
|||
}"
|
||||
`;
|
||||
|
||||
exports[`cache multiple access > variable name substring edge cases 1`] = `
|
||||
"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>", true)
|
||||
|
||||
export function render(_ctx) {
|
||||
const n0 = t0()
|
||||
_renderEffect(() => {
|
||||
const _title = _ctx.title
|
||||
_setProp(n0, "id", _title + _ctx.titles + _title)
|
||||
})
|
||||
return n0
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler v-bind > .attr modifier 1`] = `
|
||||
"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>", true)
|
||||
|
|
|
@ -32,3 +32,13 @@ export function render(_ctx) {
|
|||
return n0
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`v-html > work with dynamic component 1`] = `
|
||||
"import { createDynamicComponent as _createDynamicComponent, setHtml as _setHtml, renderEffect as _renderEffect } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const n0 = _createDynamicComponent(() => ('button'), null, null, true)
|
||||
_renderEffect(() => _setHtml(n0.nodes, _ctx.foo))
|
||||
return n0
|
||||
}"
|
||||
`;
|
||||
|
|
|
@ -103,6 +103,97 @@ export function render(_ctx) {
|
|||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot w/ nested component> 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n2 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n1 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createForwardedSlot("default", null)
|
||||
return n0
|
||||
}
|
||||
})
|
||||
return n1
|
||||
}
|
||||
}, true)
|
||||
return n2
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot> tag only 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n1 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createForwardedSlot("default", null)
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n1
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ template 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n2 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createForwardedSlot("default", null)
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n2
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ v-for 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createFor as _createFor, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n3 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createFor(() => (_ctx.b), (_for_item0) => {
|
||||
const n2 = _createForwardedSlot("default", null)
|
||||
return n2
|
||||
})
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n3
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > forwarded slots > <slot> tag w/ v-if 1`] = `
|
||||
"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createIf as _createIf, createComponentWithFallback as _createComponentWithFallback } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const _createForwardedSlot = _forwardedSlotCreator()
|
||||
const _component_Comp = _resolveComponent("Comp")
|
||||
const n3 = _createComponentWithFallback(_component_Comp, null, {
|
||||
"default": () => {
|
||||
const n0 = _createIf(() => (_ctx.ok), () => {
|
||||
const n2 = _createForwardedSlot("default", null)
|
||||
return n2
|
||||
})
|
||||
return n0
|
||||
}
|
||||
}, true)
|
||||
return n3
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`compiler: transform slot > implicit default slot 1`] = `
|
||||
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
|
||||
const t0 = _template("<div></div>")
|
||||
|
|
|
@ -33,3 +33,13 @@ export function render(_ctx) {
|
|||
return n0
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`v-text > work with dynamic component 1`] = `
|
||||
"import { createDynamicComponent as _createDynamicComponent, toDisplayString as _toDisplayString, setElementText as _setElementText, renderEffect as _renderEffect } from 'vue';
|
||||
|
||||
export function render(_ctx) {
|
||||
const n0 = _createDynamicComponent(() => ('button'), null, null, true)
|
||||
_renderEffect(() => _setElementText(n0.nodes, _toDisplayString(_ctx.foo), true))
|
||||
return n0
|
||||
}"
|
||||
`;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { BindingTypes } from '@vue/compiler-dom'
|
||||
import {
|
||||
DynamicFlag,
|
||||
type ForIRNode,
|
||||
|
@ -48,6 +49,16 @@ describe('compiler: template ref transform', () => {
|
|||
expect(code).contains('_setTemplateRef(n0, "foo")')
|
||||
})
|
||||
|
||||
test('static ref (inline mode)', () => {
|
||||
const { code } = compileWithTransformRef(`<div ref="foo" />`, {
|
||||
inline: true,
|
||||
bindingMetadata: { foo: BindingTypes.SETUP_REF },
|
||||
})
|
||||
expect(code).matchSnapshot()
|
||||
// pass the actual ref
|
||||
expect(code).contains('_setTemplateRef(n0, foo)')
|
||||
})
|
||||
|
||||
test('dynamic ref', () => {
|
||||
const { ir, code } = compileWithTransformRef(`<div :ref="foo" />`)
|
||||
|
||||
|
|
|
@ -785,6 +785,25 @@ describe('cache multiple access', () => {
|
|||
expect(code).contains('_setProp(n0, "id", _obj[1][_ctx.baz] + _obj.bar)')
|
||||
})
|
||||
|
||||
test('variable name substring edge cases', () => {
|
||||
const { code } = compileWithVBind(
|
||||
`<div :id="title + titles + title"></div>`,
|
||||
)
|
||||
expect(code).matchSnapshot()
|
||||
expect(code).contains('const _title = _ctx.title')
|
||||
expect(code).contains('_setProp(n0, "id", _title + _ctx.titles + _title)')
|
||||
})
|
||||
|
||||
test('object property name substring cases', () => {
|
||||
const { code } = compileWithVBind(
|
||||
`<div :id="p.title + p.titles + p.title"></div>`,
|
||||
)
|
||||
expect(code).matchSnapshot()
|
||||
expect(code).contains('const _p = _ctx.p')
|
||||
expect(code).contains('const _p_title = _p.title')
|
||||
expect(code).contains('_setProp(n0, "id", _p_title + _p.titles + _p_title)')
|
||||
})
|
||||
|
||||
test('cache variable used in both property shorthand and normal binding', () => {
|
||||
const { code } = compileWithVBind(`
|
||||
<div :style="{color}" :id="color"/>
|
||||
|
@ -794,6 +813,13 @@ describe('cache multiple access', () => {
|
|||
expect(code).contains('_setStyle(n0, {color: _color})')
|
||||
})
|
||||
|
||||
test('optional chaining', () => {
|
||||
const { code } = compileWithVBind(`<div :id="obj?.foo + obj?.bar"></div>`)
|
||||
expect(code).matchSnapshot()
|
||||
expect(code).contains('const _obj = _ctx.obj')
|
||||
expect(code).contains('_setProp(n0, "id", _obj?.foo + _obj?.bar)')
|
||||
})
|
||||
|
||||
test('not cache variable only used in property shorthand', () => {
|
||||
const { code } = compileWithVBind(`
|
||||
<div :style="{color}" />
|
||||
|
|
|
@ -54,6 +54,14 @@ describe('v-html', () => {
|
|||
expect(code).matchSnapshot()
|
||||
})
|
||||
|
||||
test('work with dynamic component', () => {
|
||||
const { code } = compileWithVHtml(
|
||||
`<component :is="'button'" v-html="foo"/>`,
|
||||
)
|
||||
expect(code).matchSnapshot()
|
||||
expect(code).contains('setHtml(n0.nodes, _ctx.foo))')
|
||||
})
|
||||
|
||||
test('should raise error and ignore children when v-html is present', () => {
|
||||
const onError = vi.fn()
|
||||
const { code, ir, helpers } = compileWithVHtml(
|
||||
|
|
|
@ -420,6 +420,35 @@ describe('compiler: transform slot', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('forwarded slots', () => {
|
||||
test('<slot> tag only', () => {
|
||||
const { code } = compileWithSlots(`<Comp><slot/></Comp>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('<slot> tag w/ v-if', () => {
|
||||
const { code } = compileWithSlots(`<Comp><slot v-if="ok"/></Comp>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('<slot> tag w/ v-for', () => {
|
||||
const { code } = compileWithSlots(`<Comp><slot v-for="a in b"/></Comp>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('<slot> tag w/ template', () => {
|
||||
const { code } = compileWithSlots(
|
||||
`<Comp><template #default><slot/></template></Comp>`,
|
||||
)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('<slot w/ nested component>', () => {
|
||||
const { code } = compileWithSlots(`<Comp><Comp><slot/></Comp></Comp>`)
|
||||
expect(code).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors', () => {
|
||||
test('error on extraneous children w/ named default slot', () => {
|
||||
const onError = vi.fn()
|
||||
|
|
|
@ -58,6 +58,16 @@ describe('v-text', () => {
|
|||
expect(code).matchSnapshot()
|
||||
})
|
||||
|
||||
test('work with dynamic component', () => {
|
||||
const { code } = compileWithVText(
|
||||
`<component :is="'button'" v-text="foo"/>`,
|
||||
)
|
||||
expect(code).matchSnapshot()
|
||||
expect(code).contains(
|
||||
'setElementText(n0.nodes, _toDisplayString(_ctx.foo), true)',
|
||||
)
|
||||
})
|
||||
|
||||
test('should raise error and ignore children when v-text is present', () => {
|
||||
const onError = vi.fn()
|
||||
const { code, ir } = compileWithVText(`<div v-text="test">hello</div>`, {
|
||||
|
|
|
@ -26,6 +26,7 @@ import { transformVFor } from './transforms/vFor'
|
|||
import { transformComment } from './transforms/transformComment'
|
||||
import { transformSlotOutlet } from './transforms/transformSlotOutlet'
|
||||
import { transformVSlot } from './transforms/vSlot'
|
||||
import { transformTransition } from './transforms/transformTransition'
|
||||
import type { HackOptions } from './ir'
|
||||
|
||||
export { wrapTemplate } from './transforms/utils'
|
||||
|
@ -54,6 +55,7 @@ export function compile(
|
|||
extend({}, resolvedOptions, {
|
||||
nodeTransforms: [
|
||||
...nodeTransforms,
|
||||
...(__DEV__ ? [transformTransition] : []),
|
||||
...(options.nodeTransforms || []), // user transforms
|
||||
],
|
||||
directiveTransforms: extend(
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
genCall,
|
||||
} from './generators/utils'
|
||||
import { setTemplateRefIdent } from './generators/templateRef'
|
||||
import { createForwardedSlotIdent } from './generators/slotOutlet'
|
||||
|
||||
export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
|
||||
|
||||
|
@ -129,6 +130,12 @@ export function generate(
|
|||
`const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
|
||||
)
|
||||
}
|
||||
if (ir.hasForwardedSlot) {
|
||||
push(
|
||||
NEWLINE,
|
||||
`const ${createForwardedSlotIdent} = ${context.helper('forwardedSlotCreator')}()`,
|
||||
)
|
||||
}
|
||||
push(...genBlockContent(ir.block, context, true))
|
||||
push(INDENT_END, NEWLINE)
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import type { CodegenContext } from '../generate'
|
|||
import { genEffects, genOperations } from './operation'
|
||||
import { genChildren, genSelf } from './template'
|
||||
import { toValidAssetId } from '@vue/compiler-dom'
|
||||
import { genExpression } from './expression'
|
||||
|
||||
export function genBlock(
|
||||
oper: BlockIRNode,
|
||||
|
@ -40,9 +41,13 @@ export function genBlockContent(
|
|||
customReturns?: (returns: CodeFragment[]) => CodeFragment[],
|
||||
): CodeFragment[] {
|
||||
const [frag, push] = buildCodeFragment()
|
||||
const { dynamic, effect, operation, returns } = block
|
||||
const { dynamic, effect, operation, returns, key } = block
|
||||
const resetBlock = context.enterBlock(block)
|
||||
|
||||
if (block.hasDeferredVShow) {
|
||||
push(NEWLINE, `const deferredApplyVShows = []`)
|
||||
}
|
||||
|
||||
if (root) {
|
||||
for (let name of context.ir.component) {
|
||||
const id = toValidAssetId(name, 'component')
|
||||
|
@ -72,6 +77,19 @@ export function genBlockContent(
|
|||
push(...genOperations(operation, context))
|
||||
push(...genEffects(effect, context))
|
||||
|
||||
if (block.hasDeferredVShow) {
|
||||
push(NEWLINE, `deferredApplyVShows.forEach(fn => fn())`)
|
||||
}
|
||||
|
||||
if (dynamic.needsKey) {
|
||||
for (const child of dynamic.children) {
|
||||
const keyValue = key
|
||||
? genExpression(key, context)
|
||||
: JSON.stringify(child.id)
|
||||
push(NEWLINE, `n${child.id}.$key = `, ...keyValue)
|
||||
}
|
||||
}
|
||||
|
||||
push(NEWLINE, `return `)
|
||||
|
||||
const returnNodes = returns.map(n => `n${n}`)
|
||||
|
|
|
@ -39,6 +39,7 @@ import { genEventHandler } from './event'
|
|||
import { genDirectiveModifiers, genDirectivesForElement } from './directive'
|
||||
import { genBlock } from './block'
|
||||
import { genModelHandler } from './vModel'
|
||||
import { isBuiltInComponent } from '../utils'
|
||||
|
||||
export function genCreateComponent(
|
||||
operation: CreateComponentIRNode,
|
||||
|
@ -47,25 +48,28 @@ export function genCreateComponent(
|
|||
const { helper } = context
|
||||
|
||||
const tag = genTag()
|
||||
const { root, props, slots, once } = operation
|
||||
const { root, props, slots, once, scopeId } = operation
|
||||
const rawSlots = genRawSlots(slots, context)
|
||||
const [ids, handlers] = processInlineHandlers(props, context)
|
||||
const rawProps = context.withId(() => genRawProps(props, context), ids)
|
||||
|
||||
const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
|
||||
(acc, { name, value }) => {
|
||||
(acc, { name, value }: InlineHandler) => {
|
||||
const handler = genEventHandler(context, value, undefined, false)
|
||||
return [...acc, `const ${name} = `, ...handler, NEWLINE]
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const isDynamicComponent = operation.dynamic && !operation.dynamic.isStatic
|
||||
if (isDynamicComponent) context.block.dynamicComponents.push(operation.id)
|
||||
|
||||
return [
|
||||
NEWLINE,
|
||||
...inlineHandlers,
|
||||
`const n${operation.id} = `,
|
||||
...genCall(
|
||||
operation.dynamic && !operation.dynamic.isStatic
|
||||
isDynamicComponent
|
||||
? helper('createDynamicComponent')
|
||||
: operation.asset
|
||||
? helper('createComponentWithFallback')
|
||||
|
@ -75,6 +79,7 @@ export function genCreateComponent(
|
|||
rawSlots,
|
||||
root ? 'true' : false,
|
||||
once && 'true',
|
||||
scopeId && JSON.stringify(scopeId),
|
||||
),
|
||||
...genDirectivesForElement(operation.id, context),
|
||||
]
|
||||
|
@ -92,8 +97,15 @@ export function genCreateComponent(
|
|||
} else if (operation.asset) {
|
||||
return toValidAssetId(operation.tag, 'component')
|
||||
} else {
|
||||
const { tag } = operation
|
||||
const builtInTag = isBuiltInComponent(tag)
|
||||
if (builtInTag) {
|
||||
// @ts-expect-error
|
||||
helper(builtInTag)
|
||||
return `_${builtInTag}`
|
||||
}
|
||||
return genExpression(
|
||||
extend(createSimpleExpression(operation.tag, false), { ast: null }),
|
||||
extend(createSimpleExpression(tag, false), { ast: null }),
|
||||
context,
|
||||
)
|
||||
}
|
||||
|
@ -127,7 +139,10 @@ function processInlineHandlers(
|
|||
const isMemberExp = isMemberExpression(value, context.options)
|
||||
// cache inline handlers (fn expression or inline statement)
|
||||
if (!isMemberExp) {
|
||||
const name = getUniqueHandlerName(context, `_on_${prop.key.content}`)
|
||||
const name = getUniqueHandlerName(
|
||||
context,
|
||||
`_on_${prop.key.content.replace(/-/g, '_')}`,
|
||||
)
|
||||
handlers.push({ name, value })
|
||||
ids[name] = null
|
||||
// replace the original prop value with the handler name
|
||||
|
@ -396,7 +411,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
|
|||
let propsName: string | undefined
|
||||
let exitScope: (() => void) | undefined
|
||||
let depth: number | undefined
|
||||
const { props } = oper
|
||||
const { props, key } = oper
|
||||
const idsOfProps = new Set<string>()
|
||||
|
||||
if (props) {
|
||||
|
@ -424,11 +439,28 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
|
|||
? `${propsName}[${JSON.stringify(id)}]`
|
||||
: null),
|
||||
)
|
||||
const blockFn = context.withId(
|
||||
let blockFn = context.withId(
|
||||
() => genBlock(oper, context, [propsName]),
|
||||
idMap,
|
||||
)
|
||||
exitScope && exitScope()
|
||||
|
||||
if (key) {
|
||||
blockFn = [
|
||||
`() => {`,
|
||||
INDENT_START,
|
||||
NEWLINE,
|
||||
`return `,
|
||||
...genCall(
|
||||
context.helper('createKeyedFragment'),
|
||||
[`() => `, ...genExpression(key, context)],
|
||||
blockFn,
|
||||
),
|
||||
INDENT_END,
|
||||
NEWLINE,
|
||||
`}`,
|
||||
]
|
||||
}
|
||||
|
||||
return blockFn
|
||||
}
|
||||
|
|
|
@ -283,7 +283,13 @@ export function processExpressions(
|
|||
function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
||||
const seenVariable: Record<string, number> = Object.create(null)
|
||||
const variableToExpMap = new Map<string, Set<SimpleExpressionNode>>()
|
||||
const expToVariableMap = new Map<SimpleExpressionNode, string[]>()
|
||||
const expToVariableMap = new Map<
|
||||
SimpleExpressionNode,
|
||||
Array<{
|
||||
name: string
|
||||
loc?: { start: number; end: number }
|
||||
}>
|
||||
>()
|
||||
const seenIdentifier = new Set<string>()
|
||||
const updatedVariable = new Set<string>()
|
||||
|
||||
|
@ -291,6 +297,7 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
|||
name: string,
|
||||
exp: SimpleExpressionNode,
|
||||
isIdentifier: boolean,
|
||||
loc?: { start: number; end: number },
|
||||
parentStack: Node[] = [],
|
||||
) => {
|
||||
if (isIdentifier) seenIdentifier.add(name)
|
||||
|
@ -299,7 +306,11 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
|||
name,
|
||||
(variableToExpMap.get(name) || new Set()).add(exp),
|
||||
)
|
||||
expToVariableMap.set(exp, (expToVariableMap.get(exp) || []).concat(name))
|
||||
|
||||
const variables = expToVariableMap.get(exp) || []
|
||||
variables.push({ name, loc })
|
||||
expToVariableMap.set(exp, variables)
|
||||
|
||||
if (
|
||||
parentStack.some(
|
||||
p => p.type === 'UpdateExpression' || p.type === 'AssignmentExpression',
|
||||
|
@ -317,12 +328,27 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
|
|||
|
||||
walkIdentifiers(exp.ast, (currentNode, parent, parentStack) => {
|
||||
if (parent && isMemberExpression(parent)) {
|
||||
const memberExp = extractMemberExpression(parent, name => {
|
||||
registerVariable(name, exp, true)
|
||||
const memberExp = extractMemberExpression(parent, id => {
|
||||
registerVariable(id.name, exp, true, {
|
||||
start: id.start!,
|
||||
end: id.end!,
|
||||
})
|
||||
})
|
||||
registerVariable(memberExp, exp, false, parentStack)
|
||||
registerVariable(
|
||||
memberExp,
|
||||
exp,
|
||||
false,
|
||||
{ start: parent.start!, end: parent.end! },
|
||||
parentStack,
|
||||
)
|
||||
} else if (!parentStack.some(isMemberExpression)) {
|
||||
registerVariable(currentNode.name, exp, true, parentStack)
|
||||
registerVariable(
|
||||
currentNode.name,
|
||||
exp,
|
||||
true,
|
||||
{ start: currentNode.start!, end: currentNode.end! },
|
||||
parentStack,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -340,11 +366,22 @@ function processRepeatedVariables(
|
|||
context: CodegenContext,
|
||||
seenVariable: Record<string, number>,
|
||||
variableToExpMap: Map<string, Set<SimpleExpressionNode>>,
|
||||
expToVariableMap: Map<SimpleExpressionNode, string[]>,
|
||||
expToVariableMap: Map<
|
||||
SimpleExpressionNode,
|
||||
Array<{ name: string; loc?: { start: number; end: number } }>
|
||||
>,
|
||||
seenIdentifier: Set<string>,
|
||||
updatedVariable: Set<string>,
|
||||
): DeclarationValue[] {
|
||||
const declarations: DeclarationValue[] = []
|
||||
const expToReplacementMap = new Map<
|
||||
SimpleExpressionNode,
|
||||
Array<{
|
||||
name: string
|
||||
locs: { start: number; end: number }[]
|
||||
}>
|
||||
>()
|
||||
|
||||
for (const [name, exps] of variableToExpMap) {
|
||||
if (updatedVariable.has(name)) continue
|
||||
if (seenVariable[name] > 1 && exps.size > 0) {
|
||||
|
@ -356,12 +393,20 @@ function processRepeatedVariables(
|
|||
// e.g., foo[baz] -> foo_baz.
|
||||
// for identifiers, we don't need to replace the content - they will be
|
||||
// replaced during context.withId(..., ids)
|
||||
const replaceRE = new RegExp(escapeRegExp(name), 'g')
|
||||
exps.forEach(node => {
|
||||
if (node.ast) {
|
||||
node.content = node.content.replace(replaceRE, varName)
|
||||
// re-parse the expression
|
||||
node.ast = parseExp(context, node.content)
|
||||
if (node.ast && varName !== name) {
|
||||
const replacements = expToReplacementMap.get(node) || []
|
||||
replacements.push({
|
||||
name: varName,
|
||||
locs: expToVariableMap.get(node)!.reduce(
|
||||
(locs, v) => {
|
||||
if (v.name === name && v.loc) locs.push(v.loc)
|
||||
return locs
|
||||
},
|
||||
[] as { start: number; end: number }[],
|
||||
),
|
||||
})
|
||||
expToReplacementMap.set(node, replacements)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -384,15 +429,35 @@ function processRepeatedVariables(
|
|||
}
|
||||
}
|
||||
|
||||
for (const [exp, replacements] of expToReplacementMap) {
|
||||
replacements
|
||||
.flatMap(({ name, locs }) =>
|
||||
locs.map(({ start, end }) => ({ start, end, name })),
|
||||
)
|
||||
.sort((a, b) => b.end - a.end)
|
||||
.forEach(({ start, end, name }) => {
|
||||
exp.content =
|
||||
exp.content.slice(0, start - 1) + name + exp.content.slice(end - 1)
|
||||
})
|
||||
|
||||
// re-parse the expression
|
||||
exp.ast = parseExp(context, exp.content)
|
||||
}
|
||||
|
||||
return declarations
|
||||
}
|
||||
|
||||
function shouldDeclareVariable(
|
||||
name: string,
|
||||
expToVariableMap: Map<SimpleExpressionNode, string[]>,
|
||||
expToVariableMap: Map<
|
||||
SimpleExpressionNode,
|
||||
Array<{ name: string; loc?: { start: number; end: number } }>
|
||||
>,
|
||||
exps: Set<SimpleExpressionNode>,
|
||||
): boolean {
|
||||
const vars = Array.from(exps, exp => expToVariableMap.get(exp)!)
|
||||
const vars = Array.from(exps, exp =>
|
||||
expToVariableMap.get(exp)!.map(v => v.name),
|
||||
)
|
||||
// assume name equals to `foo`
|
||||
// if each expression only references `foo`, declaration is needed
|
||||
// to avoid reactivity tracking
|
||||
|
@ -439,12 +504,15 @@ function processRepeatedExpressions(
|
|||
expressions: SimpleExpressionNode[],
|
||||
varDeclarations: DeclarationValue[],
|
||||
updatedVariable: Set<string>,
|
||||
expToVariableMap: Map<SimpleExpressionNode, string[]>,
|
||||
expToVariableMap: Map<
|
||||
SimpleExpressionNode,
|
||||
Array<{ name: string; loc?: { start: number; end: number } }>
|
||||
>,
|
||||
): DeclarationValue[] {
|
||||
const declarations: DeclarationValue[] = []
|
||||
const seenExp = expressions.reduce(
|
||||
(acc, exp) => {
|
||||
const variables = expToVariableMap.get(exp)
|
||||
const variables = expToVariableMap.get(exp)!.map(v => v.name)
|
||||
// only handle expressions that are not identifiers
|
||||
if (
|
||||
exp.ast &&
|
||||
|
@ -572,12 +640,12 @@ function genVarName(exp: string): string {
|
|||
|
||||
function extractMemberExpression(
|
||||
exp: Node,
|
||||
onIdentifier: (name: string) => void,
|
||||
onIdentifier: (id: Identifier) => void,
|
||||
): string {
|
||||
if (!exp) return ''
|
||||
switch (exp.type) {
|
||||
case 'Identifier': // foo[bar]
|
||||
onIdentifier(exp.name)
|
||||
onIdentifier(exp)
|
||||
return exp.name
|
||||
case 'StringLiteral': // foo['bar']
|
||||
return exp.extra ? (exp.extra.raw as string) : exp.value
|
||||
|
@ -588,6 +656,7 @@ function extractMemberExpression(
|
|||
case 'CallExpression': // foo[bar(baz)]
|
||||
return `${extractMemberExpression(exp.callee, onIdentifier)}(${exp.arguments.map(arg => extractMemberExpression(arg, onIdentifier)).join(', ')})`
|
||||
case 'MemberExpression': // foo[bar.baz]
|
||||
case 'OptionalMemberExpression': // foo?.bar
|
||||
const object = extractMemberExpression(exp.object, onIdentifier)
|
||||
const prop = exp.computed
|
||||
? `[${extractMemberExpression(exp.property, onIdentifier)}]`
|
||||
|
|
|
@ -7,10 +7,21 @@ export function genSetHtml(
|
|||
oper: SetHtmlIRNode,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const { helper } = context
|
||||
const {
|
||||
helper,
|
||||
block: { dynamicComponents },
|
||||
} = context
|
||||
|
||||
const isDynamicComponent = dynamicComponents.includes(oper.element)
|
||||
const { value, element } = oper
|
||||
return [
|
||||
NEWLINE,
|
||||
...genCall(helper('setHtml'), `n${element}`, genExpression(value, context)),
|
||||
...genCall(
|
||||
helper('setHtml'),
|
||||
// if the element is a dynamic component (VaporFragment)
|
||||
// it should set html to the VaporFragment's nodes
|
||||
`n${element}${isDynamicComponent ? '.nodes' : ''}`,
|
||||
genExpression(value, context),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ import { genExpression } from './expression'
|
|||
import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
|
||||
import { genRawProps } from './component'
|
||||
|
||||
export const createForwardedSlotIdent = `_createForwardedSlot`
|
||||
|
||||
export function genSlotOutlet(
|
||||
oper: SlotOutletIRNode,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const { helper } = context
|
||||
const { id, name, fallback } = oper
|
||||
const { id, name, fallback, forwarded } = oper
|
||||
const [frag, push] = buildCodeFragment()
|
||||
|
||||
const nameExpr = name.isStatic
|
||||
|
@ -26,7 +28,7 @@ export function genSlotOutlet(
|
|||
NEWLINE,
|
||||
`const n${id} = `,
|
||||
...genCall(
|
||||
helper('createSlot'),
|
||||
forwarded ? createForwardedSlotIdent : helper('createSlot'),
|
||||
nameExpr,
|
||||
genRawProps(oper.props, context) || 'null',
|
||||
fallbackArg,
|
||||
|
|
|
@ -24,10 +24,10 @@ export function genSelf(
|
|||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const [frag, push] = buildCodeFragment()
|
||||
const { id, template, operation } = dynamic
|
||||
const { id, template, operation, dynamicChildOffset } = dynamic
|
||||
|
||||
if (id !== undefined && template !== undefined) {
|
||||
push(NEWLINE, `const n${id} = t${template}()`)
|
||||
push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`)
|
||||
push(...genDirectivesForElement(id, context))
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { genExpression } from './expression'
|
|||
import type { CodegenContext } from '../generate'
|
||||
import type { DeclareOldRefIRNode, SetTemplateRefIRNode } from '../ir'
|
||||
import { type CodeFragment, NEWLINE, genCall } from './utils'
|
||||
import { BindingTypes, type SimpleExpressionNode } from '@vue/compiler-dom'
|
||||
|
||||
export const setTemplateRefIdent = `_setTemplateRef`
|
||||
|
||||
|
@ -15,7 +16,7 @@ export function genSetTemplateRef(
|
|||
...genCall(
|
||||
setTemplateRefIdent, // will be generated in root scope
|
||||
`n${oper.element}`,
|
||||
genExpression(oper.value, context),
|
||||
genRefValue(oper.value, context),
|
||||
oper.effect ? `r${oper.element}` : oper.refFor ? 'void 0' : undefined,
|
||||
oper.refFor && 'true',
|
||||
),
|
||||
|
@ -25,3 +26,20 @@ export function genSetTemplateRef(
|
|||
export function genDeclareOldRef(oper: DeclareOldRefIRNode): CodeFragment[] {
|
||||
return [NEWLINE, `let r${oper.id}`]
|
||||
}
|
||||
|
||||
function genRefValue(value: SimpleExpressionNode, context: CodegenContext) {
|
||||
// in inline mode there is no setupState object, so we can't use string
|
||||
// keys to set the ref. Instead, we need to transform it to pass the
|
||||
// actual ref instead.
|
||||
if (!__BROWSER__ && value && context.options.inline) {
|
||||
const binding = context.options.bindingMetadata[value.content]
|
||||
if (
|
||||
binding === BindingTypes.SETUP_LET ||
|
||||
binding === BindingTypes.SETUP_REF ||
|
||||
binding === BindingTypes.SETUP_MAYBE_REF
|
||||
) {
|
||||
return [value.content]
|
||||
}
|
||||
}
|
||||
return genExpression(value, context)
|
||||
}
|
||||
|
|
|
@ -9,13 +9,33 @@ export function genSetText(
|
|||
oper: SetTextIRNode,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const { helper } = context
|
||||
const {
|
||||
helper,
|
||||
block: { dynamicComponents },
|
||||
} = context
|
||||
const { element, values, generated, jsx } = oper
|
||||
const texts = combineValues(values, context, jsx)
|
||||
return [
|
||||
NEWLINE,
|
||||
...genCall(helper('setText'), `${generated ? 'x' : 'n'}${element}`, texts),
|
||||
]
|
||||
|
||||
// if the element is a dynamic component, we need to use `setElementText`
|
||||
// to set the textContent of the VaporFragment's nodes.
|
||||
return dynamicComponents.includes(oper.element)
|
||||
? [
|
||||
NEWLINE,
|
||||
...genCall(
|
||||
helper('setElementText'),
|
||||
`n${element}.nodes`,
|
||||
texts,
|
||||
'true', // isConverted
|
||||
),
|
||||
]
|
||||
: [
|
||||
NEWLINE,
|
||||
...genCall(
|
||||
helper('setText'),
|
||||
`${generated ? 'x' : 'n'}${element}`,
|
||||
texts,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
function combineValues(
|
||||
|
@ -40,6 +60,14 @@ export function genGetTextChild(
|
|||
oper: GetTextChildIRNode,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const {
|
||||
block: { dynamicComponents },
|
||||
} = context
|
||||
|
||||
// if the parent is a dynamic component, don't need to generate a child
|
||||
// because it will use the `setElementText` helper directly.
|
||||
if (dynamicComponents.includes(oper.parent)) return []
|
||||
|
||||
return [
|
||||
NEWLINE,
|
||||
`const x${oper.parent} = ${context.helper('child')}(n${oper.parent})`,
|
||||
|
|
|
@ -7,12 +7,15 @@ export function genVShow(
|
|||
oper: DirectiveIRNode,
|
||||
context: CodegenContext,
|
||||
): CodeFragment[] {
|
||||
const { deferred, element } = oper
|
||||
return [
|
||||
NEWLINE,
|
||||
...genCall(context.helper('applyVShow'), `n${oper.element}`, [
|
||||
deferred ? `deferredApplyVShows.push(() => ` : undefined,
|
||||
...genCall(context.helper('applyVShow'), `n${element}`, [
|
||||
`() => (`,
|
||||
...genExpression(oper.dir.exp!, context),
|
||||
`)`,
|
||||
]),
|
||||
deferred ? `)` : undefined,
|
||||
]
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ export enum IRNodeTypes {
|
|||
|
||||
export interface BaseIRNode {
|
||||
type: IRNodeTypes
|
||||
key?: SimpleExpressionNode | undefined
|
||||
}
|
||||
|
||||
export type CoreHelper = keyof typeof import('packages/runtime-dom/src')
|
||||
|
@ -49,10 +50,12 @@ export interface BlockIRNode extends BaseIRNode {
|
|||
type: IRNodeTypes.BLOCK
|
||||
node: RootNode | TemplateChildNode
|
||||
dynamic: IRDynamicInfo
|
||||
dynamicComponents: number[]
|
||||
tempId: number
|
||||
effect: IREffect[]
|
||||
operation: OperationNode[]
|
||||
returns: number[]
|
||||
hasDeferredVShow: boolean
|
||||
}
|
||||
|
||||
export interface RootIRNode {
|
||||
|
@ -65,6 +68,7 @@ export interface RootIRNode {
|
|||
directive: Set<string>
|
||||
block: BlockIRNode
|
||||
hasTemplateRef: boolean
|
||||
hasForwardedSlot: boolean
|
||||
}
|
||||
|
||||
export interface IfIRNode extends BaseIRNode {
|
||||
|
@ -181,6 +185,7 @@ export interface DirectiveIRNode extends BaseIRNode {
|
|||
builtin?: boolean
|
||||
asset?: boolean
|
||||
modelType?: 'text' | 'dynamic' | 'radio' | 'checkbox' | 'select'
|
||||
deferred?: boolean
|
||||
}
|
||||
|
||||
export interface CreateComponentIRNode extends BaseIRNode {
|
||||
|
@ -195,6 +200,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
|
|||
dynamic?: SimpleExpressionNode
|
||||
parent?: number
|
||||
anchor?: number
|
||||
scopeId?: string | null
|
||||
}
|
||||
|
||||
export interface DeclareOldRefIRNode extends BaseIRNode {
|
||||
|
@ -208,6 +214,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
|
|||
name: SimpleExpressionNode
|
||||
props: IRProps[]
|
||||
fallback?: BlockIRNode
|
||||
forwarded?: boolean
|
||||
parent?: number
|
||||
anchor?: number
|
||||
}
|
||||
|
@ -259,7 +266,9 @@ export interface IRDynamicInfo {
|
|||
children: IRDynamicInfo[]
|
||||
template?: number
|
||||
hasDynamicChild?: boolean
|
||||
dynamicChildOffset?: number
|
||||
operation?: OperationNode
|
||||
needsKey?: boolean
|
||||
}
|
||||
|
||||
export interface IREffect {
|
||||
|
|
|
@ -78,6 +78,7 @@ export class TransformContext<T extends AllNode = AllNode> {
|
|||
|
||||
inVOnce: boolean = false
|
||||
inVFor: number = 0
|
||||
inSlot: boolean = false
|
||||
|
||||
comment: CommentNode[] = []
|
||||
component: Set<string> = this.ir.component
|
||||
|
@ -219,6 +220,7 @@ export function transform(
|
|||
directive: new Set(),
|
||||
block: newBlock(node),
|
||||
hasTemplateRef: false,
|
||||
hasForwardedSlot: false,
|
||||
}
|
||||
|
||||
const context = new TransformContext(ir, node, options)
|
||||
|
|
|
@ -59,7 +59,7 @@ export const transformChildren: NodeTransform = (node, context) => {
|
|||
|
||||
function processDynamicChildren(context: TransformContext<ElementNode>) {
|
||||
let prevDynamics: IRDynamicInfo[] = []
|
||||
let hasStaticTemplate = false
|
||||
let staticCount = 0
|
||||
const children = context.dynamic.children
|
||||
|
||||
for (const [index, child] of children.entries()) {
|
||||
|
@ -69,22 +69,36 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
|
|||
|
||||
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
|
||||
if (prevDynamics.length) {
|
||||
if (hasStaticTemplate) {
|
||||
context.childrenTemplate[index - prevDynamics.length] = `<!>`
|
||||
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
|
||||
const anchor = (prevDynamics[0].anchor = context.increaseId())
|
||||
registerInsertion(prevDynamics, context, anchor)
|
||||
if (staticCount) {
|
||||
// each dynamic child gets its own placeholder node.
|
||||
// this makes it easier to locate the corresponding node during hydration.
|
||||
for (let i = 0; i < prevDynamics.length; i++) {
|
||||
const idx = index - prevDynamics.length + i
|
||||
context.childrenTemplate[idx] = `<!>`
|
||||
const dynamicChild = prevDynamics[i]
|
||||
dynamicChild.flags -= DynamicFlag.NON_TEMPLATE
|
||||
const anchor = (dynamicChild.anchor = context.increaseId())
|
||||
if (
|
||||
dynamicChild.operation &&
|
||||
isBlockOperation(dynamicChild.operation)
|
||||
) {
|
||||
// block types
|
||||
dynamicChild.operation.parent = context.reference()
|
||||
dynamicChild.operation.anchor = anchor
|
||||
}
|
||||
}
|
||||
} else {
|
||||
registerInsertion(prevDynamics, context, -1 /* prepend */)
|
||||
}
|
||||
prevDynamics = []
|
||||
}
|
||||
hasStaticTemplate = true
|
||||
staticCount++
|
||||
}
|
||||
}
|
||||
|
||||
if (prevDynamics.length) {
|
||||
registerInsertion(prevDynamics, context)
|
||||
registerInsertion(prevDynamics, context, undefined)
|
||||
context.dynamic.dynamicChildOffset = staticCount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { isValidHTMLNesting } from '@vue/compiler-dom'
|
||||
import {
|
||||
type AttributeNode,
|
||||
type ComponentNode,
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
createCompilerError,
|
||||
createSimpleExpression,
|
||||
isStaticArgOf,
|
||||
isValidHTMLNesting,
|
||||
} from '@vue/compiler-dom'
|
||||
import {
|
||||
camelize,
|
||||
|
@ -36,7 +36,7 @@ import {
|
|||
type VaporDirectiveNode,
|
||||
} from '../ir'
|
||||
import { EMPTY_EXPRESSION } from './utils'
|
||||
import { findProp } from '../utils'
|
||||
import { findProp, isBuiltInComponent } from '../utils'
|
||||
|
||||
export const isReservedProp: (key: string) => boolean = /*#__PURE__*/ makeMap(
|
||||
// the leading comma is intentional so empty string "" is also included
|
||||
|
@ -122,6 +122,12 @@ function transformComponentElement(
|
|||
asset = false
|
||||
}
|
||||
|
||||
const builtInTag = isBuiltInComponent(tag)
|
||||
if (builtInTag) {
|
||||
tag = builtInTag
|
||||
asset = false
|
||||
}
|
||||
|
||||
const dotIndex = tag.indexOf('.')
|
||||
if (dotIndex > 0) {
|
||||
const ns = resolveSetupReference(tag.slice(0, dotIndex), context)
|
||||
|
@ -153,6 +159,7 @@ function transformComponentElement(
|
|||
root: singleRoot && !context.inVFor,
|
||||
slots: [...context.slots],
|
||||
once: context.inVOnce,
|
||||
scopeId: context.inSlot ? context.options.scopeId : undefined,
|
||||
dynamic: dynamicComponent,
|
||||
}
|
||||
context.slots = []
|
||||
|
@ -437,7 +444,9 @@ function dedupeProperties(results: DirectiveTransformResult[]): IRProp[] {
|
|||
}
|
||||
const name = prop.key.content
|
||||
const existing = knownProps.get(name)
|
||||
if (existing) {
|
||||
// prop names and event handler names can be the same but serve different purposes
|
||||
// e.g. `:appear="true"` is a prop while `@appear="handler"` is an event handler
|
||||
if (existing && existing.handler === prop.handler) {
|
||||
if (name === 'style' || name === 'class') {
|
||||
mergePropValues(existing, prop)
|
||||
}
|
||||
|
|
|
@ -99,6 +99,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
|
|||
}
|
||||
|
||||
return () => {
|
||||
if (context.inSlot) context.ir.hasForwardedSlot = true
|
||||
exitBlock && exitBlock()
|
||||
context.dynamic.operation = {
|
||||
type: IRNodeTypes.SLOT_OUTLET_NODE,
|
||||
|
@ -106,6 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
|
|||
name: slotName,
|
||||
props: irProps,
|
||||
fallback,
|
||||
forwarded: context.inSlot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import type { NodeTransform } from '@vue/compiler-vapor'
|
||||
import { findDir, isTransitionTag } from '../utils'
|
||||
import {
|
||||
type ElementNode,
|
||||
ElementTypes,
|
||||
NodeTypes,
|
||||
isTemplateNode,
|
||||
postTransformTransition,
|
||||
} from '@vue/compiler-dom'
|
||||
|
||||
export const transformTransition: NodeTransform = (node, context) => {
|
||||
if (
|
||||
node.type === NodeTypes.ELEMENT &&
|
||||
node.tagType === ElementTypes.COMPONENT
|
||||
) {
|
||||
if (isTransitionTag(node.tag)) {
|
||||
return postTransformTransition(
|
||||
node,
|
||||
context.options.onError,
|
||||
hasMultipleChildren,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasMultipleChildren(node: ElementNode): boolean {
|
||||
const children = (node.children = node.children.filter(
|
||||
c =>
|
||||
c.type !== NodeTypes.COMMENT &&
|
||||
!(c.type === NodeTypes.TEXT && !c.content.trim()),
|
||||
))
|
||||
|
||||
const first = children[0]
|
||||
|
||||
// has v-for
|
||||
if (
|
||||
children.length === 1 &&
|
||||
first.type === NodeTypes.ELEMENT &&
|
||||
(findDir(first, 'for') || isTemplateNode(first))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
const hasElse = (node: ElementNode) =>
|
||||
findDir(node, 'else-if') || findDir(node, 'else', true)
|
||||
|
||||
// has v-if/v-else-if/v-else
|
||||
if (
|
||||
children.every(
|
||||
(c, index) =>
|
||||
c.type === NodeTypes.ELEMENT &&
|
||||
// not template
|
||||
!isTemplateNode(c) &&
|
||||
// not has v-for
|
||||
!findDir(c, 'for') &&
|
||||
// if the first child has v-if, the rest should also have v-else-if/v-else
|
||||
(index === 0 ? findDir(c, 'if') : hasElse(c)) &&
|
||||
!hasMultipleChildren(c),
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return children.length > 1
|
||||
}
|
|
@ -26,10 +26,12 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
|
|||
type: IRNodeTypes.BLOCK,
|
||||
node,
|
||||
dynamic: newDynamic(),
|
||||
dynamicComponents: [],
|
||||
effect: [],
|
||||
operation: [],
|
||||
returns: [],
|
||||
tempId: 0,
|
||||
hasDeferredVShow: false,
|
||||
})
|
||||
|
||||
export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
import { extend } from '@vue/shared'
|
||||
import { newBlock, wrapTemplate } from './utils'
|
||||
import { getSiblingIf } from './transformComment'
|
||||
import { isStaticExpression } from '../utils'
|
||||
import { isInTransition, isStaticExpression } from '../utils'
|
||||
|
||||
export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
|
||||
['if', 'else', 'else-if'],
|
||||
|
@ -135,5 +135,8 @@ export function createIfBranch(
|
|||
const branch: BlockIRNode = newBlock(node)
|
||||
const exitBlock = context.enterBlock(branch)
|
||||
context.reference()
|
||||
// generate key for branch result when it's in transition
|
||||
// the key will be used to track node leaving at runtime
|
||||
branch.dynamic.needsKey = isInTransition(context)
|
||||
return [branch, exitBlock]
|
||||
}
|
||||
|
|
|
@ -2,11 +2,13 @@ import {
|
|||
DOMErrorCodes,
|
||||
ElementTypes,
|
||||
ErrorCodes,
|
||||
NodeTypes,
|
||||
createCompilerError,
|
||||
createDOMCompilerError,
|
||||
} from '@vue/compiler-dom'
|
||||
import type { DirectiveTransform } from '../transform'
|
||||
import { IRNodeTypes } from '../ir'
|
||||
import { findProp, isTransitionTag } from '../utils'
|
||||
|
||||
export const transformVShow: DirectiveTransform = (dir, node, context) => {
|
||||
const { exp, loc } = dir
|
||||
|
@ -27,11 +29,26 @@ export const transformVShow: DirectiveTransform = (dir, node, context) => {
|
|||
return
|
||||
}
|
||||
|
||||
// lazy apply vshow if the node is inside a transition with appear
|
||||
let shouldDeferred = false
|
||||
const parentNode = context.parent && context.parent.node
|
||||
if (parentNode && parentNode.type === NodeTypes.ELEMENT) {
|
||||
shouldDeferred = !!(
|
||||
isTransitionTag(parentNode.tag) &&
|
||||
findProp(parentNode, 'appear', false, true)
|
||||
)
|
||||
|
||||
if (shouldDeferred) {
|
||||
context.parent!.parent!.block.hasDeferredVShow = true
|
||||
}
|
||||
}
|
||||
|
||||
context.registerOperation({
|
||||
type: IRNodeTypes.DIRECTIVE,
|
||||
element: context.reference(),
|
||||
dir,
|
||||
name: 'show',
|
||||
builtin: true,
|
||||
deferred: shouldDeferred,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -23,7 +23,12 @@ import {
|
|||
type SlotBlockIRNode,
|
||||
type VaporDirectiveNode,
|
||||
} from '../ir'
|
||||
import { findDir, resolveExpression } from '../utils'
|
||||
import {
|
||||
findDir,
|
||||
findProp,
|
||||
isTransitionNode,
|
||||
resolveExpression,
|
||||
} from '../utils'
|
||||
import { markNonTemplate } from './transformText'
|
||||
|
||||
export const transformVSlot: NodeTransform = (node, context) => {
|
||||
|
@ -67,7 +72,6 @@ function transformComponentSlot(
|
|||
) {
|
||||
const { children } = node
|
||||
const arg = dir && dir.arg
|
||||
|
||||
// whitespace: 'preserve'
|
||||
const emptyTextNodes: TemplateChildNode[] = []
|
||||
const nonSlotTemplateChildren = children.filter(n => {
|
||||
|
@ -83,7 +87,18 @@ function transformComponentSlot(
|
|||
})
|
||||
}
|
||||
|
||||
const [block, onExit] = createSlotBlock(node, dir, context)
|
||||
let slotKey
|
||||
if (isTransitionNode(node) && nonSlotTemplateChildren.length) {
|
||||
const keyProp = findProp(
|
||||
nonSlotTemplateChildren[0] as ElementNode,
|
||||
'key',
|
||||
) as VaporDirectiveNode
|
||||
if (keyProp) {
|
||||
slotKey = keyProp.exp
|
||||
}
|
||||
}
|
||||
|
||||
const [block, onExit] = createSlotBlock(node, dir, context, slotKey)
|
||||
|
||||
const { slots } = context
|
||||
|
||||
|
@ -244,11 +259,23 @@ function createSlotBlock(
|
|||
slotNode: ElementNode,
|
||||
dir: VaporDirectiveNode | undefined,
|
||||
context: TransformContext<ElementNode>,
|
||||
key: SimpleExpressionNode | undefined = undefined,
|
||||
): [SlotBlockIRNode, () => void] {
|
||||
const block: SlotBlockIRNode = newBlock(slotNode)
|
||||
block.props = dir && dir.exp
|
||||
if (key) {
|
||||
block.key = key
|
||||
block.dynamic.needsKey = true
|
||||
}
|
||||
const exitBlock = context.enterBlock(block)
|
||||
return [block, exitBlock]
|
||||
context.inSlot = true
|
||||
return [
|
||||
block,
|
||||
() => {
|
||||
context.inSlot = false
|
||||
exitBlock()
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function isNonWhitespaceContent(node: TemplateChildNode): boolean {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '@vue/compiler-dom'
|
||||
import type { VaporDirectiveNode } from './ir'
|
||||
import { EMPTY_EXPRESSION } from './transforms/utils'
|
||||
import type { TransformContext } from './transform'
|
||||
|
||||
export const findProp = _findProp as (
|
||||
node: ElementNode,
|
||||
|
@ -88,3 +89,43 @@ export function getLiteralExpressionValue(
|
|||
}
|
||||
return exp.isStatic ? exp.content : null
|
||||
}
|
||||
|
||||
export function isInTransition(
|
||||
context: TransformContext<ElementNode>,
|
||||
): boolean {
|
||||
const parentNode = context.parent && context.parent.node
|
||||
return !!(parentNode && isTransitionNode(parentNode as ElementNode))
|
||||
}
|
||||
|
||||
export function isTransitionNode(node: ElementNode): boolean {
|
||||
return node.type === NodeTypes.ELEMENT && isTransitionTag(node.tag)
|
||||
}
|
||||
|
||||
export function isTransitionGroupNode(node: ElementNode): boolean {
|
||||
return node.type === NodeTypes.ELEMENT && isTransitionGroupTag(node.tag)
|
||||
}
|
||||
|
||||
export function isTransitionTag(tag: string): boolean {
|
||||
tag = tag.toLowerCase()
|
||||
return tag === 'transition' || tag === 'vaportransition'
|
||||
}
|
||||
|
||||
export function isTransitionGroupTag(tag: string): boolean {
|
||||
tag = tag.toLowerCase().replace(/-/g, '')
|
||||
return tag === 'transitiongroup' || tag === 'vaportransitiongroup'
|
||||
}
|
||||
|
||||
export function isTeleportTag(tag: string): boolean {
|
||||
tag = tag.toLowerCase()
|
||||
return tag === 'teleport' || tag === 'vaporteleport'
|
||||
}
|
||||
|
||||
export function isBuiltInComponent(tag: string): string | undefined {
|
||||
if (isTransitionTag(tag)) {
|
||||
return 'VaporTransition'
|
||||
} else if (isTransitionGroupTag(tag)) {
|
||||
return 'VaporTransitionGroup'
|
||||
} else if (isTeleportTag(tag)) {
|
||||
return 'VaporTeleport'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -601,14 +601,14 @@ describe('SSR hydration', () => {
|
|||
const ctx: SSRContext = {}
|
||||
container.innerHTML = await renderToString(h(App), ctx)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--teleport start--><!--teleport end--></div>',
|
||||
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
|
||||
)
|
||||
teleportContainer.innerHTML = ctx.teleports!['#target']
|
||||
|
||||
// hydrate
|
||||
createSSRApp(App).mount(container)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--teleport start--><!--teleport end--></div>',
|
||||
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
|
||||
)
|
||||
expect(teleportContainer.innerHTML).toBe(
|
||||
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
|
||||
|
@ -617,7 +617,7 @@ describe('SSR hydration', () => {
|
|||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
|
||||
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
|
||||
expect(teleportContainer.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
|
@ -660,21 +660,21 @@ describe('SSR hydration', () => {
|
|||
// server render
|
||||
container.innerHTML = await renderToString(h(App))
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--teleport start--><!--teleport end--></div>',
|
||||
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
|
||||
)
|
||||
expect(teleportContainer.innerHTML).toBe('')
|
||||
|
||||
// hydrate
|
||||
createSSRApp(App).mount(container)
|
||||
expect(container.innerHTML).toBe(
|
||||
'<div><!--teleport start--><!--teleport end--></div>',
|
||||
'<div><!--teleport start--><!--teleport end--><!--if--></div>',
|
||||
)
|
||||
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
|
||||
expect(`Hydration children mismatch`).toHaveBeenWarned()
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
|
||||
expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
|
||||
expect(teleportContainer.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
|
@ -1846,6 +1846,36 @@ describe('SSR hydration', () => {
|
|||
}
|
||||
})
|
||||
|
||||
describe('dynamic anchor', () => {
|
||||
test('two consecutive components', () => {
|
||||
const Comp = {
|
||||
render() {
|
||||
return createTextVNode('foo')
|
||||
},
|
||||
}
|
||||
const { vnode, container } = mountWithHydration(
|
||||
`<div><span></span>foo<!--[[-->foo<!--]]--><span></span></div>`,
|
||||
() => h('div', null, [h('span'), h(Comp), h(Comp), h('span')]),
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('multiple consecutive components', () => {
|
||||
const Comp = {
|
||||
render() {
|
||||
return createTextVNode('foo')
|
||||
},
|
||||
}
|
||||
const { vnode, container } = mountWithHydration(
|
||||
`<div><span></span>foo<!--[[-->foo<!--]]-->foo<span></span></div>`,
|
||||
() => h('div', null, [h('span'), h(Comp), h(Comp), h(Comp), h('span')]),
|
||||
)
|
||||
expect(vnode.el).toBe(container.firstChild)
|
||||
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
|
||||
})
|
||||
})
|
||||
|
||||
test('hmr reload child wrapped in KeepAlive', async () => {
|
||||
const id = 'child-reload'
|
||||
const Child = {
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { ComponentPublicInstance } from './componentPublicInstance'
|
|||
import { type VNode, createVNode } from './vnode'
|
||||
import { defineComponent } from './apiDefineComponent'
|
||||
import { warn } from './warning'
|
||||
import { ref } from '@vue/reactivity'
|
||||
import { type Ref, ref } from '@vue/reactivity'
|
||||
import { ErrorCodes, handleError } from './errorHandling'
|
||||
import { isKeepAlive } from './components/KeepAlive'
|
||||
import { markAsyncBoundary } from './helpers/useId'
|
||||
|
@ -24,10 +24,10 @@ export type AsyncComponentLoader<T = any> = () => Promise<
|
|||
AsyncComponentResolveResult<T>
|
||||
>
|
||||
|
||||
export interface AsyncComponentOptions<T = any> {
|
||||
export interface AsyncComponentOptions<T = any, C = any> {
|
||||
loader: AsyncComponentLoader<T>
|
||||
loadingComponent?: Component
|
||||
errorComponent?: Component
|
||||
loadingComponent?: C
|
||||
errorComponent?: C
|
||||
delay?: number
|
||||
timeout?: number
|
||||
suspensible?: boolean
|
||||
|
@ -46,75 +46,20 @@ export const isAsyncWrapper = (i: GenericComponentInstance | VNode): boolean =>
|
|||
/*! #__NO_SIDE_EFFECTS__ */
|
||||
export function defineAsyncComponent<
|
||||
T extends Component = { new (): ComponentPublicInstance },
|
||||
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
|
||||
if (isFunction(source)) {
|
||||
source = { loader: source }
|
||||
}
|
||||
|
||||
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T, Component>): T {
|
||||
const {
|
||||
loader,
|
||||
loadingComponent,
|
||||
errorComponent,
|
||||
delay = 200,
|
||||
hydrate: hydrateStrategy,
|
||||
timeout, // undefined = never times out
|
||||
suspensible = true,
|
||||
onError: userOnError,
|
||||
} = source
|
||||
|
||||
let pendingRequest: Promise<ConcreteComponent> | null = null
|
||||
let resolvedComp: ConcreteComponent | undefined
|
||||
|
||||
let retries = 0
|
||||
const retry = () => {
|
||||
retries++
|
||||
pendingRequest = null
|
||||
return load()
|
||||
}
|
||||
|
||||
const load = (): Promise<ConcreteComponent> => {
|
||||
let thisRequest: Promise<ConcreteComponent>
|
||||
return (
|
||||
pendingRequest ||
|
||||
(thisRequest = pendingRequest =
|
||||
loader()
|
||||
.catch(err => {
|
||||
err = err instanceof Error ? err : new Error(String(err))
|
||||
if (userOnError) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const userRetry = () => resolve(retry())
|
||||
const userFail = () => reject(err)
|
||||
userOnError(err, userRetry, userFail, retries + 1)
|
||||
})
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
.then((comp: any) => {
|
||||
if (thisRequest !== pendingRequest && pendingRequest) {
|
||||
return pendingRequest
|
||||
}
|
||||
if (__DEV__ && !comp) {
|
||||
warn(
|
||||
`Async component loader resolved to undefined. ` +
|
||||
`If you are using retry(), make sure to return its return value.`,
|
||||
)
|
||||
}
|
||||
// interop module default
|
||||
if (
|
||||
comp &&
|
||||
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
|
||||
) {
|
||||
comp = comp.default
|
||||
}
|
||||
if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
|
||||
throw new Error(`Invalid async component load result: ${comp}`)
|
||||
}
|
||||
resolvedComp = comp
|
||||
return comp
|
||||
}))
|
||||
)
|
||||
}
|
||||
load,
|
||||
getResolvedComp,
|
||||
setPendingRequest,
|
||||
source: {
|
||||
loadingComponent,
|
||||
errorComponent,
|
||||
delay,
|
||||
hydrate: hydrateStrategy,
|
||||
timeout,
|
||||
suspensible = true,
|
||||
},
|
||||
} = createAsyncComponentContext(source)
|
||||
|
||||
return defineComponent({
|
||||
name: 'AsyncComponentWrapper',
|
||||
|
@ -132,7 +77,7 @@ export function defineAsyncComponent<
|
|||
}
|
||||
}
|
||||
: hydrate
|
||||
if (resolvedComp) {
|
||||
if (getResolvedComp()) {
|
||||
doHydrate()
|
||||
} else {
|
||||
load().then(() => !instance.isUnmounted && doHydrate())
|
||||
|
@ -140,7 +85,7 @@ export function defineAsyncComponent<
|
|||
},
|
||||
|
||||
get __asyncResolved() {
|
||||
return resolvedComp
|
||||
return getResolvedComp()
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
@ -148,12 +93,13 @@ export function defineAsyncComponent<
|
|||
markAsyncBoundary(instance)
|
||||
|
||||
// already resolved
|
||||
let resolvedComp = getResolvedComp()
|
||||
if (resolvedComp) {
|
||||
return () => createInnerComp(resolvedComp!, instance)
|
||||
}
|
||||
|
||||
const onError = (err: Error) => {
|
||||
pendingRequest = null
|
||||
setPendingRequest(null)
|
||||
handleError(
|
||||
err,
|
||||
instance,
|
||||
|
@ -182,27 +128,11 @@ export function defineAsyncComponent<
|
|||
})
|
||||
}
|
||||
|
||||
const loaded = ref(false)
|
||||
const error = ref()
|
||||
const delayed = ref(!!delay)
|
||||
|
||||
if (delay) {
|
||||
setTimeout(() => {
|
||||
delayed.value = false
|
||||
}, delay)
|
||||
}
|
||||
|
||||
if (timeout != null) {
|
||||
setTimeout(() => {
|
||||
if (!loaded.value && !error.value) {
|
||||
const err = new Error(
|
||||
`Async component timed out after ${timeout}ms.`,
|
||||
)
|
||||
onError(err)
|
||||
error.value = err
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
const { loaded, error, delayed } = useAsyncComponentState(
|
||||
delay,
|
||||
timeout,
|
||||
onError,
|
||||
)
|
||||
|
||||
load()
|
||||
.then(() => {
|
||||
|
@ -223,6 +153,7 @@ export function defineAsyncComponent<
|
|||
})
|
||||
|
||||
return () => {
|
||||
resolvedComp = getResolvedComp()
|
||||
if (loaded.value && resolvedComp) {
|
||||
return createInnerComp(resolvedComp, instance)
|
||||
} else if (error.value && errorComponent) {
|
||||
|
@ -252,3 +183,114 @@ function createInnerComp(
|
|||
|
||||
return vnode
|
||||
}
|
||||
|
||||
type AsyncComponentContext<T, C = ConcreteComponent> = {
|
||||
load: () => Promise<C>
|
||||
source: AsyncComponentOptions<T>
|
||||
getResolvedComp: () => C | undefined
|
||||
setPendingRequest: (request: Promise<C> | null) => void
|
||||
}
|
||||
|
||||
// shared between core and vapor
|
||||
export function createAsyncComponentContext<T, C = ConcreteComponent>(
|
||||
source: AsyncComponentLoader<T> | AsyncComponentOptions<T>,
|
||||
): AsyncComponentContext<T, C> {
|
||||
if (isFunction(source)) {
|
||||
source = { loader: source }
|
||||
}
|
||||
|
||||
const { loader, onError: userOnError } = source
|
||||
let pendingRequest: Promise<C> | null = null
|
||||
let resolvedComp: C | undefined
|
||||
|
||||
let retries = 0
|
||||
const retry = () => {
|
||||
retries++
|
||||
pendingRequest = null
|
||||
return load()
|
||||
}
|
||||
|
||||
const load = (): Promise<C> => {
|
||||
let thisRequest: Promise<C>
|
||||
return (
|
||||
pendingRequest ||
|
||||
(thisRequest = pendingRequest =
|
||||
loader()
|
||||
.catch(err => {
|
||||
err = err instanceof Error ? err : new Error(String(err))
|
||||
if (userOnError) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const userRetry = () => resolve(retry())
|
||||
const userFail = () => reject(err)
|
||||
userOnError(err, userRetry, userFail, retries + 1)
|
||||
})
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
.then((comp: any) => {
|
||||
if (thisRequest !== pendingRequest && pendingRequest) {
|
||||
return pendingRequest
|
||||
}
|
||||
if (__DEV__ && !comp) {
|
||||
warn(
|
||||
`Async component loader resolved to undefined. ` +
|
||||
`If you are using retry(), make sure to return its return value.`,
|
||||
)
|
||||
}
|
||||
if (
|
||||
comp &&
|
||||
(comp.__esModule || comp[Symbol.toStringTag] === 'Module')
|
||||
) {
|
||||
comp = comp.default
|
||||
}
|
||||
if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) {
|
||||
throw new Error(`Invalid async component load result: ${comp}`)
|
||||
}
|
||||
resolvedComp = comp
|
||||
return comp
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
source,
|
||||
getResolvedComp: () => resolvedComp,
|
||||
setPendingRequest: (request: Promise<C> | null) =>
|
||||
(pendingRequest = request),
|
||||
}
|
||||
}
|
||||
|
||||
// shared between core and vapor
|
||||
export const useAsyncComponentState = (
|
||||
delay: number | undefined,
|
||||
timeout: number | undefined,
|
||||
onError: (err: Error) => void,
|
||||
): {
|
||||
loaded: Ref<boolean>
|
||||
error: Ref<Error | undefined>
|
||||
delayed: Ref<boolean>
|
||||
} => {
|
||||
const loaded = ref(false)
|
||||
const error = ref()
|
||||
const delayed = ref(!!delay)
|
||||
|
||||
if (delay) {
|
||||
setTimeout(() => {
|
||||
delayed.value = false
|
||||
}, delay)
|
||||
}
|
||||
|
||||
if (timeout != null) {
|
||||
setTimeout(() => {
|
||||
if (!loaded.value && !error.value) {
|
||||
const err = new Error(`Async component timed out after ${timeout}ms.`)
|
||||
onError(err)
|
||||
error.value = err
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
return { loaded, error, delayed }
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import { warn } from './warning'
|
|||
import type { VNode } from './vnode'
|
||||
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
|
||||
import { NO, extend, isFunction, isObject } from '@vue/shared'
|
||||
import { version } from '.'
|
||||
import { type TransitionHooks, version } from '.'
|
||||
import { installAppCompatProperties } from './compat/global'
|
||||
import type { NormalizedPropsOptions } from './componentProps'
|
||||
import type { ObjectEmitsOptions } from './componentEmits'
|
||||
|
@ -174,7 +174,6 @@ export interface AppConfig extends GenericAppConfig {
|
|||
|
||||
/**
|
||||
* The vapor in vdom implementation is in runtime-vapor/src/vdomInterop.ts
|
||||
* @internal
|
||||
*/
|
||||
export interface VaporInteropInterface {
|
||||
mount(
|
||||
|
@ -187,8 +186,18 @@ export interface VaporInteropInterface {
|
|||
unmount(vnode: VNode, doRemove?: boolean): void
|
||||
move(vnode: VNode, container: any, anchor: any): void
|
||||
slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
|
||||
setTransitionHooks(
|
||||
component: ComponentInternalInstance,
|
||||
transition: TransitionHooks,
|
||||
): void
|
||||
hydrate(node: Node, fn: () => void): void
|
||||
|
||||
vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
|
||||
vdomMount: (
|
||||
component: ConcreteComponent,
|
||||
props?: any,
|
||||
slots?: any,
|
||||
scopeId?: string,
|
||||
) => any
|
||||
vdomUnmount: UnmountComponentFn
|
||||
vdomSlot: (
|
||||
slots: any,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
type ComponentInternalInstance,
|
||||
type ComponentOptions,
|
||||
type ConcreteComponent,
|
||||
type GenericComponentInstance,
|
||||
type SetupContext,
|
||||
getCurrentInstance,
|
||||
} from '../component'
|
||||
|
@ -19,12 +21,12 @@ import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
|
|||
import { PatchFlags, ShapeFlags, isArray, isFunction } from '@vue/shared'
|
||||
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
|
||||
import { isTeleport } from './Teleport'
|
||||
import type { RendererElement } from '../renderer'
|
||||
import { type RendererElement, getVaporInterface } from '../renderer'
|
||||
import { SchedulerJobFlags } from '../scheduler'
|
||||
|
||||
type Hook<T = () => void> = T | T[]
|
||||
|
||||
const leaveCbKey: unique symbol = Symbol('_leaveCb')
|
||||
export const leaveCbKey: unique symbol = Symbol('_leaveCb')
|
||||
const enterCbKey: unique symbol = Symbol('_enterCb')
|
||||
|
||||
export interface BaseTransitionProps<HostElement = RendererElement> {
|
||||
|
@ -87,7 +89,7 @@ export interface TransitionState {
|
|||
isUnmounting: boolean
|
||||
// Track pending leave callbacks for children of the same key.
|
||||
// This is used to force remove leaving a child when a new copy is entering.
|
||||
leavingVNodes: Map<any, Record<string, VNode>>
|
||||
leavingNodes: Map<any, Record<string, any>>
|
||||
}
|
||||
|
||||
export interface TransitionElement {
|
||||
|
@ -103,7 +105,7 @@ export function useTransitionState(): TransitionState {
|
|||
isMounted: false,
|
||||
isLeaving: false,
|
||||
isUnmounting: false,
|
||||
leavingVNodes: new Map(),
|
||||
leavingNodes: new Map(),
|
||||
}
|
||||
onMounted(() => {
|
||||
state.isMounted = true
|
||||
|
@ -138,7 +140,9 @@ export const BaseTransitionPropsValidators: Record<string, any> = {
|
|||
}
|
||||
|
||||
const recursiveGetSubtree = (instance: ComponentInternalInstance): VNode => {
|
||||
const subTree = instance.subTree
|
||||
const subTree = instance.type.__vapor
|
||||
? (instance as any).block
|
||||
: instance.subTree
|
||||
return subTree.component ? recursiveGetSubtree(subTree.component) : subTree
|
||||
}
|
||||
|
||||
|
@ -164,15 +168,7 @@ const BaseTransitionImpl: ComponentOptions = {
|
|||
const rawProps = toRaw(props)
|
||||
const { mode } = rawProps
|
||||
// check mode
|
||||
if (
|
||||
__DEV__ &&
|
||||
mode &&
|
||||
mode !== 'in-out' &&
|
||||
mode !== 'out-in' &&
|
||||
mode !== 'default'
|
||||
) {
|
||||
warn(`invalid <transition> mode: ${mode}`)
|
||||
}
|
||||
checkTransitionMode(mode)
|
||||
|
||||
if (state.isLeaving) {
|
||||
return emptyPlaceholder(child)
|
||||
|
@ -309,24 +305,83 @@ function getLeavingNodesForType(
|
|||
state: TransitionState,
|
||||
vnode: VNode,
|
||||
): Record<string, VNode> {
|
||||
const { leavingVNodes } = state
|
||||
let leavingVNodesCache = leavingVNodes.get(vnode.type)!
|
||||
const { leavingNodes } = state
|
||||
let leavingVNodesCache = leavingNodes.get(vnode.type)!
|
||||
if (!leavingVNodesCache) {
|
||||
leavingVNodesCache = Object.create(null)
|
||||
leavingVNodes.set(vnode.type, leavingVNodesCache)
|
||||
leavingNodes.set(vnode.type, leavingVNodesCache)
|
||||
}
|
||||
return leavingVNodesCache
|
||||
}
|
||||
|
||||
export interface TransitionHooksContext {
|
||||
setLeavingNodeCache: (node: any) => void
|
||||
unsetLeavingNodeCache: (node: any) => void
|
||||
earlyRemove: () => void
|
||||
cloneHooks: (node: any) => TransitionHooks
|
||||
}
|
||||
|
||||
// The transition hooks are attached to the vnode as vnode.transition
|
||||
// and will be called at appropriate timing in the renderer.
|
||||
export function resolveTransitionHooks(
|
||||
vnode: VNode,
|
||||
props: BaseTransitionProps<any>,
|
||||
state: TransitionState,
|
||||
instance: ComponentInternalInstance,
|
||||
instance: GenericComponentInstance,
|
||||
postClone?: (hooks: TransitionHooks) => void,
|
||||
): TransitionHooks {
|
||||
const key = String(vnode.key)
|
||||
const leavingVNodesCache = getLeavingNodesForType(state, vnode)
|
||||
const context: TransitionHooksContext = {
|
||||
setLeavingNodeCache: () => {
|
||||
leavingVNodesCache[key] = vnode
|
||||
},
|
||||
unsetLeavingNodeCache: () => {
|
||||
if (leavingVNodesCache[key] === vnode) {
|
||||
delete leavingVNodesCache[key]
|
||||
}
|
||||
},
|
||||
earlyRemove: () => {
|
||||
const leavingVNode = leavingVNodesCache[key]
|
||||
if (
|
||||
leavingVNode &&
|
||||
isSameVNodeType(vnode, leavingVNode) &&
|
||||
(leavingVNode.el as TransitionElement)[leaveCbKey]
|
||||
) {
|
||||
// force early removal (not cancelled)
|
||||
;(leavingVNode.el as TransitionElement)[leaveCbKey]!()
|
||||
}
|
||||
},
|
||||
cloneHooks: vnode => {
|
||||
const hooks = resolveTransitionHooks(
|
||||
vnode,
|
||||
props,
|
||||
state,
|
||||
instance,
|
||||
postClone,
|
||||
)
|
||||
if (postClone) postClone(hooks)
|
||||
return hooks
|
||||
},
|
||||
}
|
||||
|
||||
return baseResolveTransitionHooks(context, props, state, instance)
|
||||
}
|
||||
|
||||
// shared between vdom and vapor
|
||||
export function baseResolveTransitionHooks(
|
||||
context: TransitionHooksContext,
|
||||
props: BaseTransitionProps<any>,
|
||||
state: TransitionState,
|
||||
instance: GenericComponentInstance,
|
||||
): TransitionHooks {
|
||||
const {
|
||||
setLeavingNodeCache,
|
||||
unsetLeavingNodeCache,
|
||||
earlyRemove,
|
||||
cloneHooks,
|
||||
} = context
|
||||
|
||||
const {
|
||||
appear,
|
||||
mode,
|
||||
|
@ -344,8 +399,6 @@ export function resolveTransitionHooks(
|
|||
onAfterAppear,
|
||||
onAppearCancelled,
|
||||
} = props
|
||||
const key = String(vnode.key)
|
||||
const leavingVNodesCache = getLeavingNodesForType(state, vnode)
|
||||
|
||||
const callHook: TransitionHookCaller = (hook, args) => {
|
||||
hook &&
|
||||
|
@ -387,15 +440,7 @@ export function resolveTransitionHooks(
|
|||
el[leaveCbKey](true /* cancelled */)
|
||||
}
|
||||
// for toggled element with same key (v-if)
|
||||
const leavingVNode = leavingVNodesCache[key]
|
||||
if (
|
||||
leavingVNode &&
|
||||
isSameVNodeType(vnode, leavingVNode) &&
|
||||
(leavingVNode.el as TransitionElement)[leaveCbKey]
|
||||
) {
|
||||
// force early removal (not cancelled)
|
||||
;(leavingVNode.el as TransitionElement)[leaveCbKey]!()
|
||||
}
|
||||
earlyRemove()
|
||||
callHook(hook, [el])
|
||||
},
|
||||
|
||||
|
@ -434,7 +479,7 @@ export function resolveTransitionHooks(
|
|||
},
|
||||
|
||||
leave(el, remove) {
|
||||
const key = String(vnode.key)
|
||||
// const key = String(vnode.key)
|
||||
if (el[enterCbKey]) {
|
||||
el[enterCbKey](true /* cancelled */)
|
||||
}
|
||||
|
@ -453,11 +498,9 @@ export function resolveTransitionHooks(
|
|||
callHook(onAfterLeave, [el])
|
||||
}
|
||||
el[leaveCbKey] = undefined
|
||||
if (leavingVNodesCache[key] === vnode) {
|
||||
delete leavingVNodesCache[key]
|
||||
}
|
||||
unsetLeavingNodeCache(el)
|
||||
})
|
||||
leavingVNodesCache[key] = vnode
|
||||
setLeavingNodeCache(el)
|
||||
if (onLeave) {
|
||||
callAsyncHook(onLeave, [el, done])
|
||||
} else {
|
||||
|
@ -465,16 +508,8 @@ export function resolveTransitionHooks(
|
|||
}
|
||||
},
|
||||
|
||||
clone(vnode) {
|
||||
const hooks = resolveTransitionHooks(
|
||||
vnode,
|
||||
props,
|
||||
state,
|
||||
instance,
|
||||
postClone,
|
||||
)
|
||||
if (postClone) postClone(hooks)
|
||||
return hooks
|
||||
clone(node) {
|
||||
return cloneHooks(node)
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -524,8 +559,15 @@ function getInnerChild(vnode: VNode): VNode | undefined {
|
|||
|
||||
export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void {
|
||||
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
|
||||
vnode.transition = hooks
|
||||
setTransitionHooks(vnode.component.subTree, hooks)
|
||||
if ((vnode.type as ConcreteComponent).__vapor) {
|
||||
getVaporInterface(vnode.component, vnode).setTransitionHooks(
|
||||
vnode.component,
|
||||
hooks,
|
||||
)
|
||||
} else {
|
||||
vnode.transition = hooks
|
||||
setTransitionHooks(vnode.component.subTree, hooks)
|
||||
}
|
||||
} else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
|
||||
vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
|
||||
vnode.ssFallback!.transition = hooks.clone(vnode.ssFallback!)
|
||||
|
@ -571,3 +613,18 @@ export function getTransitionRawChildren(
|
|||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* dev-only
|
||||
*/
|
||||
export function checkTransitionMode(mode: string | undefined): void {
|
||||
if (
|
||||
__DEV__ &&
|
||||
mode &&
|
||||
mode !== 'in-out' &&
|
||||
mode !== 'out-in' &&
|
||||
mode !== 'default'
|
||||
) {
|
||||
warn(`invalid <transition> mode: ${mode}`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,10 +27,10 @@ export const TeleportEndKey: unique symbol = Symbol('_vte')
|
|||
|
||||
export const isTeleport = (type: any): boolean => type.__isTeleport
|
||||
|
||||
const isTeleportDisabled = (props: VNode['props']): boolean =>
|
||||
export const isTeleportDisabled = (props: VNode['props']): boolean =>
|
||||
props && (props.disabled || props.disabled === '')
|
||||
|
||||
const isTeleportDeferred = (props: VNode['props']): boolean =>
|
||||
export const isTeleportDeferred = (props: VNode['props']): boolean =>
|
||||
props && (props.defer || props.defer === '')
|
||||
|
||||
const isTargetSVG = (target: RendererElement): boolean =>
|
||||
|
@ -39,7 +39,7 @@ const isTargetSVG = (target: RendererElement): boolean =>
|
|||
const isTargetMathML = (target: RendererElement): boolean =>
|
||||
typeof MathMLElement === 'function' && target instanceof MathMLElement
|
||||
|
||||
const resolveTarget = <T = RendererElement>(
|
||||
export const resolveTarget = <T = RendererElement>(
|
||||
props: TeleportProps | null,
|
||||
select: RendererOptions['querySelector'],
|
||||
): T | null => {
|
||||
|
|
|
@ -81,6 +81,10 @@ export function renderSlot(
|
|||
}
|
||||
openBlock()
|
||||
const validSlotContent = slot && ensureValidVNode(slot(props))
|
||||
|
||||
// handle forwarded vapor slot fallback
|
||||
ensureVaporSlotFallback(validSlotContent, fallback)
|
||||
|
||||
const slotKey =
|
||||
props.key ||
|
||||
// slot content array of a dynamic conditional slot may have a branch
|
||||
|
@ -124,3 +128,20 @@ export function ensureValidVNode(
|
|||
? vnodes
|
||||
: null
|
||||
}
|
||||
|
||||
export function ensureVaporSlotFallback(
|
||||
vnodes: VNodeArrayChildren | null | undefined,
|
||||
fallback?: () => VNodeArrayChildren,
|
||||
): void {
|
||||
let vaporSlot: any
|
||||
if (
|
||||
vnodes &&
|
||||
vnodes.length === 1 &&
|
||||
isVNode(vnodes[0]) &&
|
||||
(vaporSlot = vnodes[0].vs)
|
||||
) {
|
||||
if (!vaporSlot.fallback && fallback) {
|
||||
vaporSlot.fallback = fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ function reload(id: string, newComp: HMRComponent): void {
|
|||
// create a snapshot which avoids the set being mutated during updates
|
||||
const instances = [...record.instances]
|
||||
|
||||
if (newComp.vapor) {
|
||||
if (newComp.__vapor) {
|
||||
for (const instance of instances) {
|
||||
instance.hmrReload!(newComp)
|
||||
}
|
||||
|
|
|
@ -31,11 +31,16 @@ import {
|
|||
isRenderableAttrValue,
|
||||
isReservedProp,
|
||||
isString,
|
||||
isVaporAnchors,
|
||||
normalizeClass,
|
||||
normalizeStyle,
|
||||
stringifyStyle,
|
||||
} from '@vue/shared'
|
||||
import { type RendererInternals, needTransition } from './renderer'
|
||||
import {
|
||||
type RendererInternals,
|
||||
getVaporInterface,
|
||||
needTransition,
|
||||
} from './renderer'
|
||||
import { setRef } from './rendererTemplateRef'
|
||||
import {
|
||||
type SuspenseBoundary,
|
||||
|
@ -111,7 +116,7 @@ export function createHydrationFunctions(
|
|||
o: {
|
||||
patchProp,
|
||||
createText,
|
||||
nextSibling,
|
||||
nextSibling: next,
|
||||
parentNode,
|
||||
remove,
|
||||
insert,
|
||||
|
@ -119,6 +124,15 @@ export function createHydrationFunctions(
|
|||
},
|
||||
} = rendererInternals
|
||||
|
||||
function nextSibling(node: Node) {
|
||||
let n = next(node)
|
||||
// skip vapor mode specific anchors
|
||||
if (n && isVaporAnchors(n)) {
|
||||
n = next(n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
const hydrate: RootHydrateFunction = (vnode, container) => {
|
||||
if (!container.hasChildNodes()) {
|
||||
;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
|
||||
|
@ -145,6 +159,10 @@ export function createHydrationFunctions(
|
|||
slotScopeIds: string[] | null,
|
||||
optimized = false,
|
||||
): Node | null => {
|
||||
// skip vapor mode specific anchors
|
||||
if (isVaporAnchors(node)) {
|
||||
node = nextSibling(node)!
|
||||
}
|
||||
optimized = optimized || !!vnode.dynamicChildren
|
||||
const isFragmentStart = isComment(node) && node.data === '['
|
||||
const onMismatch = () =>
|
||||
|
@ -278,10 +296,6 @@ export function createHydrationFunctions(
|
|||
)
|
||||
}
|
||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||
if ((vnode.type as ConcreteComponent).__vapor) {
|
||||
throw new Error('Vapor component hydration is not supported yet.')
|
||||
}
|
||||
|
||||
// when setting up the render effect, if the initial vnode already
|
||||
// has .el set, the component will perform hydration instead of mount
|
||||
// on its sub-tree.
|
||||
|
@ -302,15 +316,23 @@ export function createHydrationFunctions(
|
|||
nextNode = nextSibling(node)
|
||||
}
|
||||
|
||||
mountComponent(
|
||||
vnode,
|
||||
container,
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
getContainerType(container),
|
||||
optimized,
|
||||
)
|
||||
// hydrate vapor component
|
||||
if ((vnode.type as ConcreteComponent).__vapor) {
|
||||
const vaporInterface = getVaporInterface(parentComponent, vnode)
|
||||
vaporInterface.hydrate(node, () => {
|
||||
vaporInterface.mount(vnode, container, null, parentComponent)
|
||||
})
|
||||
} else {
|
||||
mountComponent(
|
||||
vnode,
|
||||
container,
|
||||
null,
|
||||
parentComponent,
|
||||
parentSuspense,
|
||||
getContainerType(container),
|
||||
optimized,
|
||||
)
|
||||
}
|
||||
|
||||
// #3787
|
||||
// if component is async, it may get moved / unmounted before its
|
||||
|
@ -451,7 +473,7 @@ export function createHydrationFunctions(
|
|||
|
||||
// The SSRed DOM contains more nodes than it should. Remove them.
|
||||
const cur = next
|
||||
next = next.nextSibling
|
||||
next = nextSibling(next)
|
||||
remove(cur)
|
||||
}
|
||||
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
|
@ -553,7 +575,7 @@ export function createHydrationFunctions(
|
|||
}
|
||||
}
|
||||
|
||||
return el.nextSibling
|
||||
return nextSibling(el)
|
||||
}
|
||||
|
||||
const hydrateChildren = (
|
||||
|
|
|
@ -118,6 +118,7 @@ export { KeepAlive, type KeepAliveProps } from './components/KeepAlive'
|
|||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
checkTransitionMode,
|
||||
type BaseTransitionProps,
|
||||
} from './components/BaseTransition'
|
||||
// For using custom directives
|
||||
|
@ -150,8 +151,10 @@ export { registerRuntimeCompiler, isRuntimeOnly } from './component'
|
|||
export {
|
||||
useTransitionState,
|
||||
resolveTransitionHooks,
|
||||
baseResolveTransitionHooks,
|
||||
setTransitionHooks,
|
||||
getTransitionRawChildren,
|
||||
leaveCbKey,
|
||||
} from './components/BaseTransition'
|
||||
export { initCustomFormatter } from './customFormatter'
|
||||
|
||||
|
@ -335,6 +338,8 @@ export type { SuspenseBoundary } from './components/Suspense'
|
|||
export type {
|
||||
TransitionState,
|
||||
TransitionHooks,
|
||||
TransitionHooksContext,
|
||||
TransitionElement,
|
||||
} from './components/BaseTransition'
|
||||
export type {
|
||||
AsyncComponentOptions,
|
||||
|
@ -505,7 +510,11 @@ export { type VaporInteropInterface } from './apiCreateApp'
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
export { type RendererInternals, MoveType } from './renderer'
|
||||
export {
|
||||
type RendererInternals,
|
||||
MoveType,
|
||||
getInheritedScopeIds,
|
||||
} from './renderer'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -557,6 +566,31 @@ export { startMeasure, endMeasure } from './profiling'
|
|||
* @internal
|
||||
*/
|
||||
export { initFeatureFlags } from './featureFlags'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export { performTransitionEnter, performTransitionLeave } from './renderer'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export { ensureVaporSlotFallback } from './helpers/renderSlot'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export {
|
||||
resolveTarget as resolveTeleportTarget,
|
||||
isTeleportDisabled,
|
||||
isTeleportDeferred,
|
||||
} from './components/Teleport'
|
||||
export {
|
||||
createAsyncComponentContext,
|
||||
useAsyncComponentState,
|
||||
isAsyncWrapper,
|
||||
} from './apiAsyncComponent'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export { markAsyncBoundary } from './helpers/useId'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
|
|
@ -107,6 +107,7 @@ export interface Renderer<HostElement = RendererElement> {
|
|||
|
||||
export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
|
||||
hydrate: RootHydrateFunction
|
||||
hydrateNode: ReturnType<typeof createHydrationFunctions>[1]
|
||||
}
|
||||
|
||||
export type ElementNamespace = 'svg' | 'mathml' | undefined
|
||||
|
@ -731,19 +732,20 @@ function baseCreateRenderer(
|
|||
}
|
||||
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
|
||||
// #1689 For inside suspense + suspense resolved case, just call it
|
||||
const needCallTransitionHooks = needTransition(parentSuspense, transition)
|
||||
if (needCallTransitionHooks) {
|
||||
transition!.beforeEnter(el)
|
||||
if (transition) {
|
||||
performTransitionEnter(
|
||||
el,
|
||||
transition,
|
||||
() => hostInsert(el, container, anchor),
|
||||
parentSuspense,
|
||||
)
|
||||
} else {
|
||||
hostInsert(el, container, anchor)
|
||||
}
|
||||
hostInsert(el, container, anchor)
|
||||
if (
|
||||
(vnodeHook = props && props.onVnodeMounted) ||
|
||||
needCallTransitionHooks ||
|
||||
dirs
|
||||
) {
|
||||
|
||||
if ((vnodeHook = props && props.onVnodeMounted) || dirs) {
|
||||
queuePostRenderEffect(() => {
|
||||
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
|
||||
needCallTransitionHooks && transition!.enter(el)
|
||||
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
|
||||
}, parentSuspense)
|
||||
}
|
||||
|
@ -764,30 +766,9 @@ function baseCreateRenderer(
|
|||
hostSetScopeId(el, slotScopeIds[i])
|
||||
}
|
||||
}
|
||||
let subTree = parentComponent && parentComponent.subTree
|
||||
if (subTree) {
|
||||
if (
|
||||
__DEV__ &&
|
||||
subTree.patchFlag > 0 &&
|
||||
subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
|
||||
) {
|
||||
subTree =
|
||||
filterSingleRoot(subTree.children as VNodeArrayChildren) || subTree
|
||||
}
|
||||
if (
|
||||
vnode === subTree ||
|
||||
(isSuspense(subTree.type) &&
|
||||
(subTree.ssContent === vnode || subTree.ssFallback === vnode))
|
||||
) {
|
||||
const parentVNode = parentComponent!.vnode!
|
||||
setScopeId(
|
||||
el,
|
||||
parentVNode,
|
||||
parentVNode.scopeId,
|
||||
parentVNode.slotScopeIds,
|
||||
parentComponent!.parent,
|
||||
)
|
||||
}
|
||||
const inheritedScopeIds = getInheritedScopeIds(vnode, parentComponent)
|
||||
for (let i = 0; i < inheritedScopeIds.length; i++) {
|
||||
hostSetScopeId(el, inheritedScopeIds[i])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2115,9 +2096,12 @@ function baseCreateRenderer(
|
|||
transition
|
||||
if (needTransition) {
|
||||
if (moveType === MoveType.ENTER) {
|
||||
transition!.beforeEnter(el!)
|
||||
hostInsert(el!, container, anchor)
|
||||
queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
|
||||
performTransitionEnter(
|
||||
el!,
|
||||
transition,
|
||||
() => hostInsert(el!, container, anchor),
|
||||
parentSuspense,
|
||||
)
|
||||
} else {
|
||||
const { leave, delayLeave, afterLeave } = transition!
|
||||
const remove = () => {
|
||||
|
@ -2300,27 +2284,15 @@ function baseCreateRenderer(
|
|||
return
|
||||
}
|
||||
|
||||
const performRemove = () => {
|
||||
hostRemove(el!)
|
||||
if (transition && !transition.persisted && transition.afterLeave) {
|
||||
transition.afterLeave()
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
vnode.shapeFlag & ShapeFlags.ELEMENT &&
|
||||
transition &&
|
||||
!transition.persisted
|
||||
) {
|
||||
const { leave, delayLeave } = transition
|
||||
const performLeave = () => leave(el!, performRemove)
|
||||
if (delayLeave) {
|
||||
delayLeave(vnode.el!, performRemove, performLeave)
|
||||
} else {
|
||||
performLeave()
|
||||
}
|
||||
if (transition) {
|
||||
performTransitionLeave(
|
||||
el!,
|
||||
transition,
|
||||
() => hostRemove(el!),
|
||||
!!(vnode.shapeFlag & ShapeFlags.ELEMENT),
|
||||
)
|
||||
} else {
|
||||
performRemove()
|
||||
hostRemove(el!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2444,7 +2416,7 @@ function baseCreateRenderer(
|
|||
const getNextHostNode: NextFn = vnode => {
|
||||
if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
|
||||
if ((vnode.type as ConcreteComponent).__vapor) {
|
||||
return hostNextSibling((vnode.component! as any).block)
|
||||
return hostNextSibling(vnode.anchor!)
|
||||
}
|
||||
return getNextHostNode(vnode.component!.subTree)
|
||||
}
|
||||
|
@ -2546,6 +2518,7 @@ function baseCreateRenderer(
|
|||
return {
|
||||
render,
|
||||
hydrate,
|
||||
hydrateNode,
|
||||
internals,
|
||||
createApp: createAppAPI(
|
||||
mountApp,
|
||||
|
@ -2648,7 +2621,7 @@ export function traverseStaticChildren(
|
|||
function locateNonHydratedAsyncRoot(
|
||||
instance: ComponentInternalInstance,
|
||||
): ComponentInternalInstance | undefined {
|
||||
const subComponent = instance.subTree.component
|
||||
const subComponent = instance.vapor ? null : instance.subTree.component
|
||||
if (subComponent) {
|
||||
if (subComponent.asyncDep && !subComponent.asyncResolved) {
|
||||
return subComponent
|
||||
|
@ -2665,7 +2638,50 @@ export function invalidateMount(hooks: LifecycleHook | undefined): void {
|
|||
}
|
||||
}
|
||||
|
||||
function getVaporInterface(
|
||||
// shared between vdom and vapor
|
||||
export function performTransitionEnter(
|
||||
el: RendererElement,
|
||||
transition: TransitionHooks,
|
||||
insert: () => void,
|
||||
parentSuspense: SuspenseBoundary | null,
|
||||
): void {
|
||||
if (needTransition(parentSuspense, transition)) {
|
||||
transition.beforeEnter(el)
|
||||
insert()
|
||||
queuePostRenderEffect(() => transition.enter(el), parentSuspense)
|
||||
} else {
|
||||
insert()
|
||||
}
|
||||
}
|
||||
|
||||
// shared between vdom and vapor
|
||||
export function performTransitionLeave(
|
||||
el: RendererElement,
|
||||
transition: TransitionHooks,
|
||||
remove: () => void,
|
||||
isElement: boolean = true,
|
||||
): void {
|
||||
const performRemove = () => {
|
||||
remove()
|
||||
if (transition && !transition.persisted && transition.afterLeave) {
|
||||
transition.afterLeave()
|
||||
}
|
||||
}
|
||||
|
||||
if (isElement && transition && !transition.persisted) {
|
||||
const { leave, delayLeave } = transition
|
||||
const performLeave = () => leave(el, performRemove)
|
||||
if (delayLeave) {
|
||||
delayLeave(el, performRemove, performLeave)
|
||||
} else {
|
||||
performLeave()
|
||||
}
|
||||
} else {
|
||||
performRemove()
|
||||
}
|
||||
}
|
||||
|
||||
export function getVaporInterface(
|
||||
instance: ComponentInternalInstance | null,
|
||||
vnode: VNode,
|
||||
): VaporInteropInterface {
|
||||
|
@ -2682,3 +2698,54 @@ function getVaporInterface(
|
|||
}
|
||||
return res!
|
||||
}
|
||||
|
||||
/**
|
||||
* shared between vdom and vapor
|
||||
*/
|
||||
export function getInheritedScopeIds(
|
||||
vnode: VNode,
|
||||
parentComponent: GenericComponentInstance | null,
|
||||
): string[] {
|
||||
const inheritedScopeIds: string[] = []
|
||||
|
||||
let currentParent = parentComponent
|
||||
let currentVNode = vnode
|
||||
|
||||
while (currentParent) {
|
||||
let subTree = currentParent.subTree
|
||||
if (!subTree) break
|
||||
|
||||
if (
|
||||
__DEV__ &&
|
||||
subTree.patchFlag > 0 &&
|
||||
subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
|
||||
) {
|
||||
subTree =
|
||||
filterSingleRoot(subTree.children as VNodeArrayChildren) || subTree
|
||||
}
|
||||
|
||||
if (
|
||||
currentVNode === subTree ||
|
||||
(isSuspense(subTree.type) &&
|
||||
(subTree.ssContent === currentVNode ||
|
||||
subTree.ssFallback === currentVNode))
|
||||
) {
|
||||
const parentVNode = currentParent.vnode!
|
||||
|
||||
if (parentVNode.scopeId) {
|
||||
inheritedScopeIds.push(parentVNode.scopeId)
|
||||
}
|
||||
|
||||
if (parentVNode.slotScopeIds) {
|
||||
inheritedScopeIds.push(...parentVNode.slotScopeIds)
|
||||
}
|
||||
|
||||
currentVNode = parentVNode
|
||||
currentParent = currentParent.parent
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return inheritedScopeIds
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ import { extend } from '@vue/shared'
|
|||
|
||||
const positionMap = new WeakMap<VNode, DOMRect>()
|
||||
const newPositionMap = new WeakMap<VNode, DOMRect>()
|
||||
const moveCbKey = Symbol('_moveCb')
|
||||
export const moveCbKey: symbol = Symbol('_moveCb')
|
||||
const enterCbKey = Symbol('_enterCb')
|
||||
|
||||
export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
|
||||
|
@ -88,7 +88,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
|
|||
|
||||
// we divide the work into three loops to avoid mixing DOM reads and writes
|
||||
// in each iteration - which helps prevent layout thrashing.
|
||||
prevChildren.forEach(callPendingCbs)
|
||||
prevChildren.forEach(vnode => callPendingCbs(vnode.el))
|
||||
prevChildren.forEach(recordPosition)
|
||||
const movedChildren = prevChildren.filter(applyTranslation)
|
||||
|
||||
|
@ -97,20 +97,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
|
|||
|
||||
movedChildren.forEach(c => {
|
||||
const el = c.el as ElementWithTransition
|
||||
const style = el.style
|
||||
addTransitionClass(el, moveClass)
|
||||
style.transform = style.webkitTransform = style.transitionDuration = ''
|
||||
const cb = ((el as any)[moveCbKey] = (e: TransitionEvent) => {
|
||||
if (e && e.target !== el) {
|
||||
return
|
||||
}
|
||||
if (!e || /transform$/.test(e.propertyName)) {
|
||||
el.removeEventListener('transitionend', cb)
|
||||
;(el as any)[moveCbKey] = null
|
||||
removeTransitionClass(el, moveClass)
|
||||
}
|
||||
})
|
||||
el.addEventListener('transitionend', cb)
|
||||
handleMovedChildren(el, moveClass)
|
||||
})
|
||||
prevChildren = []
|
||||
})
|
||||
|
@ -179,8 +166,7 @@ export const TransitionGroup = TransitionGroupImpl as unknown as {
|
|||
}
|
||||
}
|
||||
|
||||
function callPendingCbs(c: VNode) {
|
||||
const el = c.el as any
|
||||
export function callPendingCbs(el: any): void {
|
||||
if (el[moveCbKey]) {
|
||||
el[moveCbKey]()
|
||||
}
|
||||
|
@ -194,19 +180,36 @@ function recordPosition(c: VNode) {
|
|||
}
|
||||
|
||||
function applyTranslation(c: VNode): VNode | undefined {
|
||||
const oldPos = positionMap.get(c)!
|
||||
const newPos = newPositionMap.get(c)!
|
||||
const dx = oldPos.left - newPos.left
|
||||
const dy = oldPos.top - newPos.top
|
||||
if (dx || dy) {
|
||||
const s = (c.el as HTMLElement).style
|
||||
s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
|
||||
s.transitionDuration = '0s'
|
||||
if (
|
||||
baseApplyTranslation(
|
||||
positionMap.get(c)!,
|
||||
newPositionMap.get(c)!,
|
||||
c.el as ElementWithTransition,
|
||||
)
|
||||
) {
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
function hasCSSTransform(
|
||||
// shared between vdom and vapor
|
||||
export function baseApplyTranslation(
|
||||
oldPos: DOMRect,
|
||||
newPos: DOMRect,
|
||||
el: ElementWithTransition,
|
||||
): boolean {
|
||||
const dx = oldPos.left - newPos.left
|
||||
const dy = oldPos.top - newPos.top
|
||||
if (dx || dy) {
|
||||
const s = (el as HTMLElement).style
|
||||
s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
|
||||
s.transitionDuration = '0s'
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shared between vdom and vapor
|
||||
export function hasCSSTransform(
|
||||
el: ElementWithTransition,
|
||||
root: Node,
|
||||
moveClass: string,
|
||||
|
@ -233,3 +236,24 @@ function hasCSSTransform(
|
|||
container.removeChild(clone)
|
||||
return hasTransform
|
||||
}
|
||||
|
||||
// shared between vdom and vapor
|
||||
export const handleMovedChildren = (
|
||||
el: ElementWithTransition,
|
||||
moveClass: string,
|
||||
): void => {
|
||||
const style = el.style
|
||||
addTransitionClass(el, moveClass)
|
||||
style.transform = style.webkitTransform = style.transitionDuration = ''
|
||||
const cb = ((el as any)[moveCbKey] = (e: TransitionEvent) => {
|
||||
if (e && e.target !== el) {
|
||||
return
|
||||
}
|
||||
if (!e || /transform$/.test(e.propertyName)) {
|
||||
el.removeEventListener('transitionend', cb)
|
||||
;(el as any)[moveCbKey] = null
|
||||
removeTransitionClass(el, moveClass)
|
||||
}
|
||||
})
|
||||
el.addEventListener('transitionend', cb)
|
||||
}
|
||||
|
|
|
@ -319,7 +319,7 @@ export * from './jsx'
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
export { ensureRenderer, normalizeContainer }
|
||||
export { ensureRenderer, ensureHydrationRenderer, normalizeContainer }
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
@ -348,3 +348,24 @@ export {
|
|||
vModelSelectInit,
|
||||
vModelSetSelected,
|
||||
} from './directives/vModel'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export {
|
||||
resolveTransitionProps,
|
||||
TransitionPropsValidators,
|
||||
forceReflow,
|
||||
addTransitionClass,
|
||||
removeTransitionClass,
|
||||
type ElementWithTransition,
|
||||
} from './components/Transition'
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export {
|
||||
hasCSSTransform,
|
||||
callPendingCbs,
|
||||
moveCbKey,
|
||||
handleMovedChildren,
|
||||
baseApplyTranslation,
|
||||
} from './components/TransitionGroup'
|
||||
|
|
|
@ -110,4 +110,22 @@ describe('api: createDynamicComponent', () => {
|
|||
await nextTick()
|
||||
expect(html()).toBe('<div><div>B</div><!--dynamic-component--></div>')
|
||||
})
|
||||
|
||||
test('render fallback with insertionState', async () => {
|
||||
const { html, mount } = define({
|
||||
setup() {
|
||||
const html = ref('hi')
|
||||
const n1 = template('<div></div>', true)() as any
|
||||
setInsertionState(n1)
|
||||
const n0 = createComponentWithFallback(
|
||||
resolveDynamicComponent('button') as any,
|
||||
) as any
|
||||
renderEffect(() => setHtml(n0, html.value))
|
||||
return n1
|
||||
},
|
||||
}).create()
|
||||
|
||||
mount()
|
||||
expect(html()).toBe('<div><button>hi</button></div>')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,764 @@
|
|||
import { nextTick, ref } from '@vue/runtime-dom'
|
||||
import { type VaporComponent, createComponent } from '../src/component'
|
||||
import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent'
|
||||
import { makeRender } from './_utils'
|
||||
import {
|
||||
createIf,
|
||||
createTemplateRefSetter,
|
||||
renderEffect,
|
||||
template,
|
||||
} from '@vue/runtime-vapor'
|
||||
import { setElementText } from '../src/dom/prop'
|
||||
|
||||
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
|
||||
|
||||
const define = makeRender()
|
||||
|
||||
describe('api: defineAsyncComponent', () => {
|
||||
test('simple usage', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent(
|
||||
() =>
|
||||
new Promise(r => {
|
||||
resolve = r as any
|
||||
}),
|
||||
)
|
||||
|
||||
const toggle = ref(true)
|
||||
const { html } = define({
|
||||
setup() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
)
|
||||
},
|
||||
}).render()
|
||||
|
||||
expect(html()).toBe('<!--async component--><!--if-->')
|
||||
resolve!(() => template('resolved')())
|
||||
|
||||
await timeout()
|
||||
expect(html()).toBe('resolved<!--async component--><!--if-->')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(html()).toBe('<!--if-->')
|
||||
|
||||
// already resolved component should update on nextTick
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(html()).toBe('resolved<!--async component--><!--if-->')
|
||||
})
|
||||
|
||||
test('with loading component', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(r => {
|
||||
resolve = r as any
|
||||
}),
|
||||
loadingComponent: () => template('loading')(),
|
||||
delay: 1, // defaults to 200
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const { html } = define({
|
||||
setup() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
)
|
||||
},
|
||||
}).render()
|
||||
|
||||
// due to the delay, initial mount should be empty
|
||||
expect(html()).toBe('<!--async component--><!--if-->')
|
||||
|
||||
// loading show up after delay
|
||||
await timeout(1)
|
||||
expect(html()).toBe('loading<!--async component--><!--if-->')
|
||||
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(html()).toBe('resolved<!--async component--><!--if-->')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(html()).toBe('<!--if-->')
|
||||
|
||||
// already resolved component should update on nextTick without loading
|
||||
// state
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(html()).toBe('resolved<!--async component--><!--if-->')
|
||||
})
|
||||
|
||||
test('with loading component + explicit delay (0)', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(r => {
|
||||
resolve = r as any
|
||||
}),
|
||||
loadingComponent: () => template('loading')(),
|
||||
delay: 0,
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const { html } = define({
|
||||
setup() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
)
|
||||
},
|
||||
}).render()
|
||||
|
||||
// with delay: 0, should show loading immediately
|
||||
expect(html()).toBe('loading<!--async component--><!--if-->')
|
||||
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(html()).toBe('resolved<!--async component--><!--if-->')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(html()).toBe('<!--if-->')
|
||||
|
||||
// already resolved component should update on nextTick without loading
|
||||
// state
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(html()).toBe('resolved<!--async component--><!--if-->')
|
||||
})
|
||||
|
||||
test('error without error component', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
let reject: (e: Error) => void
|
||||
const Foo = defineVaporAsyncComponent(
|
||||
() =>
|
||||
new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
}),
|
||||
)
|
||||
|
||||
const toggle = ref(true)
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
)
|
||||
},
|
||||
}).create()
|
||||
|
||||
const handler = (app.config.errorHandler = vi.fn())
|
||||
const root = document.createElement('div')
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
const err = new Error('foo')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0]).toBe(err)
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--if-->')
|
||||
|
||||
// errored out on previous load, toggle and mock success this time
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
|
||||
})
|
||||
|
||||
test('error with error component', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
let reject: (e: Error) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
}),
|
||||
errorComponent: (props: { error: Error }) =>
|
||||
template(props.error.message)(),
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
)
|
||||
},
|
||||
}).create()
|
||||
const handler = (app.config.errorHandler = vi.fn())
|
||||
const root = document.createElement('div')
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
const err = new Error('errored out')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--if-->')
|
||||
|
||||
// errored out on previous load, toggle and mock success this time
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
|
||||
})
|
||||
|
||||
test('error with error component, without global handler', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
let reject: (e: Error) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
}),
|
||||
errorComponent: (props: { error: Error }) =>
|
||||
template(props.error.message)(),
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const { mount } = define({
|
||||
setup() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
)
|
||||
},
|
||||
}).create()
|
||||
const root = document.createElement('div')
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
const err = new Error('errored out')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
|
||||
expect(
|
||||
'Unhandled error during execution of async component loader',
|
||||
).toHaveBeenWarned()
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--if-->')
|
||||
|
||||
// errored out on previous load, toggle and mock success this time
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
|
||||
})
|
||||
|
||||
test('error with error + loading components', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
let reject: (e: Error) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
}),
|
||||
errorComponent: (props: { error: Error }) =>
|
||||
template(props.error.message)(),
|
||||
loadingComponent: () => template('loading')(),
|
||||
delay: 1,
|
||||
})
|
||||
|
||||
const toggle = ref(true)
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
)
|
||||
},
|
||||
}).create()
|
||||
const handler = (app.config.errorHandler = vi.fn())
|
||||
const root = document.createElement('div')
|
||||
mount(root)
|
||||
|
||||
// due to the delay, initial mount should be empty
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
// loading show up after delay
|
||||
await timeout(1)
|
||||
expect(root.innerHTML).toBe('loading<!--async component--><!--if-->')
|
||||
|
||||
const err = new Error('errored out')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(root.innerHTML).toBe('errored out<!--async component--><!--if-->')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--if-->')
|
||||
|
||||
// errored out on previous load, toggle and mock success this time
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
|
||||
// loading show up after delay
|
||||
await timeout(1)
|
||||
expect(root.innerHTML).toBe('loading<!--async component--><!--if-->')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
|
||||
})
|
||||
|
||||
test('timeout without error component', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
timeout: 1,
|
||||
})
|
||||
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
}).create()
|
||||
const handler = vi.fn()
|
||||
app.config.errorHandler = handler
|
||||
|
||||
const root = document.createElement('div')
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
|
||||
await timeout(1)
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0].message).toMatch(
|
||||
`Async component timed out after 1ms.`,
|
||||
)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
|
||||
// if it resolved after timeout, should still work
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component-->')
|
||||
})
|
||||
|
||||
test('timeout with error component', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
timeout: 1,
|
||||
errorComponent: () => template('timed out')(),
|
||||
})
|
||||
|
||||
const root = document.createElement('div')
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
}).create()
|
||||
|
||||
const handler = (app.config.errorHandler = vi.fn())
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
|
||||
await timeout(1)
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(root.innerHTML).toBe('timed out<!--async component-->')
|
||||
|
||||
// if it resolved after timeout, should still work
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component-->')
|
||||
})
|
||||
|
||||
test('timeout with error + loading components', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
delay: 1,
|
||||
timeout: 16,
|
||||
errorComponent: () => template('timed out')(),
|
||||
loadingComponent: () => template('loading')(),
|
||||
})
|
||||
|
||||
const root = document.createElement('div')
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
}).create()
|
||||
const handler = (app.config.errorHandler = vi.fn())
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
await timeout(1)
|
||||
expect(root.innerHTML).toBe('loading<!--async component-->')
|
||||
|
||||
await timeout(16)
|
||||
expect(root.innerHTML).toBe('timed out<!--async component-->')
|
||||
expect(handler).toHaveBeenCalled()
|
||||
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component-->')
|
||||
})
|
||||
|
||||
test('timeout without error component, but with loading component', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () =>
|
||||
new Promise(_resolve => {
|
||||
resolve = _resolve as any
|
||||
}),
|
||||
delay: 1,
|
||||
timeout: 16,
|
||||
loadingComponent: () => template('loading')(),
|
||||
})
|
||||
|
||||
const root = document.createElement('div')
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
}).create()
|
||||
const handler = vi.fn()
|
||||
app.config.errorHandler = handler
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
await timeout(1)
|
||||
expect(root.innerHTML).toBe('loading<!--async component-->')
|
||||
|
||||
await timeout(16)
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0].message).toMatch(
|
||||
`Async component timed out after 16ms.`,
|
||||
)
|
||||
// should still display loading
|
||||
expect(root.innerHTML).toBe('loading<!--async component-->')
|
||||
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component-->')
|
||||
})
|
||||
|
||||
test('retry (success)', async () => {
|
||||
let loaderCallCount = 0
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
let reject: (e: Error) => void
|
||||
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () => {
|
||||
loaderCallCount++
|
||||
return new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve as any
|
||||
reject = _reject
|
||||
})
|
||||
},
|
||||
onError(error, retry, fail) {
|
||||
if (error.message.match(/foo/)) {
|
||||
retry()
|
||||
} else {
|
||||
fail()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const root = document.createElement('div')
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
}).create()
|
||||
|
||||
const handler = (app.config.errorHandler = vi.fn())
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
expect(loaderCallCount).toBe(1)
|
||||
|
||||
const err = new Error('foo')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
expect(loaderCallCount).toBe(2)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
|
||||
// should render this time
|
||||
resolve!(() => template('resolved')())
|
||||
await timeout()
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component-->')
|
||||
})
|
||||
|
||||
test('retry (skipped)', async () => {
|
||||
let loaderCallCount = 0
|
||||
let reject: (e: Error) => void
|
||||
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () => {
|
||||
loaderCallCount++
|
||||
return new Promise((_resolve, _reject) => {
|
||||
reject = _reject
|
||||
})
|
||||
},
|
||||
onError(error, retry, fail) {
|
||||
if (error.message.match(/bar/)) {
|
||||
retry()
|
||||
} else {
|
||||
fail()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const root = document.createElement('div')
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
}).create()
|
||||
|
||||
const handler = (app.config.errorHandler = vi.fn())
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
expect(loaderCallCount).toBe(1)
|
||||
|
||||
const err = new Error('foo')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
// should fail because retryWhen returns false
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0]).toBe(err)
|
||||
expect(loaderCallCount).toBe(1)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
})
|
||||
|
||||
test('retry (fail w/ max retry attempts)', async () => {
|
||||
let loaderCallCount = 0
|
||||
let reject: (e: Error) => void
|
||||
|
||||
const Foo = defineVaporAsyncComponent({
|
||||
loader: () => {
|
||||
loaderCallCount++
|
||||
return new Promise((_resolve, _reject) => {
|
||||
reject = _reject
|
||||
})
|
||||
},
|
||||
onError(error, retry, fail, attempts) {
|
||||
if (error.message.match(/foo/) && attempts <= 1) {
|
||||
retry()
|
||||
} else {
|
||||
fail()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const root = document.createElement('div')
|
||||
const { app, mount } = define({
|
||||
setup() {
|
||||
return createComponent(Foo)
|
||||
},
|
||||
}).create()
|
||||
|
||||
const handler = (app.config.errorHandler = vi.fn())
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
expect(loaderCallCount).toBe(1)
|
||||
|
||||
// first retry
|
||||
const err = new Error('foo')
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
expect(loaderCallCount).toBe(2)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
|
||||
// 2nd retry, should fail due to reaching maxRetries
|
||||
reject!(err)
|
||||
await timeout()
|
||||
expect(handler).toHaveBeenCalled()
|
||||
expect(handler.mock.calls[0][0]).toBe(err)
|
||||
expect(loaderCallCount).toBe(2)
|
||||
expect(root.innerHTML).toBe('<!--async component-->')
|
||||
})
|
||||
|
||||
test('template ref forwarding', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent(
|
||||
() =>
|
||||
new Promise(r => {
|
||||
resolve = r as any
|
||||
}),
|
||||
)
|
||||
|
||||
const fooRef = ref<any>(null)
|
||||
const toggle = ref(true)
|
||||
const root = document.createElement('div')
|
||||
const { mount } = define({
|
||||
setup() {
|
||||
return { fooRef, toggle }
|
||||
},
|
||||
render() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
const setTemplateRef = createTemplateRefSetter()
|
||||
const n0 = createComponent(Foo, null, null, true)
|
||||
setTemplateRef(n0, 'fooRef')
|
||||
return n0
|
||||
},
|
||||
)
|
||||
},
|
||||
}).create()
|
||||
mount(root)
|
||||
expect(root.innerHTML).toBe('<!--async component--><!--if-->')
|
||||
expect(fooRef.value).toBe(null)
|
||||
|
||||
resolve!({
|
||||
setup: (props, { expose }) => {
|
||||
expose({
|
||||
id: 'foo',
|
||||
})
|
||||
return template('resolved')()
|
||||
},
|
||||
})
|
||||
// first time resolve, wait for macro task since there are multiple
|
||||
// microtasks / .then() calls
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
|
||||
expect(fooRef.value.id).toBe('foo')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--if-->')
|
||||
expect(fooRef.value).toBe(null)
|
||||
|
||||
// already resolved component should update on nextTick
|
||||
toggle.value = true
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('resolved<!--async component--><!--if-->')
|
||||
expect(fooRef.value.id).toBe('foo')
|
||||
})
|
||||
|
||||
test('the forwarded template ref should always exist when doing multi patching', async () => {
|
||||
let resolve: (comp: VaporComponent) => void
|
||||
const Foo = defineVaporAsyncComponent(
|
||||
() =>
|
||||
new Promise(r => {
|
||||
resolve = r as any
|
||||
}),
|
||||
)
|
||||
|
||||
const fooRef = ref<any>(null)
|
||||
const toggle = ref(true)
|
||||
const updater = ref(0)
|
||||
|
||||
const root = document.createElement('div')
|
||||
const { mount } = define({
|
||||
setup() {
|
||||
return { fooRef, toggle, updater }
|
||||
},
|
||||
render() {
|
||||
return createIf(
|
||||
() => toggle.value,
|
||||
() => {
|
||||
const setTemplateRef = createTemplateRefSetter()
|
||||
const n0 = createComponent(Foo, null, null, true)
|
||||
setTemplateRef(n0, 'fooRef')
|
||||
const n1 = template(`<span>`)()
|
||||
renderEffect(() => setElementText(n1, updater.value))
|
||||
return [n0, n1]
|
||||
},
|
||||
)
|
||||
},
|
||||
}).create()
|
||||
mount(root)
|
||||
|
||||
expect(root.innerHTML).toBe('<!--async component--><span>0</span><!--if-->')
|
||||
expect(fooRef.value).toBe(null)
|
||||
|
||||
resolve!({
|
||||
setup: (props, { expose }) => {
|
||||
expose({
|
||||
id: 'foo',
|
||||
})
|
||||
return template('resolved')()
|
||||
},
|
||||
})
|
||||
|
||||
await timeout()
|
||||
expect(root.innerHTML).toBe(
|
||||
'resolved<!--async component--><span>0</span><!--if-->',
|
||||
)
|
||||
expect(fooRef.value.id).toBe('foo')
|
||||
|
||||
updater.value++
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe(
|
||||
'resolved<!--async component--><span>1</span><!--if-->',
|
||||
)
|
||||
expect(fooRef.value.id).toBe('foo')
|
||||
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
expect(root.innerHTML).toBe('<!--if-->')
|
||||
expect(fooRef.value).toBe(null)
|
||||
})
|
||||
|
||||
test.todo('with suspense', async () => {})
|
||||
|
||||
test.todo('suspensible: false', async () => {})
|
||||
|
||||
test.todo('suspense with error handling', async () => {})
|
||||
|
||||
test.todo('with KeepAlive', async () => {})
|
||||
|
||||
test.todo('with KeepAlive + include', async () => {})
|
||||
})
|
|
@ -1,10 +1,5 @@
|
|||
import {
|
||||
VaporFragment,
|
||||
insert,
|
||||
normalizeBlock,
|
||||
prepend,
|
||||
remove,
|
||||
} from '../src/block'
|
||||
import { insert, normalizeBlock, prepend, remove } from '../src/block'
|
||||
import { VaporFragment } from '../src/fragment'
|
||||
|
||||
const node1 = document.createTextNode('node1')
|
||||
const node2 = document.createTextNode('node2')
|
||||
|
|
|
@ -4,12 +4,18 @@
|
|||
// ./rendererAttrsFallthrough.spec.ts.
|
||||
|
||||
import {
|
||||
createApp,
|
||||
h,
|
||||
isEmitListener,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
toHandlers,
|
||||
} from '@vue/runtime-dom'
|
||||
import { createComponent, defineVaporComponent } from '../src'
|
||||
import {
|
||||
createComponent,
|
||||
defineVaporComponent,
|
||||
vaporInteropPlugin,
|
||||
} from '../src'
|
||||
import { makeRender } from './_utils'
|
||||
|
||||
const define = makeRender()
|
||||
|
@ -425,3 +431,28 @@ describe('component: emit', () => {
|
|||
expect(fn).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('vdom interop', () => {
|
||||
test('vdom parent > vapor child', () => {
|
||||
const VaporChild = defineVaporComponent({
|
||||
emits: ['click'],
|
||||
setup(_, { emit }) {
|
||||
emit('click')
|
||||
return []
|
||||
},
|
||||
})
|
||||
|
||||
const fn = vi.fn()
|
||||
const App = {
|
||||
setup() {
|
||||
return () => h(VaporChild as any, { onClick: fn })
|
||||
},
|
||||
}
|
||||
|
||||
const root = document.createElement('div')
|
||||
createApp(App).use(vaporInteropPlugin).mount(root)
|
||||
|
||||
// fn should be called once
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue