diff --git a/packages/runtime-dom/__tests__/directives/vModel.spec.ts b/packages/runtime-dom/__tests__/directives/vModel.spec.ts index 357e827be..bb263a3a1 100644 --- a/packages/runtime-dom/__tests__/directives/vModel.spec.ts +++ b/packages/runtime-dom/__tests__/directives/vModel.spec.ts @@ -189,6 +189,101 @@ describe('vModel', () => { data.value = false await nextTick() expect(input.checked).toEqual(false) + + data.value = true + await nextTick() + expect(input.checked).toEqual(true) + + input.checked = false + triggerEvent('change', input) + await nextTick() + expect(data.value).toEqual(false) + }) + + it('should work with checkbox and true-value/false-value', async () => { + const component = createComponent({ + data() { + return { value: null } + }, + render() { + return [ + withVModel( + h('input', { + type: 'checkbox', + 'true-value': 'yes', + 'false-value': 'no', + 'onUpdate:modelValue': setValue.bind(this) + }), + this.value + ) + ] + } + }) + app.mount(component, root) + + const input = root.querySelector('input') + const data = root._vnode.component.data + + input.checked = true + triggerEvent('change', input) + await nextTick() + expect(data.value).toEqual('yes') + + data.value = 'no' + await nextTick() + expect(input.checked).toEqual(false) + + data.value = 'yes' + await nextTick() + expect(input.checked).toEqual(true) + + input.checked = false + triggerEvent('change', input) + await nextTick() + expect(data.value).toEqual('no') + }) + + it('should work with checkbox and true-value/false-value with object values', async () => { + const component = createComponent({ + data() { + return { value: null } + }, + render() { + return [ + withVModel( + h('input', { + type: 'checkbox', + 'true-value': { yes: 'yes' }, + 'false-value': { no: 'no' }, + 'onUpdate:modelValue': setValue.bind(this) + }), + this.value + ) + ] + } + }) + app.mount(component, root) + + const input = root.querySelector('input') + const data = root._vnode.component.data + + input.checked = true + triggerEvent('change', input) + await nextTick() + expect(data.value).toEqual({ yes: 'yes' }) + + data.value = { no: 'no' } + await nextTick() + expect(input.checked).toEqual(false) + + data.value = { yes: 'yes' } + await nextTick() + expect(input.checked).toEqual(true) + + input.checked = false + triggerEvent('change', input) + await nextTick() + expect(data.value).toEqual({ no: 'no' }) }) it(`should support array as a checkbox model`, async () => { diff --git a/packages/runtime-dom/src/directives/vModel.ts b/packages/runtime-dom/src/directives/vModel.ts index 6c3092770..8cc9ea7b8 100644 --- a/packages/runtime-dom/src/directives/vModel.ts +++ b/packages/runtime-dom/src/directives/vModel.ts @@ -101,7 +101,7 @@ export const vModelCheckbox: ObjectDirective = { assign(filtered) } } else { - assign(checked) + assign(getCheckboxValue(el, checked)) } }) }, @@ -119,7 +119,7 @@ function setChecked( if (isArray(value)) { el.checked = looseIndexOf(value, vnode.props!.value) > -1 } else if (value !== oldValue) { - el.checked = !!value + el.checked = looseEqual(value, getCheckboxValue(el, true)) } } @@ -228,6 +228,15 @@ function getValue(el: HTMLOptionElement | HTMLInputElement) { return '_value' in el ? (el as any)._value : el.value } +// retrieve raw value for true-value and false-value set via :true-value or :false-value bindings +function getCheckboxValue( + el: HTMLInputElement & { _trueValue?: any; _falseValue?: any }, + checked: boolean +) { + const key = checked ? '_trueValue' : '_falseValue' + return key in el ? el[key] : checked +} + export const vModelDynamic: ObjectDirective< HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement > = { diff --git a/packages/runtime-dom/src/patchProp.ts b/packages/runtime-dom/src/patchProp.ts index a74faa88e..1f8ff8133 100644 --- a/packages/runtime-dom/src/patchProp.ts +++ b/packages/runtime-dom/src/patchProp.ts @@ -5,9 +5,9 @@ import { patchDOMProp } from './modules/props' import { patchEvent } from './modules/events' import { isOn } from '@vue/shared' import { - VNode, ComponentInternalInstance, - SuspenseBoundary + SuspenseBoundary, + VNode } from '@vue/runtime-core' export function patchProp( @@ -53,6 +53,15 @@ export function patchProp( unmountChildren ) } else { + // special case for with + // :true-value & :false-value + // store value as dom properties since non-string values will be + // stringified. + if (key === 'true-value') { + ;(el as any)._trueValue = nextValue + } else if (key === 'false-value') { + ;(el as any)._falseValue = nextValue + } patchAttr(el, key, nextValue) } break