PatchFlags,
ShapeFlags,
NOOP,
- hasOwn
+ hasOwn,
+ invokeArrayFns
} from '@vue/shared'
import {
queueJob,
flushPostFlushCbs,
invalidateJob
} from './scheduler'
-import {
- effect,
- stop,
- ReactiveEffectOptions,
- isRef,
- DebuggerEvent
-} from '@vue/reactivity'
+import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
import { resolveProps } from './componentProps'
import { resolveSlots } from './componentSlots'
import { pushWarningContext, popWarningContext, warn } from './warning'
): ReactiveEffectOptions {
return {
scheduler: queueJob,
- onTrack: instance.rtc ? e => invokeHooks(instance.rtc!, e) : void 0,
- onTrigger: instance.rtg ? e => invokeHooks(instance.rtg!, e) : void 0
- }
-}
-
-export function invokeHooks(hooks: Function[], arg?: DebuggerEvent) {
- for (let i = 0; i < hooks.length; i++) {
- hooks[i](arg)
+ onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0,
+ onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0
}
}
}
// beforeMount hook
if (bm) {
- invokeHooks(bm)
+ invokeArrayFns(bm)
}
// onVnodeBeforeMount
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
next.el = vnode.el
// beforeUpdate hook
if (bu) {
- invokeHooks(bu)
+ invokeArrayFns(bu)
}
// onVnodeBeforeUpdate
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
const { bum, effects, update, subTree, um, da, isDeactivated } = instance
// beforeUnmount hook
if (bum) {
- invokeHooks(bum)
+ invokeArrayFns(bum)
}
if (effects) {
for (let i = 0; i < effects.length; i++) {
defineComponent,
vModelDynamic,
withDirectives,
- VNode
+ VNode,
+ ref
} from '@vue/runtime-dom'
const triggerEvent = (type: string, el: Element) => {
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() {
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
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
} else if (castToNumber) {
domValue = toNumber(domValue)
}
- assign(domValue)
+ el._assign(domValue)
})
if (trim) {
addEventListener(el, 'change', () => {
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
}
}
}
-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
}
})
},
- beforeUpdate: setChecked
+ beforeUpdate(el, binding, vnode) {
+ setChecked(el, binding, vnode)
+ el._assign = getModelAssigner(vnode)
+ }
}
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)
}