wip: work with v-show appear

This commit is contained in:
daiwei 2025-03-14 17:02:21 +08:00
parent 9ee219ab7d
commit 2caeecd595
10 changed files with 169 additions and 12 deletions

View File

@ -1061,23 +1061,103 @@ describe('vapor transition', () => {
E2E_TIMEOUT, E2E_TIMEOUT,
) )
test.todo( test(
'transition on appear with v-show', 'transition on appear with v-show',
async () => { async () => {
const btnSelector = '.show-appear > button' const btnSelector = '.show-appear > button'
const containerSelector = '.show-appear > div' const containerSelector = '.show-appear > div'
const childSelector = `${containerSelector} > div` const childSelector = `${containerSelector} > div`
let calls = await page().evaluate(() => {
return (window as any).getCalls('showAppear')
})
expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
// appear
expect(await classList(childSelector)).contains('test-appear-active')
await transitionFinish()
expect(await html(containerSelector)).toBe(
'<div class="test">content</div>',
)
calls = await page().evaluate(() => {
return (window as any).getCalls('showAppear')
})
expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
// leave
expect(
(await transitionStart(btnSelector, childSelector)).classNames,
).toStrictEqual(['test', 'test-leave-from', 'test-leave-active'])
await nextFrame()
expect(await classList(childSelector)).toStrictEqual([
'test',
'test-leave-active',
'test-leave-to',
])
await transitionFinish()
expect(await isVisible(childSelector)).toBe(false)
// enter
expect(
(await transitionStart(btnSelector, childSelector)).classNames,
).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
await nextFrame()
expect(await classList(childSelector)).toStrictEqual([
'test',
'test-enter-active',
'test-enter-to',
])
await transitionFinish()
expect(await html(containerSelector)).toBe(
'<div class="test" style="">content</div>',
)
}, },
E2E_TIMEOUT, E2E_TIMEOUT,
) )
test.todo( test(
'transition events should not call onEnter with v-show false', 'transition events should not call onEnter with v-show false',
async () => {}, async () => {
const btnSelector = '.show-appear-not-enter > button'
const containerSelector = '.show-appear-not-enter > div'
const childSelector = `${containerSelector} > div`
expect(await isVisible(childSelector)).toBe(false)
let calls = await page().evaluate(() => {
return (window as any).getCalls('notEnter')
})
expect(calls).toStrictEqual([])
// enter
expect(
(await transitionStart(btnSelector, childSelector)).classNames,
).toStrictEqual(['test', 'test-enter-from', 'test-enter-active'])
calls = await page().evaluate(() => {
return (window as any).getCalls('notEnter')
})
expect(calls).toStrictEqual(['beforeEnter', 'onEnter'])
await nextFrame()
expect(await classList(childSelector)).toStrictEqual([
'test',
'test-enter-active',
'test-enter-to',
])
calls = await page().evaluate(() => {
return (window as any).getCalls('notEnter')
})
expect(calls).not.contain('afterEnter')
await transitionFinish()
expect(await html(containerSelector)).toBe(
'<div class="test" style="">content</div>',
)
calls = await page().evaluate(() => {
return (window as any).getCalls('notEnter')
})
expect(calls).toStrictEqual(['beforeEnter', 'onEnter', 'afterEnter'])
},
E2E_TIMEOUT, E2E_TIMEOUT,
) )
test.todo('transition on appear with v-show', async () => {}, E2E_TIMEOUT)
}) })
describe('explicit durations', () => { describe('explicit durations', () => {

View File

@ -27,6 +27,7 @@ let calls = {
show: [], show: [],
showLeaveCancel: [], showLeaveCancel: [],
showAppear: [], showAppear: [],
notEnter: [],
} }
window.getCalls = key => calls[key] window.getCalls = key => calls[key]
window.resetCalls = key => (calls[key] = []) window.resetCalls = key => (calls[key] = [])
@ -398,6 +399,20 @@ function changeViewInOut() {
</div> </div>
<button @click="toggle = !toggle">button</button> <button @click="toggle = !toggle">button</button>
</div> </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 --> <!-- work with vshow end -->
<!-- explicit durations --> <!-- explicit durations -->

View File

@ -28,6 +28,13 @@ const compileWithElementTransform = makeCompile({
describe('compiler: transition', () => { describe('compiler: transition', () => {
test('basic', () => { test('basic', () => {
const { code } = compileWithElementTransform(
`<Transition><h1 v-show="show">foo</h1></Transition>`,
)
expect(code).toMatchSnapshot()
})
test('v-show + appear', () => {
const { code } = compileWithElementTransform( const { code } = compileWithElementTransform(
`<Transition appear><h1 v-show="show">foo</h1></Transition>`, `<Transition appear><h1 v-show="show">foo</h1></Transition>`,
) )

View File

@ -5,10 +5,7 @@ exports[`compiler: transition > basic 1`] = `
const t0 = _template("<h1>foo</h1>") const t0 = _template("<h1>foo</h1>")
export function render(_ctx) { export function render(_ctx) {
const n1 = _createComponent(_VaporTransition, { const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, {
appear: () => (""),
persisted: () => ("")
}, {
"default": () => { "default": () => {
const n0 = t0() const n0 = t0()
_applyVShow(n0, () => (_ctx.show)) _applyVShow(n0, () => (_ctx.show))
@ -72,6 +69,27 @@ export function render(_ctx) {
}" }"
`; `;
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 lazyApplyVShowFn = []
const n1 = _createComponent(_VaporTransition, {
appear: () => (""),
persisted: () => ("")
}, {
"default": () => {
const n0 = t0()
lazyApplyVShowFn.push(() => _applyVShow(n0, () => (_ctx.show)))
return n0
}
}, true)
lazyApplyVShowFn.forEach(fn => fn())
return n1
}"
`;
exports[`compiler: transition > work with dynamic keyed children 1`] = ` exports[`compiler: transition > work with dynamic keyed children 1`] = `
"import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, createComponent as _createComponent, template as _template } from 'vue'; "import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, createComponent as _createComponent, template as _template } from 'vue';
const t0 = _template("<h1>foo</h1>") const t0 = _template("<h1>foo</h1>")

View File

@ -44,6 +44,10 @@ export function genBlockContent(
const { dynamic, effect, operation, returns, key } = block const { dynamic, effect, operation, returns, key } = block
const resetBlock = context.enterBlock(block) const resetBlock = context.enterBlock(block)
if (block.hasLazyApplyVShow) {
push(NEWLINE, `const lazyApplyVShowFn = []`)
}
if (root) { if (root) {
genResolveAssets('component', 'resolveComponent') genResolveAssets('component', 'resolveComponent')
genResolveAssets('directive', 'resolveDirective') genResolveAssets('directive', 'resolveDirective')
@ -56,6 +60,10 @@ export function genBlockContent(
push(...genOperations(operation, context)) push(...genOperations(operation, context))
push(...genEffects(effect, context)) push(...genEffects(effect, context))
if (block.hasLazyApplyVShow) {
push(NEWLINE, `lazyApplyVShowFn.forEach(fn => fn())`)
}
if (dynamic.needsKey) { if (dynamic.needsKey) {
for (const child of dynamic.children) { for (const child of dynamic.children) {
const keyValue = key const keyValue = key

View File

@ -7,12 +7,15 @@ export function genVShow(
oper: DirectiveIRNode, oper: DirectiveIRNode,
context: CodegenContext, context: CodegenContext,
): CodeFragment[] { ): CodeFragment[] {
const { lazy, element } = oper
return [ return [
NEWLINE, NEWLINE,
...genCall(context.helper('applyVShow'), `n${oper.element}`, [ lazy ? `lazyApplyVShowFn.push(() => ` : undefined,
...genCall(context.helper('applyVShow'), `n${element}`, [
`() => (`, `() => (`,
...genExpression(oper.dir.exp!, context), ...genExpression(oper.dir.exp!, context),
`)`, `)`,
]), ]),
lazy ? `)` : undefined,
] ]
} }

View File

@ -56,6 +56,7 @@ export interface BlockIRNode extends BaseIRNode {
operation: OperationNode[] operation: OperationNode[]
expressions: SimpleExpressionNode[] expressions: SimpleExpressionNode[]
returns: number[] returns: number[]
hasLazyApplyVShow: boolean
} }
export interface RootIRNode { export interface RootIRNode {
@ -187,6 +188,7 @@ export interface DirectiveIRNode extends BaseIRNode {
builtin?: boolean builtin?: boolean
asset?: boolean asset?: boolean
modelType?: 'text' | 'dynamic' | 'radio' | 'checkbox' | 'select' modelType?: 'text' | 'dynamic' | 'radio' | 'checkbox' | 'select'
lazy?: boolean
} }
export interface CreateComponentIRNode extends BaseIRNode { export interface CreateComponentIRNode extends BaseIRNode {

View File

@ -31,6 +31,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
returns: [], returns: [],
expressions: [], expressions: [],
tempId: 0, tempId: 0,
hasLazyApplyVShow: false,
}) })
export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode { export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {

View File

@ -2,11 +2,13 @@ import {
DOMErrorCodes, DOMErrorCodes,
ElementTypes, ElementTypes,
ErrorCodes, ErrorCodes,
NodeTypes,
createCompilerError, createCompilerError,
createDOMCompilerError, createDOMCompilerError,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import type { DirectiveTransform } from '../transform' import type { DirectiveTransform } from '../transform'
import { IRNodeTypes } from '../ir' import { IRNodeTypes } from '../ir'
import { findProp, isTransitionTag } from '../utils'
export const transformVShow: DirectiveTransform = (dir, node, context) => { export const transformVShow: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir const { exp, loc } = dir
@ -27,11 +29,26 @@ export const transformVShow: DirectiveTransform = (dir, node, context) => {
return return
} }
// lazy apply vshow if the node is inside a transition with appear
let lazyApplyVShow = false
const parentNode = context.parent && context.parent.node
if (parentNode && parentNode.type === NodeTypes.ELEMENT) {
lazyApplyVShow = !!(
isTransitionTag(parentNode.tag) &&
findProp(parentNode, 'appear', false, true)
)
if (lazyApplyVShow) {
context.parent!.parent!.block.hasLazyApplyVShow = true
}
}
context.registerOperation({ context.registerOperation({
type: IRNodeTypes.DIRECTIVE, type: IRNodeTypes.DIRECTIVE,
element: context.reference(), element: context.reference(),
dir, dir,
name: 'show', name: 'show',
builtin: true, builtin: true,
lazy: lazyApplyVShow,
}) })
} }

View File

@ -52,9 +52,15 @@ function setDisplay(target: Block, value: unknown): void {
el.style.display = el[vShowOriginalDisplay]! el.style.display = el[vShowOriginalDisplay]!
$transition.enter(target) $transition.enter(target)
} else { } else {
// during initial render, the element is not yet inserted into the
// DOM, and it is hidden, no need to trigger transition
if (target.isConnected) {
$transition.leave(target, () => { $transition.leave(target, () => {
el.style.display = 'none' el.style.display = 'none'
}) })
} else {
el.style.display = 'none'
}
} }
} else { } else {
el.style.display = value ? el[vShowOriginalDisplay]! : 'none' el.style.display = value ? el[vShowOriginalDisplay]! : 'none'