mirror of https://github.com/vuejs/core.git
fix(runtime-vapor): trigger event after `v-model` (#137)
This commit is contained in:
parent
5a0bc110d9
commit
ccd3f3923f
|
@ -0,0 +1,125 @@
|
||||||
|
import { ref } from '@vue/reactivity'
|
||||||
|
import {
|
||||||
|
on,
|
||||||
|
setDOMProp,
|
||||||
|
template,
|
||||||
|
vModelDynamic,
|
||||||
|
vModelSelect,
|
||||||
|
withDirectives,
|
||||||
|
} from '../../src'
|
||||||
|
import { makeRender } from '../_utils'
|
||||||
|
import { nextTick } from '@vue/runtime-dom'
|
||||||
|
|
||||||
|
const define = makeRender()
|
||||||
|
|
||||||
|
const triggerEvent = (type: string, el: Element) => {
|
||||||
|
const event = new Event(type)
|
||||||
|
el.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('directive: v-model', () => {
|
||||||
|
test('should work with text input', async () => {
|
||||||
|
const spy = vi.fn()
|
||||||
|
|
||||||
|
const data = ref<string | null | undefined>('')
|
||||||
|
const { host } = define(() => {
|
||||||
|
const t0 = template('<input />')
|
||||||
|
const n0 = t0() as HTMLInputElement
|
||||||
|
withDirectives(n0, [[vModelDynamic, () => data.value]])
|
||||||
|
on(n0, 'update:modelValue', () => val => (data.value = val))
|
||||||
|
on(n0, 'input', () => () => spy(data.value))
|
||||||
|
return n0
|
||||||
|
}).render()
|
||||||
|
|
||||||
|
const input = host.querySelector('input')!
|
||||||
|
expect(input.value).toEqual('')
|
||||||
|
|
||||||
|
input.value = 'foo'
|
||||||
|
triggerEvent('input', input)
|
||||||
|
await nextTick()
|
||||||
|
expect(data.value).toEqual('foo')
|
||||||
|
expect(spy).toHaveBeenCalledWith('foo')
|
||||||
|
|
||||||
|
data.value = 'bar'
|
||||||
|
await nextTick()
|
||||||
|
expect(input.value).toEqual('bar')
|
||||||
|
|
||||||
|
data.value = undefined
|
||||||
|
await nextTick()
|
||||||
|
expect(input.value).toEqual('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should work with select', async () => {
|
||||||
|
const spy = vi.fn()
|
||||||
|
const data = ref<string | null | undefined>('')
|
||||||
|
const { host } = define(() => {
|
||||||
|
const t0 = template(
|
||||||
|
'<select><option>red</option><option>green</option><option>blue</option></select>',
|
||||||
|
)
|
||||||
|
const n0 = t0() as HTMLInputElement
|
||||||
|
withDirectives(n0, [[vModelSelect, () => data.value]])
|
||||||
|
on(n0, 'update:modelValue', () => val => (data.value = val))
|
||||||
|
on(n0, 'change', () => () => spy(data.value))
|
||||||
|
return n0
|
||||||
|
}).render()
|
||||||
|
|
||||||
|
const select = host.querySelector('select')!
|
||||||
|
expect(select.value).toEqual('')
|
||||||
|
|
||||||
|
select.value = 'red'
|
||||||
|
triggerEvent('change', select)
|
||||||
|
await nextTick()
|
||||||
|
expect(data.value).toEqual('red')
|
||||||
|
expect(spy).toHaveBeenCalledWith('red')
|
||||||
|
|
||||||
|
data.value = 'blue'
|
||||||
|
await nextTick()
|
||||||
|
expect(select.value).toEqual('blue')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should work with number input', async () => {
|
||||||
|
const data = ref<number | null | undefined>(null)
|
||||||
|
const { host } = define(() => {
|
||||||
|
const t0 = template('<input />')
|
||||||
|
const n0 = t0() as HTMLInputElement
|
||||||
|
|
||||||
|
setDOMProp(n0, 'type', 'number')
|
||||||
|
withDirectives(n0, [[vModelDynamic, () => data.value]])
|
||||||
|
on(n0, 'update:modelValue', () => val => (data.value = val))
|
||||||
|
return n0
|
||||||
|
}).render()
|
||||||
|
|
||||||
|
const input = host.querySelector('input')!
|
||||||
|
expect(input.value).toEqual('')
|
||||||
|
expect(input.type).toEqual('number')
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
input.value = 1
|
||||||
|
triggerEvent('input', input)
|
||||||
|
await nextTick()
|
||||||
|
expect(typeof data.value).toEqual('number')
|
||||||
|
expect(data.value).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should work with multiple listeners', async () => {
|
||||||
|
const spy = vi.fn()
|
||||||
|
|
||||||
|
const data = ref<string | null | undefined>('')
|
||||||
|
const { host } = define(() => {
|
||||||
|
const t0 = template('<input />')
|
||||||
|
const n0 = t0() as HTMLInputElement
|
||||||
|
withDirectives(n0, [[vModelDynamic, () => data.value]])
|
||||||
|
on(n0, 'update:modelValue', () => val => (data.value = val))
|
||||||
|
on(n0, 'update:modelValue', () => spy)
|
||||||
|
return n0
|
||||||
|
}).render()
|
||||||
|
|
||||||
|
const input = host.querySelector('input')!
|
||||||
|
|
||||||
|
input.value = 'foo'
|
||||||
|
triggerEvent('input', input)
|
||||||
|
await nextTick()
|
||||||
|
expect(data.value).toEqual('foo')
|
||||||
|
expect(spy).toHaveBeenCalledWith('foo')
|
||||||
|
})
|
||||||
|
})
|
|
@ -114,11 +114,7 @@ describe('renderWatch', () => {
|
||||||
).render()
|
).render()
|
||||||
const { change, changeRender } = instance.setupState as any
|
const { change, changeRender } = instance.setupState as any
|
||||||
|
|
||||||
expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0'])
|
expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0', 'post 0'])
|
||||||
calls.length = 0
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
expect(calls).toEqual(['post 0'])
|
|
||||||
calls.length = 0
|
calls.length = 0
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
|
|
|
@ -50,7 +50,9 @@ export const vModelText: ObjectDirective<
|
||||||
const assigner = getModelAssigner(el)
|
const assigner = getModelAssigner(el)
|
||||||
assignFnMap.set(el, assigner)
|
assignFnMap.set(el, assigner)
|
||||||
|
|
||||||
const castToNumber = number // || (vnode.props && vnode.props.type === 'number')
|
const metadata = getMetadata(el)
|
||||||
|
const castToNumber = number || metadata[MetadataKind.prop].type === 'number'
|
||||||
|
|
||||||
addEventListener(el, lazy ? 'change' : 'input', e => {
|
addEventListener(el, lazy ? 'change' : 'input', e => {
|
||||||
if ((e.target as any).composing) return
|
if ((e.target as any).composing) return
|
||||||
let domValue: string | number = el.value
|
let domValue: string | number = el.value
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { MetadataKind, getMetadata, recordEventMetadata } from '../metadata'
|
import { MetadataKind, getMetadata, recordEventMetadata } from '../metadata'
|
||||||
import { withKeys, withModifiers } from '@vue/runtime-dom'
|
import { withKeys, withModifiers } from '@vue/runtime-dom'
|
||||||
|
import { queuePostRenderEffect } from '../scheduler'
|
||||||
|
|
||||||
export function addEventListener(
|
export function addEventListener(
|
||||||
el: HTMLElement,
|
el: HTMLElement,
|
||||||
|
@ -30,20 +31,22 @@ export function on(
|
||||||
) {
|
) {
|
||||||
const handler: DelegatedHandler = eventHandler(handlerGetter, options)
|
const handler: DelegatedHandler = eventHandler(handlerGetter, options)
|
||||||
const cleanupMetadata = recordEventMetadata(el, event, handler)
|
const cleanupMetadata = recordEventMetadata(el, event, handler)
|
||||||
const cleanupEvent = addEventListener(el, event, handler, options)
|
queuePostRenderEffect(() => {
|
||||||
|
const cleanupEvent = addEventListener(el, event, handler, options)
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
cleanupMetadata()
|
cleanupMetadata()
|
||||||
cleanupEvent()
|
cleanupEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = getCurrentScope()
|
const scope = getCurrentScope()
|
||||||
const effect = getCurrentEffect()
|
const effect = getCurrentEffect()
|
||||||
if (effect && effect.scope === scope) {
|
if (effect && effect.scope === scope) {
|
||||||
onEffectCleanup(cleanup)
|
onEffectCleanup(cleanup)
|
||||||
} else if (scope) {
|
} else if (scope) {
|
||||||
onScopeDispose(cleanup)
|
onScopeDispose(cleanup)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DelegatedHandler = {
|
export type DelegatedHandler = {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { initProps } from './componentProps'
|
import { initProps } from './componentProps'
|
||||||
import { invokeDirectiveHook } from './directives'
|
import { invokeDirectiveHook } from './directives'
|
||||||
import { insert, querySelector, remove } from './dom/element'
|
import { insert, querySelector, remove } from './dom/element'
|
||||||
import { queuePostRenderEffect } from './scheduler'
|
import { flushPostFlushCbs, queuePostRenderEffect } from './scheduler'
|
||||||
|
|
||||||
export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``)
|
export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``)
|
||||||
|
|
||||||
|
@ -34,7 +34,12 @@ export function render(
|
||||||
): ComponentInternalInstance {
|
): ComponentInternalInstance {
|
||||||
const instance = createComponentInstance(comp, props)
|
const instance = createComponentInstance(comp, props)
|
||||||
initProps(instance, props, !isFunction(instance.component))
|
initProps(instance, props, !isFunction(instance.component))
|
||||||
return mountComponent(instance, (container = normalizeContainer(container)))
|
const component = mountComponent(
|
||||||
|
instance,
|
||||||
|
(container = normalizeContainer(container)),
|
||||||
|
)
|
||||||
|
flushPostFlushCbs()
|
||||||
|
return component
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeContainer(container: string | ParentNode): ParentNode {
|
function normalizeContainer(container: string | ParentNode): ParentNode {
|
||||||
|
|
|
@ -70,7 +70,7 @@ function queueFlush() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushPostFlushCbs() {
|
export function flushPostFlushCbs() {
|
||||||
if (!pendingPostFlushCbs.length) return
|
if (!pendingPostFlushCbs.length) return
|
||||||
|
|
||||||
const deduped = [...new Set(pendingPostFlushCbs)]
|
const deduped = [...new Set(pendingPostFlushCbs)]
|
||||||
|
|
Loading…
Reference in New Issue