]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core/emits): merge emits options from mixins/extends
authorEvan You <yyx990803@gmail.com>
Mon, 13 Jul 2020 15:55:46 +0000 (11:55 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 13 Jul 2020 15:55:46 +0000 (11:55 -0400)
fix #1562

packages/runtime-core/__tests__/componentEmits.spec.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentEmits.ts
packages/runtime-core/src/componentProps.ts

index 17bde516036c76cb135e71c96bf354da61ea087d..30b030eaa036f63d5933f5801e89cc98065efc3c 100644 (file)
@@ -143,12 +143,47 @@ describe('component: emit', () => {
     expect(`event validation failed for event "foo"`).toHaveBeenWarned()
   })
 
+  test('merging from mixins', () => {
+    const mixin = {
+      emits: {
+        foo: (arg: number) => arg > 0
+      }
+    }
+    const Foo = defineComponent({
+      mixins: [mixin],
+      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(false)
-    expect(isEmitListener({ click: null }, 'onClick')).toBe(true)
-    expect(isEmitListener({ click: null }, 'onclick')).toBe(false)
-    expect(isEmitListener(['click'], 'onBlick')).toBe(false)
-    expect(isEmitListener({ click: null }, 'onBlick')).toBe(false)
+    const def1 = { emits: ['click'] }
+    expect(isEmitListener(def1, 'onClick')).toBe(true)
+    expect(isEmitListener(def1, 'onclick')).toBe(false)
+    expect(isEmitListener(def1, 'onBlick')).toBe(false)
+
+    const def2 = { emits: { click: null } }
+    expect(isEmitListener(def2, 'onClick')).toBe(true)
+    expect(isEmitListener(def2, 'onclick')).toBe(false)
+    expect(isEmitListener(def2, '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)
   })
 })
index b467d57948148fae2cc9b4af0c3ec393d04c4415..5869c2b70c9410012f9f9610304eb0551eeb6cb3 100644 (file)
@@ -59,6 +59,10 @@ export interface ComponentInternalOptions {
    * @internal
    */
   __props?: NormalizedPropsOptions | []
+  /**
+   * @internal
+   */
+  __emits?: ObjectEmitsOptions
   /**
    * @internal
    */
index 69298112f677566a4638417f5e54ff63e21e4e4e..c44b3e1516fe9c663680e22ccad13a79f2bacf23 100644 (file)
@@ -6,9 +6,9 @@ import {
   capitalize,
   hyphenate,
   isFunction,
-  def
+  extend
 } from '@vue/shared'
-import { ComponentInternalInstance } from './component'
+import { ComponentInternalInstance, Component } from './component'
 import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
 import { warn } from './warning'
 import { normalizePropsOptions } from './componentProps'
@@ -43,7 +43,7 @@ export function emit(
   const props = instance.vnode.props || EMPTY_OBJ
 
   if (__DEV__) {
-    const options = normalizeEmitsOptions(instance.type.emits)
+    const options = normalizeEmitsOptions(instance.type)
     if (options) {
       if (!(event in options)) {
         const propsOptions = normalizePropsOptions(instance.type)[0]
@@ -84,34 +84,52 @@ export function emit(
   }
 }
 
-export function normalizeEmitsOptions(
-  options: EmitsOptions | undefined
+function normalizeEmitsOptions(
+  comp: Component
 ): ObjectEmitsOptions | undefined {
-  if (!options) {
-    return
-  } else if (isArray(options)) {
-    if ((options as any)._n) {
-      return (options as any)._n
+  if (hasOwn(comp, '__emits')) {
+    return comp.__emits
+  }
+
+  const raw = comp.emits
+  let normalized: ObjectEmitsOptions = {}
+
+  // apply mixin/extends props
+  let hasExtends = false
+  if (__FEATURE_OPTIONS__ && !isFunction(comp)) {
+    if (comp.extends) {
+      hasExtends = true
+      extend(normalized, normalizeEmitsOptions(comp.extends))
     }
-    const normalized: ObjectEmitsOptions = {}
-    options.forEach(key => (normalized[key] = null))
-    def(options, '_n', normalized)
-    return normalized
+    if (comp.mixins) {
+      hasExtends = true
+      comp.mixins.forEach(m => extend(normalized, normalizeEmitsOptions(m)))
+    }
+  }
+
+  if (!raw && !hasExtends) {
+    return (comp.__emits = undefined)
+  }
+
+  if (isArray(raw)) {
+    raw.forEach(key => (normalized[key] = null))
   } else {
-    return options
+    extend(normalized, raw)
   }
+  return (comp.__emits = normalized)
 }
 
 // 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 {
+export function isEmitListener(comp: Component, key: string): boolean {
+  if (!isOn(key)) {
+    return false
+  }
+  const emits = normalizeEmitsOptions(comp)
   return (
-    isOn(key) &&
-    (hasOwn(
-      (emits = normalizeEmitsOptions(emits) as ObjectEmitsOptions),
-      key[2].toLowerCase() + key.slice(3)
-    ) ||
+    !!emits &&
+    (hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
       hasOwn(emits, key.slice(2)))
   )
 }
index 73aaec26b4d6b4e90a1b3ded88cade8571613401..e0814c74b2f562e71c1050fb73c2674e23adcf64 100644 (file)
@@ -242,8 +242,6 @@ function setFullProps(
   attrs: Data
 ) {
   const [options, needCastKeys] = normalizePropsOptions(instance.type)
-  const emits = instance.type.emits
-
   if (rawProps) {
     for (const key in rawProps) {
       const value = rawProps[key]
@@ -256,7 +254,7 @@ function setFullProps(
       let camelKey
       if (options && hasOwn(options, (camelKey = camelize(key)))) {
         props[camelKey] = value
-      } else if (!emits || !isEmitListener(emits, key)) {
+      } else if (!isEmitListener(instance.type, key)) {
         // Any non-declared (either as a prop or an emitted event) props are put
         // into a separate `attrs` object for spreading. Make sure to preserve
         // original key casing