Skip to content

Commit ba3b3cd

Browse files
committed
fix(runtime-core/emits): merge emits options from mixins/extends
fix #1562
1 parent c2d3da9 commit ba3b3cd

File tree

4 files changed

+85
-30
lines changed

4 files changed

+85
-30
lines changed

packages/runtime-core/__tests__/componentEmits.spec.ts

+41-6
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,47 @@ describe('component: emit', () => {
143143
expect(`event validation failed for event "foo"`).toHaveBeenWarned()
144144
})
145145

146+
test('merging from mixins', () => {
147+
const mixin = {
148+
emits: {
149+
foo: (arg: number) => arg > 0
150+
}
151+
}
152+
const Foo = defineComponent({
153+
mixins: [mixin],
154+
render() {},
155+
created() {
156+
this.$emit('foo', -1)
157+
}
158+
})
159+
render(h(Foo), nodeOps.createElement('div'))
160+
expect(`event validation failed for event "foo"`).toHaveBeenWarned()
161+
})
162+
146163
test('isEmitListener', () => {
147-
expect(isEmitListener(['click'], 'onClick')).toBe(true)
148-
expect(isEmitListener(['click'], 'onclick')).toBe(false)
149-
expect(isEmitListener({ click: null }, 'onClick')).toBe(true)
150-
expect(isEmitListener({ click: null }, 'onclick')).toBe(false)
151-
expect(isEmitListener(['click'], 'onBlick')).toBe(false)
152-
expect(isEmitListener({ click: null }, 'onBlick')).toBe(false)
164+
const def1 = { emits: ['click'] }
165+
expect(isEmitListener(def1, 'onClick')).toBe(true)
166+
expect(isEmitListener(def1, 'onclick')).toBe(false)
167+
expect(isEmitListener(def1, 'onBlick')).toBe(false)
168+
169+
const def2 = { emits: { click: null } }
170+
expect(isEmitListener(def2, 'onClick')).toBe(true)
171+
expect(isEmitListener(def2, 'onclick')).toBe(false)
172+
expect(isEmitListener(def2, 'onBlick')).toBe(false)
173+
174+
const mixin1 = { emits: ['foo'] }
175+
const mixin2 = { emits: ['bar'] }
176+
const extend = { emits: ['baz'] }
177+
const def3 = {
178+
emits: { click: null },
179+
mixins: [mixin1, mixin2],
180+
extends: extend
181+
}
182+
expect(isEmitListener(def3, 'onClick')).toBe(true)
183+
expect(isEmitListener(def3, 'onFoo')).toBe(true)
184+
expect(isEmitListener(def3, 'onBar')).toBe(true)
185+
expect(isEmitListener(def3, 'onBaz')).toBe(true)
186+
expect(isEmitListener(def3, 'onclick')).toBe(false)
187+
expect(isEmitListener(def3, 'onBlick')).toBe(false)
153188
})
154189
})

packages/runtime-core/src/component.ts

+4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export interface ComponentInternalOptions {
5959
* @internal
6060
*/
6161
__props?: NormalizedPropsOptions | []
62+
/**
63+
* @internal
64+
*/
65+
__emits?: ObjectEmitsOptions
6266
/**
6367
* @internal
6468
*/

packages/runtime-core/src/componentEmits.ts

+39-21
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
capitalize,
77
hyphenate,
88
isFunction,
9-
def
9+
extend
1010
} from '@vue/shared'
11-
import { ComponentInternalInstance } from './component'
11+
import { ComponentInternalInstance, Component } from './component'
1212
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
1313
import { warn } from './warning'
1414
import { normalizePropsOptions } from './componentProps'
@@ -43,7 +43,7 @@ export function emit(
4343
const props = instance.vnode.props || EMPTY_OBJ
4444

4545
if (__DEV__) {
46-
const options = normalizeEmitsOptions(instance.type.emits)
46+
const options = normalizeEmitsOptions(instance.type)
4747
if (options) {
4848
if (!(event in options)) {
4949
const propsOptions = normalizePropsOptions(instance.type)[0]
@@ -84,34 +84,52 @@ export function emit(
8484
}
8585
}
8686

87-
export function normalizeEmitsOptions(
88-
options: EmitsOptions | undefined
87+
function normalizeEmitsOptions(
88+
comp: Component
8989
): ObjectEmitsOptions | undefined {
90-
if (!options) {
91-
return
92-
} else if (isArray(options)) {
93-
if ((options as any)._n) {
94-
return (options as any)._n
90+
if (hasOwn(comp, '__emits')) {
91+
return comp.__emits
92+
}
93+
94+
const raw = comp.emits
95+
let normalized: ObjectEmitsOptions = {}
96+
97+
// apply mixin/extends props
98+
let hasExtends = false
99+
if (__FEATURE_OPTIONS__ && !isFunction(comp)) {
100+
if (comp.extends) {
101+
hasExtends = true
102+
extend(normalized, normalizeEmitsOptions(comp.extends))
95103
}
96-
const normalized: ObjectEmitsOptions = {}
97-
options.forEach(key => (normalized[key] = null))
98-
def(options, '_n', normalized)
99-
return normalized
104+
if (comp.mixins) {
105+
hasExtends = true
106+
comp.mixins.forEach(m => extend(normalized, normalizeEmitsOptions(m)))
107+
}
108+
}
109+
110+
if (!raw && !hasExtends) {
111+
return (comp.__emits = undefined)
112+
}
113+
114+
if (isArray(raw)) {
115+
raw.forEach(key => (normalized[key] = null))
100116
} else {
101-
return options
117+
extend(normalized, raw)
102118
}
119+
return (comp.__emits = normalized)
103120
}
104121

105122
// Check if an incoming prop key is a declared emit event listener.
106123
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
107124
// both considered matched listeners.
108-
export function isEmitListener(emits: EmitsOptions, key: string): boolean {
125+
export function isEmitListener(comp: Component, key: string): boolean {
126+
if (!isOn(key)) {
127+
return false
128+
}
129+
const emits = normalizeEmitsOptions(comp)
109130
return (
110-
isOn(key) &&
111-
(hasOwn(
112-
(emits = normalizeEmitsOptions(emits) as ObjectEmitsOptions),
113-
key[2].toLowerCase() + key.slice(3)
114-
) ||
131+
!!emits &&
132+
(hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
115133
hasOwn(emits, key.slice(2)))
116134
)
117135
}

packages/runtime-core/src/componentProps.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,6 @@ function setFullProps(
242242
attrs: Data
243243
) {
244244
const [options, needCastKeys] = normalizePropsOptions(instance.type)
245-
const emits = instance.type.emits
246-
247245
if (rawProps) {
248246
for (const key in rawProps) {
249247
const value = rawProps[key]
@@ -256,7 +254,7 @@ function setFullProps(
256254
let camelKey
257255
if (options && hasOwn(options, (camelKey = camelize(key)))) {
258256
props[camelKey] = value
259-
} else if (!emits || !isEmitListener(emits, key)) {
257+
} else if (!isEmitListener(instance.type, key)) {
260258
// Any non-declared (either as a prop or an emitted event) props are put
261259
// into a separate `attrs` object for spreading. Make sure to preserve
262260
// original key casing

0 commit comments

Comments
 (0)