@@ -5,7 +5,8 @@ import {
|
||||
defineComponent,
|
||||
vModelDynamic,
|
||||
withDirectives,
|
||||
VNode
|
||||
VNode,
|
||||
ref
|
||||
} from '@vue/runtime-dom'
|
||||
|
||||
const triggerEvent = (type: string, el: Element) => {
|
||||
@@ -58,6 +59,72 @@ describe('vModel', () => {
|
||||
expect(input.value).toEqual('bar')
|
||||
})
|
||||
|
||||
it('should work with multiple listeners', async () => {
|
||||
const spy = jest.fn()
|
||||
const component = defineComponent({
|
||||
data() {
|
||||
return { value: null }
|
||||
},
|
||||
render() {
|
||||
return [
|
||||
withVModel(
|
||||
h('input', {
|
||||
'onUpdate:modelValue': [setValue.bind(this), spy]
|
||||
}),
|
||||
this.value
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
render(h(component), root)
|
||||
|
||||
const input = root.querySelector('input')!
|
||||
const data = root._vnode.component.data
|
||||
|
||||
input.value = 'foo'
|
||||
triggerEvent('input', input)
|
||||
await nextTick()
|
||||
expect(data.value).toEqual('foo')
|
||||
expect(spy).toHaveBeenCalledWith('foo')
|
||||
})
|
||||
|
||||
it('should work with updated listeners', async () => {
|
||||
const spy1 = jest.fn()
|
||||
const spy2 = jest.fn()
|
||||
const toggle = ref(true)
|
||||
|
||||
const component = defineComponent({
|
||||
render() {
|
||||
return [
|
||||
withVModel(
|
||||
h('input', {
|
||||
'onUpdate:modelValue': toggle.value ? spy1 : spy2
|
||||
}),
|
||||
'foo'
|
||||
)
|
||||
]
|
||||
}
|
||||
})
|
||||
render(h(component), root)
|
||||
|
||||
const input = root.querySelector('input')!
|
||||
|
||||
input.value = 'foo'
|
||||
triggerEvent('input', input)
|
||||
await nextTick()
|
||||
expect(spy1).toHaveBeenCalledWith('foo')
|
||||
|
||||
// udpate listener
|
||||
toggle.value = false
|
||||
await nextTick()
|
||||
|
||||
input.value = 'bar'
|
||||
triggerEvent('input', input)
|
||||
await nextTick()
|
||||
expect(spy1).not.toHaveBeenCalledWith('bar')
|
||||
expect(spy2).toHaveBeenCalledWith('bar')
|
||||
})
|
||||
|
||||
it('should work with textarea', async () => {
|
||||
const component = defineComponent({
|
||||
data() {
|
||||
|
||||
@@ -6,10 +6,14 @@ import {
|
||||
warn
|
||||
} from '@vue/runtime-core'
|
||||
import { addEventListener } from '../modules/events'
|
||||
import { isArray, looseEqual, looseIndexOf } from '@vue/shared'
|
||||
import { isArray, looseEqual, looseIndexOf, invokeArrayFns } from '@vue/shared'
|
||||
|
||||
const getModelAssigner = (vnode: VNode): ((value: any) => void) =>
|
||||
vnode.props!['onUpdate:modelValue']
|
||||
type AssignerFn = (value: any) => void
|
||||
|
||||
const getModelAssigner = (vnode: VNode): AssignerFn => {
|
||||
const fn = vnode.props!['onUpdate:modelValue']
|
||||
return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
|
||||
}
|
||||
|
||||
function onCompositionStart(e: Event) {
|
||||
;(e.target as any).composing = true
|
||||
@@ -34,14 +38,16 @@ function toNumber(val: string): number | string {
|
||||
return isNaN(n) ? val : n
|
||||
}
|
||||
|
||||
type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>
|
||||
|
||||
// We are exporting the v-model runtime directly as vnode hooks so that it can
|
||||
// be tree-shaken in case v-model is never used.
|
||||
export const vModelText: ObjectDirective<
|
||||
export const vModelText: ModelDirective<
|
||||
HTMLInputElement | HTMLTextAreaElement
|
||||
> = {
|
||||
beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
|
||||
el.value = value
|
||||
const assign = getModelAssigner(vnode)
|
||||
el._assign = getModelAssigner(vnode)
|
||||
const castToNumber = number || el.type === 'number'
|
||||
addEventListener(el, lazy ? 'change' : 'input', () => {
|
||||
let domValue: string | number = el.value
|
||||
@@ -50,7 +56,7 @@ export const vModelText: ObjectDirective<
|
||||
} else if (castToNumber) {
|
||||
domValue = toNumber(domValue)
|
||||
}
|
||||
assign(domValue)
|
||||
el._assign(domValue)
|
||||
})
|
||||
if (trim) {
|
||||
addEventListener(el, 'change', () => {
|
||||
@@ -67,7 +73,8 @@ export const vModelText: ObjectDirective<
|
||||
addEventListener(el, 'change', onCompositionEnd)
|
||||
}
|
||||
},
|
||||
beforeUpdate(el, { value, oldValue, modifiers: { trim, number } }) {
|
||||
beforeUpdate(el, { value, oldValue, modifiers: { trim, number } }, vnode) {
|
||||
el._assign = getModelAssigner(vnode)
|
||||
if (value === oldValue) {
|
||||
return
|
||||
}
|
||||
@@ -83,14 +90,15 @@ export const vModelText: ObjectDirective<
|
||||
}
|
||||
}
|
||||
|
||||
export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
|
||||
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
|
||||
beforeMount(el, binding, vnode) {
|
||||
setChecked(el, binding, vnode)
|
||||
const assign = getModelAssigner(vnode)
|
||||
el._assign = getModelAssigner(vnode)
|
||||
addEventListener(el, 'change', () => {
|
||||
const modelValue = (el as any)._modelValue
|
||||
const elementValue = getValue(el)
|
||||
const checked = el.checked
|
||||
const assign = el._assign
|
||||
if (isArray(modelValue)) {
|
||||
const index = looseIndexOf(modelValue, elementValue)
|
||||
const found = index !== -1
|
||||
@@ -106,7 +114,10 @@ export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeUpdate: setChecked
|
||||
beforeUpdate(el, binding, vnode) {
|
||||
setChecked(el, binding, vnode)
|
||||
el._assign = getModelAssigner(vnode)
|
||||
}
|
||||
}
|
||||
|
||||
function setChecked(
|
||||
@@ -124,33 +135,37 @@ function setChecked(
|
||||
}
|
||||
}
|
||||
|
||||
export const vModelRadio: ObjectDirective<HTMLInputElement> = {
|
||||
export const vModelRadio: ModelDirective<HTMLInputElement> = {
|
||||
beforeMount(el, { value }, vnode) {
|
||||
el.checked = looseEqual(value, vnode.props!.value)
|
||||
const assign = getModelAssigner(vnode)
|
||||
el._assign = getModelAssigner(vnode)
|
||||
addEventListener(el, 'change', () => {
|
||||
assign(getValue(el))
|
||||
el._assign(getValue(el))
|
||||
})
|
||||
},
|
||||
beforeUpdate(el, { value, oldValue }, vnode) {
|
||||
el._assign = getModelAssigner(vnode)
|
||||
if (value !== oldValue) {
|
||||
el.checked = looseEqual(value, vnode.props!.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const vModelSelect: ObjectDirective<HTMLSelectElement> = {
|
||||
export const vModelSelect: ModelDirective<HTMLSelectElement> = {
|
||||
// use mounted & updated because <select> relies on its children <option>s.
|
||||
mounted(el, { value }, vnode) {
|
||||
setSelected(el, value)
|
||||
const assign = getModelAssigner(vnode)
|
||||
el._assign = getModelAssigner(vnode)
|
||||
addEventListener(el, 'change', () => {
|
||||
const selectedVal = Array.prototype.filter
|
||||
.call(el.options, (o: HTMLOptionElement) => o.selected)
|
||||
.map(getValue)
|
||||
assign(el.multiple ? selectedVal : selectedVal[0])
|
||||
el._assign(el.multiple ? selectedVal : selectedVal[0])
|
||||
})
|
||||
},
|
||||
beforeUpdate(el, _binding, vnode) {
|
||||
el._assign = getModelAssigner(vnode)
|
||||
},
|
||||
updated(el, { value }) {
|
||||
setSelected(el, value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user