瀏覽代碼

fix(compiler-vapor): merge component v-model onUpdate handlers (#14229)

edison 4 月之前
父節點
當前提交
e6bff23a4a

+ 8 - 2
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap

@@ -90,7 +90,10 @@ exports[`compiler: element transform > component > props merging: event handlers
 
 export function render(_ctx) {
   const _component_Foo = _resolveComponent("Foo")
-  const n0 = _createComponentWithFallback(_component_Foo, { onClick: () => [_ctx.a, _ctx.b] }, null, true)
+  const n0 = _createComponentWithFallback(_component_Foo, { onClick: () => [
+    _ctx.a,
+    _ctx.b
+  ] }, null, true)
   return n0
 }"
 `;
@@ -102,7 +105,10 @@ export function render(_ctx) {
   const _component_Foo = _resolveComponent("Foo")
   const _on_click = e => _ctx.a(e)
   const _on_click1 = e => _ctx.b(e)
-  const n0 = _createComponentWithFallback(_component_Foo, { onClick: () => [_on_click, _on_click1] }, null, true)
+  const n0 = _createComponentWithFallback(_component_Foo, { onClick: () => [
+    _on_click,
+    _on_click1
+  ] }, null, true)
   return n0
 }"
 `;

+ 46 - 18
packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap

@@ -1,13 +1,29 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
+exports[`compiler: vModel transform > component > component v-model should merge with explicit @update:modelValue 1`] = `
+"
+  const _component_Comp = _resolveComponent("Comp")
+  const n0 = _createComponentWithFallback(_component_Comp, {
+    modelValue: () => (counter.value),
+    "onUpdate:modelValue": () => [
+      _value => (counter.value = _value),
+      onUpdate
+    ]
+  }, null, true)
+  return n0
+"
+`;
+
 exports[`compiler: vModel transform > component > v-model for component should generate modelModifiers 1`] = `
 "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
 
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
-  const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo),
-  "onUpdate:modelValue": () => _value => (_ctx.foo = _value),
-  modelModifiers: () => ({ trim: true, "bar-baz": true }) }, null, true)
+  const n0 = _createComponentWithFallback(_component_Comp, {
+    modelValue: () => (_ctx.foo),
+    "onUpdate:modelValue": () => _value => (_ctx.foo = _value),
+    modelModifiers: () => ({ trim: true, "bar-baz": true })
+  }, null, true)
   return n0
 }"
 `;
@@ -17,8 +33,10 @@ exports[`compiler: vModel transform > component > v-model for component should w
 
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
-  const n0 = _createComponentWithFallback(_component_Comp, { modelValue: () => (_ctx.foo),
-  "onUpdate:modelValue": () => _value => (_ctx.foo = _value) }, null, true)
+  const n0 = _createComponentWithFallback(_component_Comp, {
+    modelValue: () => (_ctx.foo),
+    "onUpdate:modelValue": () => _value => (_ctx.foo = _value)
+  }, null, true)
   return n0
 }"
 `;
@@ -45,8 +63,10 @@ exports[`compiler: vModel transform > component > v-model with arguments for com
 
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
-  const n0 = _createComponentWithFallback(_component_Comp, { bar: () => (_ctx.foo),
-  "onUpdate:bar": () => _value => (_ctx.foo = _value) }, null, true)
+  const n0 = _createComponentWithFallback(_component_Comp, {
+    bar: () => (_ctx.foo),
+    "onUpdate:bar": () => _value => (_ctx.foo = _value)
+  }, null, true)
   return n0
 }"
 `;
@@ -57,12 +77,16 @@ exports[`compiler: vModel transform > component > v-model with dynamic arguments
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n0 = _createComponentWithFallback(_component_Comp, { $: [
-    () => ({ [_ctx.foo]: _ctx.foo,
-    ["onUpdate:" + _ctx.foo]: () => _value => (_ctx.foo = _value),
-    [_ctx.foo + "Modifiers"]: () => ({ trim: true }) }),
-    () => ({ [_ctx.bar]: _ctx.bar,
-    ["onUpdate:" + _ctx.bar]: () => _value => (_ctx.bar = _value),
-    [_ctx.bar + "Modifiers"]: () => ({ number: true }) })
+    () => ({
+      [_ctx.foo]: _ctx.foo,
+      ["onUpdate:" + _ctx.foo]: () => _value => (_ctx.foo = _value),
+      [_ctx.foo + "Modifiers"]: () => ({ trim: true })
+    }),
+    () => ({
+      [_ctx.bar]: _ctx.bar,
+      ["onUpdate:" + _ctx.bar]: () => _value => (_ctx.bar = _value),
+      [_ctx.bar + "Modifiers"]: () => ({ number: true })
+    })
   ] }, null, true)
   return n0
 }"
