From ba3b3cdda98f6efb5d4c4fafc579b8f568a19bde Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 13 Jul 2020 11:55:46 -0400 Subject: [PATCH] fix(runtime-core/emits): merge emits options from mixins/extends fix #1562 --- .../__tests__/componentEmits.spec.ts | 47 ++++++++++++-- packages/runtime-core/src/component.ts | 4 ++ packages/runtime-core/src/componentEmits.ts | 64 ++++++++++++------- packages/runtime-core/src/componentProps.ts | 4 +- 4 files changed, 87 insertions(+), 32 deletions(-) diff --git a/packages/runtime-core/__tests__/componentEmits.spec.ts b/packages/runtime-core/__tests__/componentEmits.spec.ts index 17bde516..30b030ea 100644 --- a/packages/runtime-core/__tests__/componentEmits.spec.ts +++ b/packages/runtime-core/__tests__/componentEmits.spec.ts @@ -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) }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index b467d579..5869c2b7 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -59,6 +59,10 @@ export interface ComponentInternalOptions { * @internal */ __props?: NormalizedPropsOptions | [] + /** + * @internal + */ + __emits?: ObjectEmitsOptions /** * @internal */ diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index 69298112..c44b3e15 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -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 - } - const normalized: ObjectEmitsOptions = {} - options.forEach(key => (normalized[key] = null)) - def(options, '_n', normalized) - return normalized - } else { - return options + 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)) + } + 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 { + 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))) ) } diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 73aaec26..e0814c74 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -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