Skip to content

Commit

Permalink
feat(runtime-core): emits validation and warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Apr 4, 2020
1 parent 24e9efc commit c7c3a6a
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 11 deletions.
106 changes: 106 additions & 0 deletions packages/runtime-core/__tests__/componentEmits.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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)
})
})
37 changes: 31 additions & 6 deletions packages/runtime-core/src/componentEmits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import {
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,
Expand Down Expand Up @@ -40,6 +42,29 @@ export function emit(
...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
Expand Down Expand Up @@ -81,13 +106,13 @@ export function normalizeEmitsOptions(
// 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)))
)
}
6 changes: 3 additions & 3 deletions packages/runtime-core/src/componentOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export type ComponentOptionsWithoutProps<
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
Expand All @@ -116,7 +116,7 @@ export type ComponentOptionsWithArrayProps<
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> & {
Expand All @@ -129,7 +129,7 @@ export type ComponentOptionsWithObjectProps<
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> & {
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/componentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} 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>
Expand Down Expand Up @@ -115,7 +115,7 @@ export function resolveProps(
}

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

Expand Down

0 comments on commit c7c3a6a

Please sign in to comment.