]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-core): emits validation and warnings
authorEvan You <yyx990803@gmail.com>
Sat, 4 Apr 2020 00:40:34 +0000 (20:40 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 4 Apr 2020 00:40:34 +0000 (20:40 -0400)
packages/runtime-core/__tests__/componentEmits.spec.ts [new file with mode: 0644]
packages/runtime-core/src/componentEmits.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentProps.ts

diff --git a/packages/runtime-core/__tests__/componentEmits.spec.ts b/packages/runtime-core/__tests__/componentEmits.spec.ts
new file mode 100644 (file)
index 0000000..331b814
--- /dev/null
@@ -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)
+  })
+})
index b2720653c82a39423df1290781f6f9814ed923a8..78a78ef51d54a97e33c897c016a04a9a947e324a 100644 (file)
@@ -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,
@@ -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
@@ -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)))
   )
 }
index d939731672467c9a9188b49098d94a50d9806b3a..842e9a0cebc6034f06c1b7d55feeefdc146a1d24 100644 (file)
@@ -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
@@ -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> & {
@@ -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> & {
index eb746b367a084f7ea7fa26f4d205078deff8a7d3..f34bfafd41adac526945d045bce5fe480c4a8703 100644 (file)
@@ -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>
@@ -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