From c94ef02421d7422bc59d10cf2eee9f4e7dcea6c8 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Mon, 10 Apr 2023 02:06:21 -0500 Subject: [PATCH] fix(runtime-core): properly merge props and emits options from mixins (#8052) close #7989 --- .../__tests__/componentEmits.spec.ts | 54 ++++++++++++++++++- packages/runtime-core/src/apiSetupHelpers.ts | 9 +++- packages/runtime-core/src/componentOptions.ts | 36 +++++++++++-- 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/runtime-core/__tests__/componentEmits.spec.ts b/packages/runtime-core/__tests__/componentEmits.spec.ts index 0e5f1e3983d..d8774b078b6 100644 --- a/packages/runtime-core/__tests__/componentEmits.spec.ts +++ b/packages/runtime-core/__tests__/componentEmits.spec.ts @@ -8,7 +8,8 @@ import { h, nodeOps, toHandlers, - nextTick + nextTick, + ComponentPublicInstance } from '@vue/runtime-test' import { isEmitListener } from '../src/componentEmits' @@ -454,4 +455,55 @@ describe('component: emit', () => { await nextTick() expect(fn).not.toHaveBeenCalled() }) + + test('merge string array emits', async () => { + const ComponentA = defineComponent({ + emits: ['one', 'two'] + }) + const ComponentB = defineComponent({ + emits: ['three'] + }) + const renderFn = vi.fn(function (this: ComponentPublicInstance) { + expect(this.$options.emits).toEqual(['one', 'two', 'three']) + return h('div') + }) + const ComponentC = defineComponent({ + render: renderFn, + mixins: [ComponentA, ComponentB] + }) + const el = nodeOps.createElement('div') + expect(renderFn).toHaveBeenCalledTimes(0) + render(h(ComponentC), el) + expect(renderFn).toHaveBeenCalledTimes(1) + }) + + test('merge object emits', async () => { + const twoFn = vi.fn((v: unknown) => !v) + const ComponentA = defineComponent({ + emits: { + one: null, + two: twoFn + } + }) + const ComponentB = defineComponent({ + emits: ['three'] + }) + const renderFn = vi.fn(function (this: ComponentPublicInstance) { + expect(this.$options.emits).toEqual({ + one: null, + two: twoFn, + three: null + }) + expect(this.$options.emits.two).toBe(twoFn) + return h('div') + }) + const ComponentC = defineComponent({ + render: renderFn, + mixins: [ComponentA, ComponentB] + }) + const el = nodeOps.createElement('div') + expect(renderFn).toHaveBeenCalledTimes(0) + render(h(ComponentC), el) + expect(renderFn).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index e0fe434210f..de7426ad325 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -396,10 +396,15 @@ function getContext(): SetupContext { return i.setupContext || (i.setupContext = createSetupContext(i)) } -function normalizePropsOrEmits(props: ComponentPropsOptions | EmitsOptions) { +/** + * @internal + */ +export function normalizePropsOrEmits( + props: ComponentPropsOptions | EmitsOptions +) { return isArray(props) ? props.reduce( - (normalized, p) => ((normalized[p] = {}), normalized), + (normalized, p) => ((normalized[p] = null), normalized), {} as ComponentObjectPropsOptions | ObjectEmitsOptions ) : props diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 481c2adb67e..bba5ade9ad4 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -51,7 +51,8 @@ import { import { ComponentObjectPropsOptions, ExtractPropTypes, - ExtractDefaultPropTypes + ExtractDefaultPropTypes, + ComponentPropsOptions } from './componentProps' import { EmitsOptions, EmitsToProps } from './componentEmits' import { Directive } from './directives' @@ -75,6 +76,7 @@ import { import { OptionMergeFunction } from './apiCreateApp' import { LifecycleHooks } from './enums' import { SlotsType } from './componentSlots' +import { normalizePropsOrEmits } from './apiSetupHelpers' /** * Interface for declaring custom options. @@ -1069,8 +1071,8 @@ export function mergeOptions( export const internalOptionMergeStrats: Record = { data: mergeDataFn, - props: mergeObjectOptions, // TODO - emits: mergeObjectOptions, // TODO + props: mergeEmitsOrPropsOptions, + emits: mergeEmitsOrPropsOptions, // objects methods: mergeObjectOptions, computed: mergeObjectOptions, @@ -1147,7 +1149,33 @@ function mergeAsArray(to: T[] | T | undefined, from: T | T[]) { } function mergeObjectOptions(to: Object | undefined, from: Object | undefined) { - return to ? extend(extend(Object.create(null), to), from) : from + return to ? extend(Object.create(null), to, from) : from +} + +function mergeEmitsOrPropsOptions( + to: EmitsOptions | undefined, + from: EmitsOptions | undefined +): EmitsOptions | undefined +function mergeEmitsOrPropsOptions( + to: ComponentPropsOptions | undefined, + from: ComponentPropsOptions | undefined +): ComponentPropsOptions | undefined +function mergeEmitsOrPropsOptions( + to: ComponentPropsOptions | EmitsOptions | undefined, + from: ComponentPropsOptions | EmitsOptions | undefined +) { + if (to) { + if (isArray(to) && isArray(from)) { + return [...new Set([...to, ...from])] + } + return extend( + Object.create(null), + normalizePropsOrEmits(to), + normalizePropsOrEmits(from ?? {}) + ) + } else { + return from + } } function mergeWatchOptions(