Skip to content

Commit

Permalink
fix(runtime-core/emits): merge emits options from mixins/extends
Browse files Browse the repository at this point in the history
fix #1562
  • Loading branch information
yyx990803 committed Jul 13, 2020
1 parent c2d3da9 commit ba3b3cd
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 30 deletions.
47 changes: 41 additions & 6 deletions packages/runtime-core/__tests__/componentEmits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
4 changes: 4 additions & 0 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export interface ComponentInternalOptions {
* @internal
*/
__props?: NormalizedPropsOptions | []
/**
* @internal
*/
__emits?: ObjectEmitsOptions
/**
* @internal
*/
Expand Down
60 changes: 39 additions & 21 deletions packages/runtime-core/src/componentEmits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)))
)
}
4 changes: 1 addition & 3 deletions packages/runtime-core/src/componentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down

0 comments on commit ba3b3cd

Please sign in to comment.