parent
c1d5928f3b
commit
f42d11e8e1
@ -10,13 +10,18 @@ import {
|
|||||||
import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode'
|
import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode'
|
||||||
import { warn } from '../warning'
|
import { warn } from '../warning'
|
||||||
import { onBeforeUnmount, injectHook, onUnmounted } from '../apiLifecycle'
|
import { onBeforeUnmount, injectHook, onUnmounted } from '../apiLifecycle'
|
||||||
import { isString, isArray, ShapeFlags, remove } from '@vue/shared'
|
import {
|
||||||
|
isString,
|
||||||
|
isArray,
|
||||||
|
ShapeFlags,
|
||||||
|
remove,
|
||||||
|
invokeArrayFns
|
||||||
|
} from '@vue/shared'
|
||||||
import { watch } from '../apiWatch'
|
import { watch } from '../apiWatch'
|
||||||
import { SuspenseBoundary } from './Suspense'
|
import { SuspenseBoundary } from './Suspense'
|
||||||
import {
|
import {
|
||||||
RendererInternals,
|
RendererInternals,
|
||||||
queuePostRenderEffect,
|
queuePostRenderEffect,
|
||||||
invokeHooks,
|
|
||||||
MoveType,
|
MoveType,
|
||||||
RendererElement,
|
RendererElement,
|
||||||
RendererNode
|
RendererNode
|
||||||
@ -106,7 +111,7 @@ const KeepAliveImpl = {
|
|||||||
queuePostRenderEffect(() => {
|
queuePostRenderEffect(() => {
|
||||||
child.isDeactivated = false
|
child.isDeactivated = false
|
||||||
if (child.a) {
|
if (child.a) {
|
||||||
invokeHooks(child.a)
|
invokeArrayFns(child.a)
|
||||||
}
|
}
|
||||||
}, parentSuspense)
|
}, parentSuspense)
|
||||||
}
|
}
|
||||||
@ -116,7 +121,7 @@ const KeepAliveImpl = {
|
|||||||
queuePostRenderEffect(() => {
|
queuePostRenderEffect(() => {
|
||||||
const component = vnode.component!
|
const component = vnode.component!
|
||||||
if (component.da) {
|
if (component.da) {
|
||||||
invokeHooks(component.da)
|
invokeArrayFns(component.da)
|
||||||
}
|
}
|
||||||
component.isDeactivated = true
|
component.isDeactivated = true
|
||||||
}, parentSuspense)
|
}, parentSuspense)
|
||||||
|
@ -32,7 +32,8 @@ import {
|
|||||||
PatchFlags,
|
PatchFlags,
|
||||||
ShapeFlags,
|
ShapeFlags,
|
||||||
NOOP,
|
NOOP,
|
||||||
hasOwn
|
hasOwn,
|
||||||
|
invokeArrayFns
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
queueJob,
|
queueJob,
|
||||||
@ -40,13 +41,7 @@ import {
|
|||||||
flushPostFlushCbs,
|
flushPostFlushCbs,
|
||||||
invalidateJob
|
invalidateJob
|
||||||
} from './scheduler'
|
} from './scheduler'
|
||||||
import {
|
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
|
||||||
effect,
|
|
||||||
stop,
|
|
||||||
ReactiveEffectOptions,
|
|
||||||
isRef,
|
|
||||||
DebuggerEvent
|
|
||||||
} from '@vue/reactivity'
|
|
||||||
import { resolveProps } from './componentProps'
|
import { resolveProps } from './componentProps'
|
||||||
import { resolveSlots } from './componentSlots'
|
import { resolveSlots } from './componentSlots'
|
||||||
import { pushWarningContext, popWarningContext, warn } from './warning'
|
import { pushWarningContext, popWarningContext, warn } from './warning'
|
||||||
@ -265,14 +260,8 @@ function createDevEffectOptions(
|
|||||||
): ReactiveEffectOptions {
|
): ReactiveEffectOptions {
|
||||||
return {
|
return {
|
||||||
scheduler: queueJob,
|
scheduler: queueJob,
|
||||||
onTrack: instance.rtc ? e => invokeHooks(instance.rtc!, e) : void 0,
|
onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0,
|
||||||
onTrigger: instance.rtg ? e => invokeHooks(instance.rtg!, e) : void 0
|
onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function invokeHooks(hooks: Function[], arg?: DebuggerEvent) {
|
|
||||||
for (let i = 0; i < hooks.length; i++) {
|
|
||||||
hooks[i](arg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1106,7 +1095,7 @@ function baseCreateRenderer(
|
|||||||
}
|
}
|
||||||
// beforeMount hook
|
// beforeMount hook
|
||||||
if (bm) {
|
if (bm) {
|
||||||
invokeHooks(bm)
|
invokeArrayFns(bm)
|
||||||
}
|
}
|
||||||
// onVnodeBeforeMount
|
// onVnodeBeforeMount
|
||||||
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
|
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
|
||||||
@ -1189,7 +1178,7 @@ function baseCreateRenderer(
|
|||||||
next.el = vnode.el
|
next.el = vnode.el
|
||||||
// beforeUpdate hook
|
// beforeUpdate hook
|
||||||
if (bu) {
|
if (bu) {
|
||||||
invokeHooks(bu)
|
invokeArrayFns(bu)
|
||||||
}
|
}
|
||||||
// onVnodeBeforeUpdate
|
// onVnodeBeforeUpdate
|
||||||
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
|
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
|
||||||
@ -1812,7 +1801,7 @@ function baseCreateRenderer(
|
|||||||
const { bum, effects, update, subTree, um, da, isDeactivated } = instance
|
const { bum, effects, update, subTree, um, da, isDeactivated } = instance
|
||||||
// beforeUnmount hook
|
// beforeUnmount hook
|
||||||
if (bum) {
|
if (bum) {
|
||||||
invokeHooks(bum)
|
invokeArrayFns(bum)
|
||||||
}
|
}
|
||||||
if (effects) {
|
if (effects) {
|
||||||
for (let i = 0; i < effects.length; i++) {
|
for (let i = 0; i < effects.length; i++) {
|
||||||
|
@ -5,7 +5,8 @@ import {
|
|||||||
defineComponent,
|
defineComponent,
|
||||||
vModelDynamic,
|
vModelDynamic,
|
||||||
withDirectives,
|
withDirectives,
|
||||||
VNode
|
VNode,
|
||||||
|
ref
|
||||||
} from '@vue/runtime-dom'
|
} from '@vue/runtime-dom'
|
||||||
|
|
||||||
const triggerEvent = (type: string, el: Element) => {
|
const triggerEvent = (type: string, el: Element) => {
|
||||||
@ -58,6 +59,72 @@ describe('vModel', () => {
|
|||||||
expect(input.value).toEqual('bar')
|
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 () => {
|
it('should work with textarea', async () => {
|
||||||
const component = defineComponent({
|
const component = defineComponent({
|
||||||
data() {
|
data() {
|
||||||
|
@ -6,10 +6,14 @@ import {
|
|||||||
warn
|
warn
|
||||||
} from '@vue/runtime-core'
|
} from '@vue/runtime-core'
|
||||||
import { addEventListener } from '../modules/events'
|
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) =>
|
type AssignerFn = (value: any) => void
|
||||||
vnode.props!['onUpdate:modelValue']
|
|
||||||
|
const getModelAssigner = (vnode: VNode): AssignerFn => {
|
||||||
|
const fn = vnode.props!['onUpdate:modelValue']
|
||||||
|
return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
|
||||||
|
}
|
||||||
|
|
||||||
function onCompositionStart(e: Event) {
|
function onCompositionStart(e: Event) {
|
||||||
;(e.target as any).composing = true
|
;(e.target as any).composing = true
|
||||||
@ -34,14 +38,16 @@ function toNumber(val: string): number | string {
|
|||||||
return isNaN(n) ? val : n
|
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
|
// 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.
|
// be tree-shaken in case v-model is never used.
|
||||||
export const vModelText: ObjectDirective<
|
export const vModelText: ModelDirective<
|
||||||
HTMLInputElement | HTMLTextAreaElement
|
HTMLInputElement | HTMLTextAreaElement
|
||||||
> = {
|
> = {
|
||||||
beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
|
beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
|
||||||
el.value = value
|
el.value = value
|
||||||
const assign = getModelAssigner(vnode)
|
el._assign = getModelAssigner(vnode)
|
||||||
const castToNumber = number || el.type === 'number'
|
const castToNumber = number || el.type === 'number'
|
||||||
addEventListener(el, lazy ? 'change' : 'input', () => {
|
addEventListener(el, lazy ? 'change' : 'input', () => {
|
||||||
let domValue: string | number = el.value
|
let domValue: string | number = el.value
|
||||||
@ -50,7 +56,7 @@ export const vModelText: ObjectDirective<
|
|||||||
} else if (castToNumber) {
|
} else if (castToNumber) {
|
||||||
domValue = toNumber(domValue)
|
domValue = toNumber(domValue)
|
||||||
}
|
}
|
||||||
assign(domValue)
|
el._assign(domValue)
|
||||||
})
|
})
|
||||||
if (trim) {
|
if (trim) {
|
||||||
addEventListener(el, 'change', () => {
|
addEventListener(el, 'change', () => {
|
||||||
@ -67,7 +73,8 @@ export const vModelText: ObjectDirective<
|
|||||||
addEventListener(el, 'change', onCompositionEnd)
|
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) {
|
if (value === oldValue) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -83,14 +90,15 @@ export const vModelText: ObjectDirective<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
|
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
|
||||||
beforeMount(el, binding, vnode) {
|
beforeMount(el, binding, vnode) {
|
||||||
setChecked(el, binding, vnode)
|
setChecked(el, binding, vnode)
|
||||||
const assign = getModelAssigner(vnode)
|
el._assign = getModelAssigner(vnode)
|
||||||
addEventListener(el, 'change', () => {
|
addEventListener(el, 'change', () => {
|
||||||
const modelValue = (el as any)._modelValue
|
const modelValue = (el as any)._modelValue
|
||||||
const elementValue = getValue(el)
|
const elementValue = getValue(el)
|
||||||
const checked = el.checked
|
const checked = el.checked
|
||||||
|
const assign = el._assign
|
||||||
if (isArray(modelValue)) {
|
if (isArray(modelValue)) {
|
||||||
const index = looseIndexOf(modelValue, elementValue)
|
const index = looseIndexOf(modelValue, elementValue)
|
||||||
const found = index !== -1
|
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(
|
function setChecked(
|
||||||
@ -124,33 +135,37 @@ function setChecked(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vModelRadio: ObjectDirective<HTMLInputElement> = {
|
export const vModelRadio: ModelDirective<HTMLInputElement> = {
|
||||||
beforeMount(el, { value }, vnode) {
|
beforeMount(el, { value }, vnode) {
|
||||||
el.checked = looseEqual(value, vnode.props!.value)
|
el.checked = looseEqual(value, vnode.props!.value)
|
||||||
const assign = getModelAssigner(vnode)
|
el._assign = getModelAssigner(vnode)
|
||||||
addEventListener(el, 'change', () => {
|
addEventListener(el, 'change', () => {
|
||||||
assign(getValue(el))
|
el._assign(getValue(el))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
beforeUpdate(el, { value, oldValue }, vnode) {
|
beforeUpdate(el, { value, oldValue }, vnode) {
|
||||||
|
el._assign = getModelAssigner(vnode)
|
||||||
if (value !== oldValue) {
|
if (value !== oldValue) {
|
||||||
el.checked = looseEqual(value, vnode.props!.value)
|
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.
|
// use mounted & updated because <select> relies on its children <option>s.
|
||||||
mounted(el, { value }, vnode) {
|
mounted(el, { value }, vnode) {
|
||||||
setSelected(el, value)
|
setSelected(el, value)
|
||||||
const assign = getModelAssigner(vnode)
|
el._assign = getModelAssigner(vnode)
|
||||||
addEventListener(el, 'change', () => {
|
addEventListener(el, 'change', () => {
|
||||||
const selectedVal = Array.prototype.filter
|
const selectedVal = Array.prototype.filter
|
||||||
.call(el.options, (o: HTMLOptionElement) => o.selected)
|
.call(el.options, (o: HTMLOptionElement) => o.selected)
|
||||||
.map(getValue)
|
.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 }) {
|
updated(el, { value }) {
|
||||||
setSelected(el, value)
|
setSelected(el, value)
|
||||||
}
|
}
|
||||||
|
@ -119,3 +119,9 @@ export const toDisplayString = (val: unknown): string => {
|
|||||||
? JSON.stringify(val, null, 2)
|
? JSON.stringify(val, null, 2)
|
||||||
: String(val)
|
: String(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function invokeArrayFns(fns: Function[], arg?: any) {
|
||||||
|
for (let i = 0; i < fns.length; i++) {
|
||||||
|
fns[i](arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user