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()
|
||||
const { change, changeRender } = instance.setupState as any
|
||||
|
||||
expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0'])
|
||||
calls.length = 0
|
||||
|
||||
await nextTick()
|
||||
expect(calls).toEqual(['post 0'])
|
||||
expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0', 'post 0'])
|
||||
calls.length = 0
|
||||
|
||||
// Update
|
||||
|
|
|
@ -50,7 +50,9 @@ export const vModelText: ObjectDirective<
|
|||
const assigner = getModelAssigner(el)
|
||||
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 => {
|
||||
if ((e.target as any).composing) return
|
||||
let domValue: string | number = el.value
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from '@vue/reactivity'
|
||||
import { MetadataKind, getMetadata, recordEventMetadata } from '../metadata'
|
||||
import { withKeys, withModifiers } from '@vue/runtime-dom'
|
||||
import { queuePostRenderEffect } from '../scheduler'
|
||||
|
||||
export function addEventListener(
|
||||
el: HTMLElement,
|
||||
|
@ -30,20 +31,22 @@ export function on(
|
|||
) {
|
||||
const handler: DelegatedHandler = eventHandler(handlerGetter, options)
|
||||
const cleanupMetadata = recordEventMetadata(el, event, handler)
|
||||
const cleanupEvent = addEventListener(el, event, handler, options)
|
||||
queuePostRenderEffect(() => {
|
||||
const cleanupEvent = addEventListener(el, event, handler, options)
|
||||
|
||||
function cleanup() {
|
||||
cleanupMetadata()
|
||||
cleanupEvent()
|
||||
}
|
||||
function cleanup() {
|
||||
cleanupMetadata()
|
||||
cleanupEvent()
|
||||
}
|
||||
|
||||
const scope = getCurrentScope()
|
||||
const effect = getCurrentEffect()
|
||||
if (effect && effect.scope === scope) {
|
||||
onEffectCleanup(cleanup)
|
||||
} else if (scope) {
|
||||
onScopeDispose(cleanup)
|
||||
}
|
||||
const scope = getCurrentScope()
|
||||
const effect = getCurrentEffect()
|
||||
if (effect && effect.scope === scope) {
|
||||
onEffectCleanup(cleanup)
|
||||
} else if (scope) {
|
||||
onScopeDispose(cleanup)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export type DelegatedHandler = {
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { initProps } from './componentProps'
|
||||
import { invokeDirectiveHook } from './directives'
|
||||
import { insert, querySelector, remove } from './dom/element'
|
||||
import { queuePostRenderEffect } from './scheduler'
|
||||
import { flushPostFlushCbs, queuePostRenderEffect } from './scheduler'
|
||||
|
||||
export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``)
|
||||
|
||||
|
@ -34,7 +34,12 @@ export function render(
|
|||
): ComponentInternalInstance {
|
||||
const instance = createComponentInstance(comp, props)
|
||||
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 {
|
||||
|
|
|
@ -70,7 +70,7 @@ function queueFlush() {
|
|||
}
|
||||
}
|
||||
|
||||
function flushPostFlushCbs() {
|
||||
export function flushPostFlushCbs() {
|
||||
if (!pendingPostFlushCbs.length) return
|
||||
|
||||
const deduped = [...new Set(pendingPostFlushCbs)]
|
||||
|
|
Loading…
Reference in New Issue