Преглед изворни кода

fix(defineModel): support kebab-case/camelCase mismatches (#9950)

skirtle пре 2 година
родитељ
комит
10ccb9bfa0

+ 78 - 0
packages/runtime-core/__tests__/apiSetupHelpers.spec.ts

@@ -314,6 +314,84 @@ describe('SFC <script setup> helpers', () => {
       expect(serializeInner(root)).toBe('bar')
     })
 
+    test('kebab-case v-model (should not be local)', async () => {
+      let foo: any
+
+      const compRender = vi.fn()
+      const Comp = defineComponent({
+        props: ['fooBar'],
+        emits: ['update:fooBar'],
+        setup(props) {
+          foo = useModel(props, 'fooBar')
+          return () => {
+            compRender()
+            return foo.value
+          }
+        },
+      })
+
+      const updateFooBar = vi.fn()
+      const root = nodeOps.createElement('div')
+      // v-model:foo-bar compiles to foo-bar and onUpdate:fooBar
+      render(
+        h(Comp, { 'foo-bar': 'initial', 'onUpdate:fooBar': updateFooBar }),
+        root,
+      )
+      expect(compRender).toBeCalledTimes(1)
+      expect(serializeInner(root)).toBe('initial')
+
+      expect(foo.value).toBe('initial')
+      foo.value = 'bar'
+      // should not be using local mode, so nothing should actually change
+      expect(foo.value).toBe('initial')
+
+      await nextTick()
+      expect(compRender).toBeCalledTimes(1)
+      expect(updateFooBar).toBeCalledTimes(1)
+      expect(updateFooBar).toHaveBeenCalledWith('bar')
+      expect(foo.value).toBe('initial')
+      expect(serializeInner(root)).toBe('initial')
+    })
+
+    test('kebab-case update listener (should not be local)', async () => {
+      let foo: any
+
+      const compRender = vi.fn()
+      const Comp = defineComponent({
+        props: ['fooBar'],
+        emits: ['update:fooBar'],
+        setup(props) {
+          foo = useModel(props, 'fooBar')
+          return () => {
+            compRender()
+            return foo.value
+          }
+        },
+      })
+
+      const updateFooBar = vi.fn()
+      const root = nodeOps.createElement('div')
+      // The template compiler won't create hyphenated listeners, but it could have been passed manually
+      render(
+        h(Comp, { 'foo-bar': 'initial', 'onUpdate:foo-bar': updateFooBar }),
+        root,
+      )
+      expect(compRender).toBeCalledTimes(1)
+      expect(serializeInner(root)).toBe('initial')
+
+      expect(foo.value).toBe('initial')
+      foo.value = 'bar'
+      // should not be using local mode, so nothing should actually change
+      expect(foo.value).toBe('initial')
+
+      await nextTick()
+      expect(compRender).toBeCalledTimes(1)
+      expect(updateFooBar).toBeCalledTimes(1)
+      expect(updateFooBar).toHaveBeenCalledWith('bar')
+      expect(foo.value).toBe('initial')
+      expect(serializeInner(root)).toBe('initial')
+    })
+
     test('default value', async () => {
       let count: any
       const inc = () => {

+ 7 - 2
packages/runtime-core/src/apiSetupHelpers.ts

@@ -6,6 +6,7 @@ import {
   camelize,
   extend,
   hasChanged,
+  hyphenate,
   isArray,
   isFunction,
   isPromise,
@@ -382,6 +383,7 @@ export function useModel(
   }
 
   const camelizedName = camelize(name)
+  const hyphenatedName = hyphenate(name)
 
   const res = customRef((track, trigger) => {
     let localValue: any
@@ -403,9 +405,12 @@ export function useModel(
           !(
             rawProps &&
             // check if parent has passed v-model
-            (name in rawProps || camelizedName in rawProps) &&
+            (name in rawProps ||
+              camelizedName in rawProps ||
+              hyphenatedName in rawProps) &&
             (`onUpdate:${name}` in rawProps ||
-              `onUpdate:${camelizedName}` in rawProps)
+              `onUpdate:${camelizedName}` in rawProps ||
+              `onUpdate:${hyphenatedName}` in rawProps)
           ) &&
           hasChanged(value, localValue)
         ) {