fix(v-on): refactor DOM event options modifer handling
fix #1567 Previously multiple `v-on` handlers with different event attach option modifers (`.once`, `.capture` and `.passive`) are generated as an array of objects in the form of `[{ handler, options }]` - however, this makes it pretty complex for `runtime-dom` to properly handle all possible value permutations, as each handler may need to be attached with different options. With this commit, they are now generated as event props with different keys - e.g. `v-on:click.capture` is now generated as a prop named `onClick.capture`. This allows them to be patched as separate props which makes the runtime handling much simpler.
This commit is contained in:
@@ -160,30 +160,61 @@ describe('component: emit', () => {
|
||||
expect(`event validation failed for event "foo"`).toHaveBeenWarned()
|
||||
})
|
||||
|
||||
test('isEmitListener', () => {
|
||||
const def1 = { emits: ['click'] }
|
||||
expect(isEmitListener(def1, 'onClick')).toBe(true)
|
||||
expect(isEmitListener(def1, 'onclick')).toBe(false)
|
||||
expect(isEmitListener(def1, 'onBlick')).toBe(false)
|
||||
test('.once', () => {
|
||||
const Foo = defineComponent({
|
||||
render() {},
|
||||
emits: {
|
||||
foo: null
|
||||
},
|
||||
created() {
|
||||
this.$emit('foo')
|
||||
this.$emit('foo')
|
||||
}
|
||||
})
|
||||
const fn = jest.fn()
|
||||
render(
|
||||
h(Foo, {
|
||||
'onFoo.once': fn
|
||||
}),
|
||||
nodeOps.createElement('div')
|
||||
)
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
const def2 = { emits: { click: null } }
|
||||
expect(isEmitListener(def2, 'onClick')).toBe(true)
|
||||
expect(isEmitListener(def2, 'onclick')).toBe(false)
|
||||
expect(isEmitListener(def2, 'onBlick')).toBe(false)
|
||||
describe('isEmitListener', () => {
|
||||
test('array option', () => {
|
||||
const def1 = { emits: ['click'] }
|
||||
expect(isEmitListener(def1, 'onClick')).toBe(true)
|
||||
expect(isEmitListener(def1, 'onclick')).toBe(false)
|
||||
expect(isEmitListener(def1, 'onBlick')).toBe(false)
|
||||
})
|
||||
|
||||
const mixin1 = { emits: ['foo'] }
|
||||
const mixin2 = { emits: ['bar'] }
|
||||
const extend = { emits: ['baz'] }
|
||||
const def3 = {
|
||||
emits: { click: null },
|
||||
mixins: [mixin1, mixin2],
|
||||
extends: extend
|
||||
}
|
||||
expect(isEmitListener(def3, 'onClick')).toBe(true)
|
||||
expect(isEmitListener(def3, 'onFoo')).toBe(true)
|
||||
expect(isEmitListener(def3, 'onBar')).toBe(true)
|
||||
expect(isEmitListener(def3, 'onBaz')).toBe(true)
|
||||
expect(isEmitListener(def3, 'onclick')).toBe(false)
|
||||
expect(isEmitListener(def3, 'onBlick')).toBe(false)
|
||||
test('object option', () => {
|
||||
const def2 = { emits: { click: null } }
|
||||
expect(isEmitListener(def2, 'onClick')).toBe(true)
|
||||
expect(isEmitListener(def2, 'onclick')).toBe(false)
|
||||
expect(isEmitListener(def2, 'onBlick')).toBe(false)
|
||||
})
|
||||
|
||||
test('with mixins and extends', () => {
|
||||
const mixin1 = { emits: ['foo'] }
|
||||
const mixin2 = { emits: ['bar'] }
|
||||
const extend = { emits: ['baz'] }
|
||||
const def3 = {
|
||||
mixins: [mixin1, mixin2],
|
||||
extends: extend
|
||||
}
|
||||
expect(isEmitListener(def3, 'onFoo')).toBe(true)
|
||||
expect(isEmitListener(def3, 'onBar')).toBe(true)
|
||||
expect(isEmitListener(def3, 'onBaz')).toBe(true)
|
||||
expect(isEmitListener(def3, 'onclick')).toBe(false)
|
||||
expect(isEmitListener(def3, 'onBlick')).toBe(false)
|
||||
})
|
||||
|
||||
test('.once listeners', () => {
|
||||
const def2 = { emits: { click: null } }
|
||||
expect(isEmitListener(def2, 'onClick.once')).toBe(true)
|
||||
expect(isEmitListener(def2, 'onclick.once')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -246,6 +246,8 @@ export interface ComponentInternalInstance {
|
||||
slots: InternalSlots
|
||||
refs: Data
|
||||
emit: EmitFn
|
||||
// used for keeping track of .once event handlers on components
|
||||
emitted: Record<string, boolean> | null
|
||||
|
||||
/**
|
||||
* setup related
|
||||
@@ -396,7 +398,8 @@ export function createComponentInstance(
|
||||
rtg: null,
|
||||
rtc: null,
|
||||
ec: null,
|
||||
emit: null as any // to be set immediately
|
||||
emit: null as any, // to be set immediately
|
||||
emitted: null
|
||||
}
|
||||
if (__DEV__) {
|
||||
instance.ctx = createRenderContext(instance)
|
||||
|
||||
@@ -67,12 +67,21 @@ export function emit(
|
||||
}
|
||||
}
|
||||
|
||||
let handler = props[`on${capitalize(event)}`]
|
||||
let handlerName = `on${capitalize(event)}`
|
||||
let handler = props[handlerName]
|
||||
// for v-model update:xxx events, also trigger kebab-case equivalent
|
||||
// for props passed via kebab-case
|
||||
if (!handler && event.startsWith('update:')) {
|
||||
event = hyphenate(event)
|
||||
handler = props[`on${capitalize(event)}`]
|
||||
handlerName = `on${capitalize(hyphenate(event))}`
|
||||
handler = props[handlerName]
|
||||
}
|
||||
if (!handler) {
|
||||
handler = props[handlerName + `.once`]
|
||||
if (!instance.emitted) {
|
||||
;(instance.emitted = {} as Record<string, boolean>)[handlerName] = true
|
||||
} else if (instance.emitted[handlerName]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (handler) {
|
||||
callWithAsyncErrorHandling(
|
||||
@@ -123,13 +132,13 @@ function normalizeEmitsOptions(
|
||||
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
|
||||
// both considered matched listeners.
|
||||
export function isEmitListener(comp: Component, key: string): boolean {
|
||||
if (!isOn(key)) {
|
||||
let emits: ObjectEmitsOptions | undefined
|
||||
if (!isOn(key) || !(emits = normalizeEmitsOptions(comp))) {
|
||||
return false
|
||||
}
|
||||
const emits = normalizeEmitsOptions(comp)
|
||||
key = key.replace(/\.once$/, '')
|
||||
return (
|
||||
!!emits &&
|
||||
(hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
|
||||
hasOwn(emits, key.slice(2)))
|
||||
hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
|
||||
hasOwn(emits, key.slice(2))
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user