Skip to content

Commit 10ccb9b

Browse files
fix(defineModel): support kebab-case/camelCase mismatches (#9950)
1 parent f300a40 commit 10ccb9b

File tree

2 files changed

+85
-2
lines changed

2 files changed

+85
-2
lines changed

packages/runtime-core/__tests__/apiSetupHelpers.spec.ts

+78
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,84 @@ describe('SFC <script setup> helpers', () => {
314314
expect(serializeInner(root)).toBe('bar')
315315
})
316316

317+
test('kebab-case v-model (should not be local)', async () => {
318+
let foo: any
319+
320+
const compRender = vi.fn()
321+
const Comp = defineComponent({
322+
props: ['fooBar'],
323+
emits: ['update:fooBar'],
324+
setup(props) {
325+
foo = useModel(props, 'fooBar')
326+
return () => {
327+
compRender()
328+
return foo.value
329+
}
330+
},
331+
})
332+
333+
const updateFooBar = vi.fn()
334+
const root = nodeOps.createElement('div')
335+
// v-model:foo-bar compiles to foo-bar and onUpdate:fooBar
336+
render(
337+
h(Comp, { 'foo-bar': 'initial', 'onUpdate:fooBar': updateFooBar }),
338+
root,
339+
)
340+
expect(compRender).toBeCalledTimes(1)
341+
expect(serializeInner(root)).toBe('initial')
342+
343+
expect(foo.value).toBe('initial')
344+
foo.value = 'bar'
345+
// should not be using local mode, so nothing should actually change
346+
expect(foo.value).toBe('initial')
347+
348+
await nextTick()
349+
expect(compRender).toBeCalledTimes(1)
350+
expect(updateFooBar).toBeCalledTimes(1)
351+
expect(updateFooBar).toHaveBeenCalledWith('bar')
352+
expect(foo.value).toBe('initial')
353+
expect(serializeInner(root)).toBe('initial')
354+
})
355+
356+
test('kebab-case update listener (should not be local)', async () => {
357+
let foo: any
358+
359+
const compRender = vi.fn()
360+
const Comp = defineComponent({
361+
props: ['fooBar'],
362+
emits: ['update:fooBar'],
363+
setup(props) {
364+
foo = useModel(props, 'fooBar')
365+
return () => {
366+
compRender()
367+
return foo.value
368+
}
369+
},
370+
})
371+
372+
const updateFooBar = vi.fn()
373+
const root = nodeOps.createElement('div')
374+
// The template compiler won't create hyphenated listeners, but it could have been passed manually
375+
render(
376+
h(Comp, { 'foo-bar': 'initial', 'onUpdate:foo-bar': updateFooBar }),
377+
root,
378+
)
379+
expect(compRender).toBeCalledTimes(1)
380+
expect(serializeInner(root)).toBe('initial')
381+
382+
expect(foo.value).toBe('initial')
383+
foo.value = 'bar'
384+
// should not be using local mode, so nothing should actually change
385+
expect(foo.value).toBe('initial')
386+
387+
await nextTick()
388+
expect(compRender).toBeCalledTimes(1)
389+
expect(updateFooBar).toBeCalledTimes(1)
390+
expect(updateFooBar).toHaveBeenCalledWith('bar')
391+
expect(foo.value).toBe('initial')
392+
expect(serializeInner(root)).toBe('initial')
393+
})
394+
317395
test('default value', async () => {
318396
let count: any
319397
const inc = () => {

packages/runtime-core/src/apiSetupHelpers.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
camelize,
77
extend,
88
hasChanged,
9+
hyphenate,
910
isArray,
1011
isFunction,
1112
isPromise,
@@ -382,6 +383,7 @@ export function useModel(
382383
}
383384

384385
const camelizedName = camelize(name)
386+
const hyphenatedName = hyphenate(name)
385387

386388
const res = customRef((track, trigger) => {
387389
let localValue: any
@@ -403,9 +405,12 @@ export function useModel(
403405
!(
404406
rawProps &&
405407
// check if parent has passed v-model
406-
(name in rawProps || camelizedName in rawProps) &&
408+
(name in rawProps ||
409+
camelizedName in rawProps ||
410+
hyphenatedName in rawProps) &&
407411
(`onUpdate:${name}` in rawProps ||
408-
`onUpdate:${camelizedName}` in rawProps)
412+
`onUpdate:${camelizedName}` in rawProps ||
413+
`onUpdate:${hyphenatedName}` in rawProps)
409414
) &&
410415
hasChanged(value, localValue)
411416
) {

0 commit comments

Comments
 (0)