@@ -74,8 +98,10 @@ exports[`compiler: vModel transform > component > v-model with dynamic arguments
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n0 = _createComponentWithFallback(_component_Comp, { $: [
-    () => ({ [_ctx.arg]: _ctx.foo,
-    ["onUpdate:" + _ctx.arg]: () => _value => (_ctx.foo = _value) })
+    () => ({
+      [_ctx.arg]: _ctx.foo,
+      ["onUpdate:" + _ctx.arg]: () => _value => (_ctx.foo = _value)
+    })
   ] }, null, true)
   return n0
 }"
@@ -86,9 +112,11 @@ exports[`compiler: vModel transform > component > v-model:model with arguments f
 
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
-  const n0 = _createComponentWithFallback(_component_Comp, { model: () => (_ctx.foo),
-  "onUpdate:model": () => _value => (_ctx.foo = _value),
-  modelModifiers$: () => ({ trim: true }) }, null, true)
+  const n0 = _createComponentWithFallback(_component_Comp, {
+    model: () => (_ctx.foo),
+    "onUpdate:model": () => _value => (_ctx.foo = _value),
+    modelModifiers$: () => ({ trim: true })
+  }, null, true)
   return n0
 }"
 `;

+ 8 - 2
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts

@@ -333,7 +333,10 @@ describe('compiler: element transform', () => {
         `<Foo @click.foo="a" @click.bar="b" />`,
       )
       expect(code).toMatchSnapshot()
-      expect(code).contains('onClick: () => [_ctx.a, _ctx.b]')
+      expect(code).contains(`onClick: () => [
+    _ctx.a,
+    _ctx.b
+  ]`)
     })
 
     test('props merging: inline event handlers', () => {
@@ -343,7 +346,10 @@ describe('compiler: element transform', () => {
       expect(code).toMatchSnapshot()
       expect(code).contains('const _on_click = e => _ctx.a(e)')
       expect(code).contains('const _on_click1 = e => _ctx.b(e)')
-      expect(code).contains('onClick: () => [_on_click, _on_click1]')
+      expect(code).contains(`onClick: () => [
+    _on_click,
+    _on_click1
+  ]`)
     })
 
     test('props merging: style', () => {

+ 33 - 2
packages/compiler-vapor/__tests__/transforms/vModel.spec.ts

@@ -6,11 +6,13 @@ import {
   transformVModel,
 } from '../../src'
 import { BindingTypes, DOMErrorCodes } from '@vue/compiler-dom'
+import { transformVOn } from '../../src/transforms/vOn'
 
 const compileWithVModel = makeCompile({
   nodeTransforms: [transformElement, transformChildren],
   directiveTransforms: {
     model: transformVModel,
+    on: transformVOn,
   },
 })
 
@@ -248,8 +250,10 @@ describe('compiler: vModel transform', () => {
       const { code, ir } = compileWithVModel('<Comp v-model:[arg]="foo" />')
       expect(code).toMatchSnapshot()
       expect(code).contains(
-        `[_ctx.arg]: _ctx.foo,
-    ["onUpdate:" + _ctx.arg]: () => _value => (_ctx.foo = _value)`,
+        `() => ({
+      [_ctx.arg]: _ctx.foo,
+      ["onUpdate:" + _ctx.arg]: () => _value => (_ctx.foo = _value)
+    })`,
       )
       expect(ir.block.dynamic.children[0].operation).toMatchObject({
         type: IRNodeTypes.CREATE_COMPONENT_NODE,
@@ -345,7 +349,13 @@ describe('compiler: vModel transform', () => {
         '<Comp v-model:[foo].trim="foo" v-model:[bar].number="bar" />',
       )
       expect(code).toMatchSnapshot()
+      expect(code).contain(
+        '["onUpdate:" + _ctx.foo]: () => _value => (_ctx.foo = _value)',
+      )
       expect(code).contain(`[_ctx.foo + "Modifiers"]: () => ({ trim: true })`)
+      expect(code).contain(
+        '["onUpdate:" + _ctx.bar]: () => _value => (_ctx.bar = _value)',
+      )
       expect(code).contain(`[_ctx.bar + "Modifiers"]: () => ({ number: true })`)
       expect(ir.block.dynamic.children[0].operation).toMatchObject({
         type: IRNodeTypes.CREATE_COMPONENT_NODE,
@@ -366,5 +376,26 @@ describe('compiler: vModel transform', () => {
         ],
       })
     })
+
+    test('component v-model should merge with explicit @update:modelValue', () => {
+      const { code } = compileWithVModel(
+        '<Comp v-model="counter" @update:modelValue="onUpdate" />',
+        {
+          inline: true,
+          bindingMetadata: {
+            counter: BindingTypes.SETUP_REF,
+            onUpdate: BindingTypes.SETUP_CONST,
+          },
+        },
+      )
+
+      expect(code).toMatchSnapshot()
+      expect(code).toContain(
+        `"onUpdate:modelValue": () => [
+      _value => (counter.value = _value),
+      onUpdate
+    ]`,
+      )
+    })
   })
 })

+ 181 - 34
packages/compiler-vapor/src/generators/component.ts

@@ -1,4 +1,10 @@
-import { camelize, extend, getModifierPropName, isArray } from '@vue/shared'
+import {
+  camelize,
+  extend,
+  getModifierPropName,
+  isArray,
+  toHandlerKey,
+} from '@vue/shared'
 import type { CodegenContext } from '../generate'
 import {
   type BlockIRNode,
@@ -67,7 +73,7 @@ export function genCreateComponent(
 
   const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
     (acc, { name, value }: InlineHandler) => {
-      const handler = genEventHandler(context, [value], undefined, false)
+      const handler = genEventHandler(context, [value], undefined, false, false)
       return [...acc, `const ${name} = `, ...handler, NEWLINE]
     },
     [],
@@ -191,7 +197,138 @@ function genStaticProps(
   context: CodegenContext,
   dynamicProps?: CodeFragment[],
 ): CodeFragment[] {
-  const args = props.map(prop => genProp(prop, context, true))
+  const args: CodeFragment[][] = []
+
+  type HandlerGroup = {
+    keyFrag: CodeFragment[]
+    handlers: CodeFragment[][]
+    index: number
+  }
+  const handlerGroups = new Map<string, HandlerGroup>()
+
+  const ensureHandlerGroup = (
+    keyName: string,
+    keyFrag: CodeFragment[],
+  ): HandlerGroup => {
+    let group = handlerGroups.get(keyName)
+    if (!group) {
+      const index = args.length
+      // placeholder, filled later
+      args.push([])
+      group = { keyFrag, handlers: [], index }
+      handlerGroups.set(keyName, group)
+    }
+    return group
+  }
+
+  const addHandler = (
+    keyName: string,
+    keyFrag: CodeFragment[],
+    handlerExp: CodeFragment[],
+  ) => {
+    ensureHandlerGroup(keyName, keyFrag).handlers.push(handlerExp)
+  }
+
+  const getStaticPropKeyName = (prop: IRProp): string | undefined => {
+    if (!prop.key.isStatic) return
+    const handlerModifierPostfix =
+      prop.handlerModifiers && prop.handlerModifiers.options
+        ? prop.handlerModifiers.options
+            .map(m => m.charAt(0).toUpperCase() + m.slice(1))
+            .join('')
+        : ''
+    const keyName =
+      (prop.handler
+        ? toHandlerKey(camelize(prop.key.content))
+        : prop.key.content) + handlerModifierPostfix
+    return keyName
+  }
+
+  for (const prop of props) {
+    if (prop.handler) {
+      const keyName = getStaticPropKeyName(prop)
+      if (!keyName) {
+        // dynamic key handlers are emitted as-is
+        args.push(genProp(prop, context, true))
+        continue
+      }
+
+      const keyFrag = genPropKey(prop, context)
+      const hasModifiers =
+        !!prop.handlerModifiers &&
+        (prop.handlerModifiers.keys.length > 0 ||
+          prop.handlerModifiers.nonKeys.length > 0)
+
+      if (hasModifiers || prop.values.length <= 1) {
+        const handlerExp = genEventHandler(
+          context,
+          prop.values,
+          prop.handlerModifiers,
+          true,
+          false,
+        )
+        addHandler(keyName, keyFrag, handlerExp)
+      } else {
+        // no modifiers: flatten multiple handler values
+        for (const value of prop.values) {
+          const handlerExp = genEventHandler(
+            context,
+            [value],
+            prop.handlerModifiers,
+            true,
+            false,
+          )
+          addHandler(keyName, keyFrag, handlerExp)
+        }
+      }
+      continue
+    }
+
+    // normal (non-handler) props
+    args.push(genProp(prop, context, true))
+
+    // v-model on component: synthesize onUpdate:* and modifiers props, and
+    // dedupe/merge with user provided @update:* handlers.
+    if (prop.model) {
+      // onUpdate:* handler
+      if (prop.key.isStatic) {
+        const keyName = `onUpdate:${camelize(prop.key.content)}`
+        const keyFrag: CodeFragment[] = [JSON.stringify(keyName)]
+        addHandler(keyName, keyFrag, genModelHandler(prop.values[0], context))
+      } else {
+        const keyFrag: CodeFragment[] = [
+          '["onUpdate:" + ',
+          ...genExpression(prop.key, context),
+          ']',
+        ]
+        args.push([
+          ...keyFrag,
+          ': () => ',
+          ...genModelHandler(prop.values[0], context),
+        ])
+      }
+
+      // modelModifiers prop
+      const { key, modelModifiers } = prop
+      if (modelModifiers && modelModifiers.length) {
+        const modifiersKey = key.isStatic
+          ? [getModifierPropName(key.content)]
+          : ['[', ...genExpression(key, context), ' + "Modifiers"]']
+        const modifiersVal = genDirectiveModifiers(modelModifiers)
+        args.push([...modifiersKey, `: () => ({ ${modifiersVal} })`])
+      }
+    }
+  }
+
+  // fill handler placeholders
+  for (const group of handlerGroups.values()) {
+    const handlerValue =
+      group.handlers.length > 1
+        ? genMulti(DELIMITERS_ARRAY_NEWLINE, ...group.handlers)
+        : group.handlers[0]
+    args[group.index] = [...group.keyFrag, ': () => ', ...handlerValue]
+  }
+
   if (dynamicProps) {
     args.push([`$: `, ...dynamicProps])
   }
@@ -215,9 +352,45 @@ function genDynamicProps(
       }
       continue
     } else {
-      if (p.kind === IRDynamicPropsKind.ATTRIBUTE)
-        expr = genMulti(DELIMITERS_OBJECT, genProp(p, context))
-      else {
+      if (p.kind === IRDynamicPropsKind.ATTRIBUTE) {
+        if (p.model) {
+          const entries: CodeFragment[][] = [genProp(p, context)]
+
+          // onUpdate:* handler for component v-model with dynamic argument
+          const updateKey = p.key.isStatic
+            ? ([
+                JSON.stringify(`onUpdate:${camelize(p.key.content)}`),
+              ] as CodeFragment[])
+            : ([
+                '["onUpdate:" + ',
+                ...genExpression(p.key, context),
+                ']',
+              ] as CodeFragment[])
+          entries.push([
+            ...updateKey,
+            ': () => ',
+            ...genModelHandler(p.values[0], context),
+          ])
+
+          // *Modifiers
+          const { modelModifiers } = p
+          if (modelModifiers && modelModifiers.length) {
+            const modifiersKey = p.key.isStatic
+              ? ([getModifierPropName(p.key.content)] as CodeFragment[])
+              : ([
+                  '[',
+                  ...genExpression(p.key, context),
+                  ' + "Modifiers"]',
+                ] as CodeFragment[])
+            const modifiersVal = genDirectiveModifiers(modelModifiers)
+            entries.push([...modifiersKey, `: () => ({ ${modifiersVal} })`])
+          }
+
+          expr = genMulti(DELIMITERS_OBJECT_NEWLINE, ...entries)
+        } else {
+          expr = genMulti(DELIMITERS_OBJECT, genProp(p, context))
+        }
+      } else {
         expr = genExpression(p.value, context)
         if (p.handler)
           expr = genCall(
@@ -245,41 +418,15 @@ function genProp(prop: IRProp, context: CodegenContext, isStatic?: boolean) {
           context,
           prop.values,
           prop.handlerModifiers,
-          true /* wrap handlers passed to components */,
+          true /* asComponentProp */,
+          true /* wrapInGetter */,
         )
       : isStatic
         ? ['() => (', ...values, ')']
         : values),
-    ...(prop.model
-      ? [...genModelEvent(prop, context), ...genModelModifiers(prop, context)]
-      : []),
   ]
 }
 
-function genModelEvent(prop: IRProp, context: CodegenContext): CodeFragment[] {
-  const name = prop.key.isStatic
-    ? [JSON.stringify(`onUpdate:${camelize(prop.key.content)}`)]
-    : ['["onUpdate:" + ', ...genExpression(prop.key, context), ']']
-  const handler = genModelHandler(prop.values[0], context)
-
-  return [',', NEWLINE, ...name, ': () => ', ...handler]
-}
-
-function genModelModifiers(
-  prop: IRProp,
-  context: CodegenContext,
-): CodeFragment[] {
-  const { key, modelModifiers } = prop
-  if (!modelModifiers || !modelModifiers.length) return []
-
-  const modifiersKey = key.isStatic
-    ? [getModifierPropName(key.content)]
-    : ['[', ...genExpression(key, context), ' + "Modifiers"]']
-
-  const modifiersVal = genDirectiveModifiers(modelModifiers)
-  return [',', NEWLINE, ...modifiersKey, `: () => ({ ${modifiersVal} })`]
-}
-
 function genRawSlots(slots: IRSlots[], context: CodegenContext) {
   if (!slots.length) return
   const staticSlots = slots[0]

+ 5 - 2
packages/compiler-vapor/src/generators/event.ts

@@ -117,7 +117,10 @@ export function genEventHandler(
     nonKeys: string[]
     keys: string[]
   } = { nonKeys: [], keys: [] },
-  // passed as component prop - need additional wrap
+  // when true, generate handler expressions suitable for passing as component
+  // props (avoid wrapping member expressions with invocation).
+  asComponentProp: boolean = false,
+  // when true, wrap the result in a getter function `() => ...`.
   extraWrap: boolean = false,
 ): CodeFragment[] {
   let handlerExp: CodeFragment[] = []
@@ -130,7 +133,7 @@ export function genEventHandler(
         if (isMemberExpression(value, context.options)) {
           // e.g. @click="foo.bar"
           exp = genExpression(value, context)
-          if (!isConstantBinding(value, context) && !extraWrap) {
+          if (!isConstantBinding(value, context) && !asComponentProp) {
             // non constant, wrap with invocation as `e => foo.bar(e)`
             // when passing as component handler, access is always dynamic so we
             // can skip this