fix(runtime-vapor): trigger event after `v-model` (#137)

This commit is contained in:
FireBushtree 2024-03-01 18:23:49 +08:00 committed by GitHub
parent 5a0bc110d9
commit ccd3f3923f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 152 additions and 21 deletions

View File

@ -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')
})
})

View File

@ -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

View File

@ -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

View File

@ -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 = {

View File

@ -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 {

View File

@ -70,7 +70,7 @@ function queueFlush() {
}
}
function flushPostFlushCbs() {
export function flushPostFlushCbs() {
if (!pendingPostFlushCbs.length) return
const deduped = [...new Set(pendingPostFlushCbs)]