--- /dev/null
+// Note: emits and listener fallthrough is tested in
+// ./rendererAttrsFallthrough.spec.ts.
+
+import { mockWarn } from '@vue/shared'
+import { render, defineComponent, h, nodeOps } from '@vue/runtime-test'
+import { isEmitListener } from '../src/componentEmits'
+
+describe('emits option', () => {
+ mockWarn()
+
+ test('trigger both raw event and capitalize handlers', () => {
+ const Foo = defineComponent({
+ render() {},
+ created() {
+ // the `emit` function is bound on component instances
+ this.$emit('foo')
+ this.$emit('bar')
+ }
+ })
+
+ const onfoo = jest.fn()
+ const onBar = jest.fn()
+ const Comp = () => h(Foo, { onfoo, onBar })
+ render(h(Comp), nodeOps.createElement('div'))
+
+ expect(onfoo).toHaveBeenCalled()
+ expect(onBar).toHaveBeenCalled()
+ })
+
+ test('trigger hyphendated events for update:xxx events', () => {
+ const Foo = defineComponent({
+ render() {},
+ created() {
+ this.$emit('update:fooProp')
+ this.$emit('update:barProp')
+ }
+ })
+
+ const fooSpy = jest.fn()
+ const barSpy = jest.fn()
+ const Comp = () =>
+ h(Foo, {
+ 'onUpdate:fooProp': fooSpy,
+ 'onUpdate:bar-prop': barSpy
+ })
+ render(h(Comp), nodeOps.createElement('div'))
+
+ expect(fooSpy).toHaveBeenCalled()
+ expect(barSpy).toHaveBeenCalled()
+ })
+
+ test('warning for undeclared event (array)', () => {
+ const Foo = defineComponent({
+ emits: ['foo'],
+ render() {},
+ created() {
+ // @ts-ignore
+ this.$emit('bar')
+ }
+ })
+ render(h(Foo), nodeOps.createElement('div'))
+ expect(
+ `Component emitted event "bar" but it is not declared`
+ ).toHaveBeenWarned()
+ })
+
+ test('warning for undeclared event (object)', () => {
+ const Foo = defineComponent({
+ emits: {
+ foo: null
+ },
+ render() {},
+ created() {
+ // @ts-ignore
+ this.$emit('bar')
+ }
+ })
+ render(h(Foo), nodeOps.createElement('div'))
+ expect(
+ `Component emitted event "bar" but it is not declared`
+ ).toHaveBeenWarned()
+ })
+
+ test('validator warning', () => {
+ const Foo = defineComponent({
+ emits: {
+ foo: (arg: number) => arg > 0
+ },
+ render() {},
+ created() {
+ this.$emit('foo', -1)
+ }
+ })
+ render(h(Foo), nodeOps.createElement('div'))
+ expect(`event validation failed for event "foo"`).toHaveBeenWarned()
+ })
+
+ test('isEmitListener', () => {
+ expect(isEmitListener(['click'], 'onClick')).toBe(true)
+ expect(isEmitListener(['click'], 'onclick')).toBe(true)
+ expect(isEmitListener({ click: null }, 'onClick')).toBe(true)
+ expect(isEmitListener({ click: null }, 'onclick')).toBe(true)
+ expect(isEmitListener(['click'], 'onBlick')).toBe(false)
+ expect(isEmitListener({ click: null }, 'onBlick')).toBe(false)
+ })
+})
hasOwn,
EMPTY_OBJ,
capitalize,
- hyphenate
+ hyphenate,
+ isFunction
} from '@vue/shared'
import { ComponentInternalInstance } from './component'
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
+import { warn } from './warning'
export type ObjectEmitsOptions = Record<
string,
...args: any[]
): 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${event}`] || props[`on${capitalize(event)}`]
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
// 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: ObjectEmitsOptions,
- key: string
-): boolean {
+export function isEmitListener(emits: EmitsOptions, key: string): boolean {
return (
isOn(key) &&
- (hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
+ (hasOwn(
+ (emits = normalizeEmitsOptions(emits) as ObjectEmitsOptions),
+ key[2].toLowerCase() + key.slice(3)
+ ) ||
hasOwn(emits, key.slice(2)))
)
}
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
- E extends EmitsOptions = Record<string, any>,
+ E extends EmitsOptions = EmitsOptions,
EE extends string = string
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
props?: undefined
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
- E extends EmitsOptions = Record<string, any>,
+ E extends EmitsOptions = EmitsOptions,
EE extends string = string,
Props = Readonly<{ [key in PropNames]?: any }>
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
- E extends EmitsOptions = Record<string, any>,
+ E extends EmitsOptions = EmitsOptions,
EE extends string = string,
Props = Readonly<ExtractPropTypes<PropsOptions>>
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
} from '@vue/shared'
import { warn } from './warning'
import { Data, ComponentInternalInstance } from './component'
-import { normalizeEmitsOptions, isEmitListener } from './componentEmits'
+import { isEmitListener } from './componentEmits'
export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P>
}
const { 0: options, 1: needCastKeys } = normalizePropsOptions(_options)!
- const emits = normalizeEmitsOptions(instance.type.emits)
+ const emits = instance.type.emits
const props: Data = {}
let attrs: Data | undefined = undefined