Skip to content

Commit

Permalink
fix(runtime-core): fix props/emits resolving with global mixins
Browse files Browse the repository at this point in the history
fix #1975
  • Loading branch information
yyx990803 committed Aug 31, 2020
1 parent 2bbeea9 commit 8ed0b34
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 101 deletions.
43 changes: 8 additions & 35 deletions packages/runtime-core/__tests__/componentEmits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,40 +178,13 @@ describe('component: emit', () => {
expect(fn).toHaveBeenCalledTimes(1)
})

describe('isEmitListener', () => {
test('array option', () => {
const def1 = { emits: ['click'] }
expect(isEmitListener(def1, 'onClick')).toBe(true)
expect(isEmitListener(def1, 'onclick')).toBe(false)
expect(isEmitListener(def1, 'onBlick')).toBe(false)
})

test('object option', () => {
const def2 = { emits: { click: null } }
expect(isEmitListener(def2, 'onClick')).toBe(true)
expect(isEmitListener(def2, 'onclick')).toBe(false)
expect(isEmitListener(def2, 'onBlick')).toBe(false)
})

test('with mixins and extends', () => {
const mixin1 = { emits: ['foo'] }
const mixin2 = { emits: ['bar'] }
const extend = { emits: ['baz'] }
const def3 = {
mixins: [mixin1, mixin2],
extends: extend
}
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)
})

test('.once listeners', () => {
const def2 = { emits: { click: null } }
expect(isEmitListener(def2, 'onClickOnce')).toBe(true)
expect(isEmitListener(def2, 'onclickOnce')).toBe(false)
})
test('isEmitListener', () => {
const options = { click: null }
expect(isEmitListener(options, 'onClick')).toBe(true)
expect(isEmitListener(options, 'onclick')).toBe(false)
expect(isEmitListener(options, 'onBlick')).toBe(false)
// .once listeners
expect(isEmitListener(options, 'onClickOnce')).toBe(true)
expect(isEmitListener(options, 'onclickOnce')).toBe(false)
})
})
43 changes: 42 additions & 1 deletion packages/runtime-core/__tests__/componentProps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
FunctionalComponent,
defineComponent,
ref,
serializeInner
serializeInner,
createApp
} from '@vue/runtime-test'
import { render as domRender, nextTick } from 'vue'

Expand Down Expand Up @@ -309,4 +310,44 @@ describe('component props', () => {
expect(setupProps).toMatchObject(props)
expect(renderProxy.$props).toMatchObject(props)
})

