]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: work with v-show appear
authordaiwei <daiwei521@126.com>
Fri, 14 Mar 2025 09:02:21 +0000 (17:02 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 14 Mar 2025 09:02:21 +0000 (17:02 +0800)
packages-private/vapor-e2e-test/__tests__/transition.spec.ts
packages-private/vapor-e2e-test/transition/App.vue
packages/compiler-vapor/__tests__/transforms/TransformTransition.spec.ts
packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap
packages/compiler-vapor/src/generators/block.ts
packages/compiler-vapor/src/generators/vShow.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/utils.ts
packages/compiler-vapor/src/transforms/vShow.ts
packages/runtime-vapor/src/directives/vShow.ts

index 2a9de7e73a9d596b4d7399ead23ed87944270254..0bfc30598cca9cf7c74e187e15b7d9eeebbcb564 100644 (file)
@@ -1061,23 +1061,103 @@ describe('vapor transition', () => {
       E2E_TIMEOUT,
     )
 
-    test.todo(
+    test(
       'transition on appear with v-show',
       async () => {
         const btnSelector = '.show-appear > button'
         const containerSelector = '.show-appear > 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,
     )
 
-    test.todo(
+    test(
       '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,
     )
-
-    test.todo('transition on appear with v-show', async () => {}, E2E_TIMEOUT)
   })
 
   describe('explicit durations', () => {
index a5b32a621c975d666e90eaddc6c7d4704b3f7581..4855098243b6ae805430ea43bc0b5ddbdc583055 100644 (file)
@@ -27,6 +27,7 @@ let calls = {
   show: [],
   showLeaveCancel: [],
   showAppear: [],
+  notEnter: [],
 }
 window.getCalls = key => calls[key]
 window.resetCalls = key => (calls[key] = [])
@@ -398,6 +399,20 @@ function changeViewInOut() {
       </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 -->
index 7c86b9f37c39e3595c910293bb87ab1b156dc02f..bcb7b44ce5b79033221cef209d5fa9c127571d77 100644 (file)
@@ -28,6 +28,13 @@ const compileWithElementTransform = makeCompile({
 
 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>`,
     )
index a1de229f5c2d634e7f4769cb1e2b72f89376a9ab..37ae42ed4bed6ee955a2328f19a530ce017e3f6b 100644 (file)
@@ -5,10 +5,7 @@ exports[`compiler: transition > basic 1`] = `
 const t0 = _template("<h1>foo</h1>")
 
 export function render(_ctx) {
-  const n1 = _createComponent(_VaporTransition, {
-    appear: () => (""), 
-    persisted: () => ("")
-  }, {
+  const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, {
     "default": () => {
       const n0 = t0()
       _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`] = `
 "import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, createComponent as _createComponent, template as _template } from 'vue';
 const t0 = _template("<h1>foo</h1>")
index 654816e09d7ef28c9b58c3f4ff319b100f39d9f0..e7f0610ac5bc8cbd6ef121b59dba0a2ece0b6a49 100644 (file)
@@ -44,6 +44,10 @@ export function genBlockContent(
   const { dynamic, effect, operation, returns, key } = block
   const resetBlock = context.enterBlock(block)
 
+  if (block.hasLazyApplyVShow) {
+    push(NEWLINE, `const lazyApplyVShowFn = []`)
+  }
+
   if (root) {
     genResolveAssets('component', 'resolveComponent')
     genResolveAssets('directive', 'resolveDirective')
@@ -56,6 +60,10 @@ export function genBlockContent(
   push(...genOperations(operation, context))
   push(...genEffects(effect, context))
 
+  if (block.hasLazyApplyVShow) {
+    push(NEWLINE, `lazyApplyVShowFn.forEach(fn => fn())`)
+  }
+
   if (dynamic.needsKey) {
     for (const child of dynamic.children) {
       const keyValue = key
index 9a6ccefcded93932fffa7d1adbf002182473b735..701127916b1a8ecd2ed7b1beac5b0442439dacec 100644 (file)
@@ -7,12 +7,15 @@ export function genVShow(
   oper: DirectiveIRNode,
   context: CodegenContext,
 ): CodeFragment[] {
+  const { lazy, element } = oper
   return [
     NEWLINE,
-    ...genCall(context.helper('applyVShow'), `n${oper.element}`, [
+    lazy ? `lazyApplyVShowFn.push(() => ` : undefined,
+    ...genCall(context.helper('applyVShow'), `n${element}`, [
       `() => (`,
       ...genExpression(oper.dir.exp!, context),
       `)`,
     ]),
+    lazy ? `)` : undefined,
   ]
 }
index cc0c9dcf307c6e33cd64ef67d1020fdb2a3dd7db..7cd93b015fc3bd8f5e16a88e738a723ae79f9a83 100644 (file)
@@ -56,6 +56,7 @@ export interface BlockIRNode extends BaseIRNode {
   operation: OperationNode[]
   expressions: SimpleExpressionNode[]
   returns: number[]
+  hasLazyApplyVShow: boolean
 }
 
 export interface RootIRNode {
@@ -187,6 +188,7 @@ export interface DirectiveIRNode extends BaseIRNode {
   builtin?: boolean
   asset?: boolean
   modelType?: 'text' | 'dynamic' | 'radio' | 'checkbox' | 'select'
+  lazy?: boolean
 }
 
 export interface CreateComponentIRNode extends BaseIRNode {
index b8e7adc60596dace721318181395ba43abf35c44..99056d44c5e72f8830c731bd0a0bf1714e595e92 100644 (file)
@@ -31,6 +31,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
   returns: [],
   expressions: [],
   tempId: 0,
+  hasLazyApplyVShow: false,
 })
 
 export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
index f1135d6b0a5902f7d9602c00c2d43b2e3ea2b7d0..3622cf0fff39d6c2b8a716e64c6a7446cc5ba1bc 100644 (file)
@@ -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 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({
     type: IRNodeTypes.DIRECTIVE,
     element: context.reference(),
     dir,
     name: 'show',
     builtin: true,
+    lazy: lazyApplyVShow,
   })
 }
index 410f0da235f7e39d2ec17537657d68de54bc38f0..5cd9c66f2945523b21a1b661bc24eda3af8f81fa 100644 (file)
@@ -52,9 +52,15 @@ function setDisplay(target: Block, value: unknown): void {
         el.style.display = el[vShowOriginalDisplay]!
         $transition.enter(target)
       } else {
-        $transition.leave(target, () => {
+        // 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, () => {
+            el.style.display = 'none'
+          })
+        } else {
           el.style.display = 'none'
-        })
+        }
       }
     } else {
       el.style.display = value ? el[vShowOriginalDisplay]! : 'none'