6eb3399311
fix #949 BREAKING CHANGE: Only props starting with `on` followed by an uppercase letter or a non-letter character are considered event listeners.
117 lines
2.9 KiB
TypeScript
117 lines
2.9 KiB
TypeScript
import {
|
|
isArray,
|
|
isOn,
|
|
hasOwn,
|
|
EMPTY_OBJ,
|
|
capitalize,
|
|
hyphenate,
|
|
isFunction,
|
|
def
|
|
} from '@vue/shared'
|
|
import { ComponentInternalInstance } from './component'
|
|
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
|
|
import { warn } from './warning'
|
|
|
|
export type ObjectEmitsOptions = Record<
|
|
string,
|
|
((...args: any[]) => any) | null
|
|
>
|
|
export type EmitsOptions = ObjectEmitsOptions | string[]
|
|
|
|
type UnionToIntersection<U> = (U extends any
|
|
? (k: U) => void
|
|
: never) extends ((k: infer I) => void)
|
|
? I
|
|
: never
|
|
|
|
export type EmitFn<
|
|
Options = ObjectEmitsOptions,
|
|
Event extends keyof Options = keyof Options
|
|
> = Options extends any[]
|
|
? (event: Options[0], ...args: any[]) => void
|
|
: UnionToIntersection<
|
|
{
|
|
[key in Event]: Options[key] extends ((...args: infer Args) => any)
|
|
? (event: key, ...args: Args) => void
|
|
: (event: key, ...args: any[]) => void
|
|
}[Event]
|
|
>
|
|
|
|
export function emit(
|
|
instance: ComponentInternalInstance,
|
|
event: string,
|
|
...args: any[]
|
|
) {
|
|
const props = instance.vnode.props || EMPTY_OBJ
|
|
|
|
if (__DEV__) {
|
|
const options = normalizeEmitsOptions(instance.type.emits)
|
|
if (options) {
|
|
if (!(event in options)) {
|
|
warn(
|
|
`Component emitted event "${event}" but it is not declared in the ` +
|
|
`emits option.`
|
|
)
|
|
} else {
|
|
const validator = options[event]
|
|
if (isFunction(validator)) {
|
|
const isValid = validator(...args)
|
|
if (!isValid) {
|
|
warn(
|
|
`Invalid event arguments: event validation failed for event "${event}".`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let handler = props[`on${capitalize(event)}`]
|
|
// for v-model update:xxx events, also trigger kebab-case equivalent
|
|
// for props passed via kebab-case
|
|
if (!handler && event.indexOf('update:') === 0) {
|
|
event = hyphenate(event)
|
|
handler = props[`on${capitalize(event)}`]
|
|
}
|
|
if (handler) {
|
|
callWithAsyncErrorHandling(
|
|
handler,
|
|
instance,
|
|
ErrorCodes.COMPONENT_EVENT_HANDLER,
|
|
args
|
|
)
|
|
}
|
|
}
|
|
|
|
export function normalizeEmitsOptions(
|
|
options: EmitsOptions | undefined
|
|
): ObjectEmitsOptions | undefined {
|
|
if (!options) {
|
|
return
|
|
} else if (isArray(options)) {
|
|
if ((options as any)._n) {
|
|
return (options as any)._n
|
|
}
|
|
const normalized: ObjectEmitsOptions = {}
|
|
options.forEach(key => (normalized[key] = null))
|
|
def(options, '_n', normalized)
|
|
return normalized
|
|
} else {
|
|
return options
|
|
}
|
|
}
|
|
|
|
// Check if an incoming prop key is a declared emit event listener.
|
|
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
|
|
// both considered matched listeners.
|
|
export function isEmitListener(emits: EmitsOptions, key: string): boolean {
|
|
return (
|
|
isOn(key) &&
|
|
(hasOwn(
|
|
(emits = normalizeEmitsOptions(emits) as ObjectEmitsOptions),
|
|
key[2].toLowerCase() + key.slice(3)
|
|
) ||
|
|
hasOwn(emits, key.slice(2)))
|
|
)
|
|
}
|