test('merging props from global mixins', () => {
let setupProps: any
let renderProxy: any

const M1 = {
props: ['m1']
}
const M2 = {
props: { m2: null }
}
const Comp = {
props: ['self'],
setup(props: any) {
setupProps = props
},
render(this: any) {
renderProxy = this
return h('div', [this.self, this.m1, this.m2])
}
}

const props = {
self: 'from self, ',
m1: 'from mixin 1, ',
m2: 'from mixin 2'
}
const app = createApp(Comp, props)
app.mixin(M1)
app.mixin(M2)

const root = nodeOps.createElement('div')
app.mount(root)

expect(serializeInner(root)).toMatch(
`from self, from mixin 1, from mixin 2`
)
expect(setupProps).toMatchObject(props)
expect(renderProxy.$props).toMatchObject(props)
})
})
4 changes: 4 additions & 0 deletions packages/runtime-core/src/apiCreateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface App<HostElement = any> {
provide<T>(key: InjectionKey<T> | string, value: T): this

// internal, but we need to expose these for the server-renderer and devtools
_uid: number
_component: ConcreteComponent
_props: Data | null
_container: HostElement | null
Expand Down Expand Up @@ -108,6 +109,8 @@ export type CreateAppFunction<HostElement> = (
rootProps?: Data | null
) => App<HostElement>

let uid = 0

export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
Expand All @@ -124,6 +127,7 @@ export function createAppAPI<HostElement>(
let isMounted = false

const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
Expand Down
39 changes: 30 additions & 9 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
import {
ComponentPropsOptions,
NormalizedPropsOptions,
initProps
initProps,
normalizePropsOptions
} from './componentProps'
import { Slots, initSlots, InternalSlots } from './componentSlots'
import { warn } from './warning'
Expand All @@ -30,7 +31,8 @@ import {
EmitsOptions,
ObjectEmitsOptions,
EmitFn,
emit
emit,
normalizeEmitsOptions
} from './componentEmits'
import {
EMPTY_OBJ,
Expand Down Expand Up @@ -72,11 +74,11 @@ export interface ComponentInternalOptions {
/**
* @internal
*/
__props?: NormalizedPropsOptions | []
__props?: Record<number, NormalizedPropsOptions>
/**
* @internal
*/
__emits?: ObjectEmitsOptions
__emits?: Record<number, ObjectEmitsOptions | null>
/**
* @internal
*/
Expand Down Expand Up @@ -231,6 +233,16 @@ export interface ComponentInternalInstance {
* @internal
*/
directives: Record<string, Directive> | null
/**
* reoslved props options
* @internal
*/
propsOptions: NormalizedPropsOptions
/**
* resolved emits options
* @internal
*/
emitsOptions: ObjectEmitsOptions | null

// the rest are only for stateful components ---------------------------------

Expand All @@ -254,14 +266,17 @@ export interface ComponentInternalInstance {
*/
ctx: Data

// internal state
// state
data: Data
props: Data
attrs: Data
slots: InternalSlots
refs: Data
emit: EmitFn
// used for keeping track of .once event handlers on components
/**
* used for keeping track of .once event handlers on components
* @internal
*/
emitted: Record<string, boolean> | null

/**
Expand Down Expand Up @@ -387,6 +402,14 @@ export function createComponentInstance(
components: null,
directives: null,

// resolved props and emits options
propsOptions: normalizePropsOptions(type, appContext),
emitsOptions: normalizeEmitsOptions(type, appContext),

// emit
emit: null as any, // to be set immediately
emitted: null,

// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
Expand Down Expand Up @@ -419,9 +442,7 @@ export function createComponentInstance(
a: null,
rtg: null,
rtc: null,
ec: null,
emit: null as any, // to be set immediately
emitted: null
ec: null
}
if (__DEV__) {
instance.ctx = createRenderContext(instance)
Expand Down
64 changes: 41 additions & 23 deletions packages/runtime-core/src/componentEmits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import {
isFunction,
extend
} from '@vue/shared'
import { ComponentInternalInstance, ConcreteComponent } from './component'
import {
ComponentInternalInstance,
ComponentOptions,
ConcreteComponent
} from './component'
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
import { warn } from './warning'
import { normalizePropsOptions } from './componentProps'
import { UnionToIntersection } from './helpers/typeUtils'
import { devtoolsComponentEmit } from './devtools'
import { AppContext } from './apiCreateApp'

export type ObjectEmitsOptions = Record<
string,
Expand Down Expand Up @@ -44,18 +48,20 @@ export function emit(
const props = instance.vnode.props || EMPTY_OBJ

if (__DEV__) {
const options = normalizeEmitsOptions(instance.type)
if (options) {
if (!(event in options)) {
const propsOptions = normalizePropsOptions(instance.type)[0]
const {
emitsOptions,
propsOptions: [propsOptions]
} = instance
if (emitsOptions) {
if (!(event in emitsOptions)) {
if (!propsOptions || !(`on` + capitalize(event) in propsOptions)) {
warn(
`Component emitted event "${event}" but it is neither declared in ` +
`the emits option nor as an "on${capitalize(event)}" prop.`
)
}
} else {
const validator = options[event]
const validator = emitsOptions[event]
if (isFunction(validator)) {
const isValid = validator(...args)
if (!isValid) {
Expand Down Expand Up @@ -98,11 +104,16 @@ export function emit(
}
}

function normalizeEmitsOptions(
comp: ConcreteComponent
): ObjectEmitsOptions | undefined {
if (hasOwn(comp, '__emits')) {
return comp.__emits
export function normalizeEmitsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false
): ObjectEmitsOptions | null {
const appId = appContext.app ? appContext.app._uid : -1
const cache = comp.__emits || (comp.__emits = {})
const cached = cache[appId]
if (cached !== undefined) {
return cached
}

const raw = comp.emits
Expand All @@ -111,39 +122,46 @@ function normalizeEmitsOptions(
// apply mixin/extends props
let hasExtends = false
if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
if (comp.extends) {
const extendEmits = (raw: ComponentOptions) => {
hasExtends = true
extend(normalized, normalizeEmitsOptions(comp.extends))
extend(normalized, normalizeEmitsOptions(raw, appContext, true))
}
if (!asMixin && appContext.mixins.length) {
appContext.mixins.forEach(extendEmits)
}
if (comp.extends) {
extendEmits(comp.extends)
}
if (comp.mixins) {
hasExtends = true
comp.mixins.forEach(m => extend(normalized, normalizeEmitsOptions(m)))
comp.mixins.forEach(extendEmits)
}
}

if (!raw && !hasExtends) {
return (comp.__emits = undefined)
return (cache[appId] = null)
}

if (isArray(raw)) {
raw.forEach(key => (normalized[key] = null))
} else {
extend(normalized, raw)
}
return (comp.__emits = normalized)
return (cache[appId] = 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(comp: ConcreteComponent, key: string): boolean {
let emits: ObjectEmitsOptions | undefined
if (!isOn(key) || !(emits = normalizeEmitsOptions(comp))) {
export function isEmitListener(
options: ObjectEmitsOptions | null,
key: string
): boolean {
if (!options || !isOn(key)) {
return false
}
key = key.replace(/Once$/, '')
return (
hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
hasOwn(emits, key.slice(2))
hasOwn(options, key[2].toLowerCase() + key.slice(3)) ||
hasOwn(options, key.slice(2))
)
}
8 changes: 2 additions & 6 deletions packages/runtime-core/src/componentOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,7 @@ import {
WritableComputedOptions,
toRaw
} from '@vue/reactivity'
import {
ComponentObjectPropsOptions,
ExtractPropTypes,
normalizePropsOptions
} from './componentProps'
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
import { EmitsOptions } from './componentEmits'
import { Directive } from './directives'
import {
Expand Down Expand Up @@ -431,7 +427,7 @@ export function applyOptions(
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null

if (__DEV__) {
const propsOptions = normalizePropsOptions(options)[0]
const [propsOptions] = instance.propsOptions
if (propsOptions) {
for (const key in propsOptions) {
checkDuplicateProperties!(OptionTypes.PROPS, key)
Expand Down
Loading

0 comments on commit 8ed0b34

Please sign in to comment.