Skip to content

Commit bf473a6

Browse files
committed
feat(runtime-core): type and attr fallthrough support for emits option
1 parent c409d4f commit bf473a6

9 files changed

+351
-97
lines changed

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

+68-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
onUpdated,
99
defineComponent,
1010
openBlock,
11-
createBlock
11+
createBlock,
12+
FunctionalComponent
1213
} from '@vue/runtime-dom'
1314
import { mockWarn } from '@vue/shared'
1415

@@ -428,4 +429,70 @@ describe('attribute fallthrough', () => {
428429
await nextTick()
429430
expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
430431
})
432+
433+
it('should not let listener fallthrough when declared in emits (stateful)', () => {
434+
const Child = defineComponent({
435+
emits: ['click'],
436+
render() {
437+
return h(
438+
'button',
439+
{
440+
onClick: () => {
441+
this.$emit('click', 'custom')
442+
}
443+
},
444+
'hello'
445+
)
446+
}
447+
})
448+
449+
const onClick = jest.fn()
450+
const App = {
451+
render() {
452+
return h(Child, {
453+
onClick
454+
})
455+
}
456+
}
457+
458+
const root = document.createElement('div')
459+
document.body.appendChild(root)
460+
render(h(App), root)
461+
462+
const node = root.children[0] as HTMLElement
463+
node.dispatchEvent(new CustomEvent('click'))
464+
expect(onClick).toHaveBeenCalledTimes(1)
465+
expect(onClick).toHaveBeenCalledWith('custom')
466+
})
467+
468+
it('should not let listener fallthrough when declared in emits (functional)', () => {
469+
const Child: FunctionalComponent<{}, { click: any }> = (_, { emit }) => {
470+
// should not be in props
471+
expect((_ as any).onClick).toBeUndefined()
472+
return h('button', {
473+
onClick: () => {
474+
emit('click', 'custom')
475+
}
476+
})
477+
}
478+
Child.emits = ['click']
479+
480+
const onClick = jest.fn()
481+
const App = {
482+
render() {
483+
return h(Child, {
484+
onClick
485+
})
486+
}
487+
}
488+
489+
const root = document.createElement('div')
490+
document.body.appendChild(root)
491+
render(h(App), root)
492+
493+
const node = root.children[0] as HTMLElement
494+
node.dispatchEvent(new CustomEvent('click'))
495+
expect(onClick).toHaveBeenCalledTimes(1)
496+
expect(onClick).toHaveBeenCalledWith('custom')
497+
})
431498
})

packages/runtime-core/src/apiDefineComponent.ts

+36-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
MethodOptions,
44
ComponentOptionsWithoutProps,
55
ComponentOptionsWithArrayProps,
6-
ComponentOptionsWithObjectProps
6+
ComponentOptionsWithObjectProps,
7+
EmitsOptions
78
} from './apiOptions'
89
import { SetupContext, RenderFunction } from './component'
910
import { ComponentPublicInstance } from './componentProxy'
@@ -39,20 +40,23 @@ export function defineComponent<Props, RawBindings = object>(
3940
// (uses user defined props interface)
4041
// return type is for Vetur and TSX support
4142
export function defineComponent<
42-
Props,
43-
RawBindings,
44-
D,
43+
Props = {},
44+
RawBindings = {},
45+
D = {},
4546
C extends ComputedOptions = {},
46-
M extends MethodOptions = {}
47+
M extends MethodOptions = {},
48+
E extends EmitsOptions = Record<string, any>,
49+
EE extends string = string
4750
>(
48-
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M>
51+
options: ComponentOptionsWithoutProps<Props, RawBindings, D, C, M, E, EE>
4952
): {
5053
new (): ComponentPublicInstance<
5154
Props,
5255
RawBindings,
5356
D,
5457
C,
5558
M,
59+
E,
5660
VNodeProps & Props
5761
>
5862
}
@@ -65,12 +69,22 @@ export function defineComponent<
6569
RawBindings,
6670
D,
6771
C extends ComputedOptions = {},
68-
M extends MethodOptions = {}
72+
M extends MethodOptions = {},
73+
E extends EmitsOptions = Record<string, any>,
74+
EE extends string = string
6975
>(
70-
options: ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M>
76+
options: ComponentOptionsWithArrayProps<
77+
PropNames,
78+
RawBindings,
79+
D,
80+
C,
81+
M,
82+
E,
83+
EE
84+
>
7185
): {
7286
// array props technically doesn't place any contraints on props in TSX
73-
new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M>
87+
new (): ComponentPublicInstance<VNodeProps, RawBindings, D, C, M, E>
7488
}
7589

7690
// overload 4: object format with object props declaration
@@ -82,16 +96,27 @@ export function defineComponent<
8296
RawBindings,
8397
D,
8498
C extends ComputedOptions = {},
85-
M extends MethodOptions = {}
99+
M extends MethodOptions = {},
100+
E extends EmitsOptions = Record<string, any>,
101+
EE extends string = string
86102
>(
87-
options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M>
103+
options: ComponentOptionsWithObjectProps<
104+
PropsOptions,
105+
RawBindings,
106+
D,
107+
C,
108+
M,
109+
E,
110+
EE
111+
>
88112
): {
89113
new (): ComponentPublicInstance<
90114
ExtractPropTypes<PropsOptions>,
91115
RawBindings,
92116
D,
93117
C,
94118
M,
119+
E,
95120
VNodeProps & ExtractPropTypes<PropsOptions, false>
96121
>
97122
}

packages/runtime-core/src/apiOptions.ts

+23-11
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ export interface ComponentOptionsBase<
5050
RawBindings,
5151
D,
5252
C extends ComputedOptions,
53-
M extends MethodOptions
54-
> extends LegacyOptions<Props, RawBindings, D, C, M>, SFCInternalOptions {
53+
M extends MethodOptions,
54+
E extends EmitsOptions,
55+
EE extends string = string
56+
> extends LegacyOptions<Props, D, C, M>, SFCInternalOptions {
5557
setup?: (
5658
this: void,
5759
props: Props,
58-
ctx: SetupContext
60+
ctx: SetupContext<E>
5961
) => RawBindings | RenderFunction | void
6062
name?: string
6163
template?: string | object // can be a direct DOM node
@@ -75,6 +77,7 @@ export interface ComponentOptionsBase<
7577
components?: Record<string, PublicAPIComponent>
7678
directives?: Record<string, Directive>
7779
inheritAttrs?: boolean
80+
emits?: E | EE[]
7881

7982
// Internal ------------------------------------------------------------------
8083

@@ -97,32 +100,40 @@ export type ComponentOptionsWithoutProps<
97100
RawBindings = {},
98101
D = {},
99102
C extends ComputedOptions = {},
100-
M extends MethodOptions = {}
101-
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
103+
M extends MethodOptions = {},
104+
E extends EmitsOptions = Record<string, any>,
105+
EE extends string = string
106+
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
102107
props?: undefined
103-
} & ThisType<ComponentPublicInstance<{}, RawBindings, D, C, M, Readonly<Props>>>
108+
} & ThisType<
109+
ComponentPublicInstance<{}, RawBindings, D, C, M, E, Readonly<Props>>
110+
>
104111

105112
export type ComponentOptionsWithArrayProps<
106113
PropNames extends string = string,
107114
RawBindings = {},
108115
D = {},
109116
C extends ComputedOptions = {},
110117
M extends MethodOptions = {},
118+
E extends EmitsOptions = Record<string, any>,
119+
EE extends string = string,
111120
Props = Readonly<{ [key in PropNames]?: any }>
112-
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
121+
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
113122
props: PropNames[]
114-
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
123+
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
115124

116125
export type ComponentOptionsWithObjectProps<
117126
PropsOptions = ComponentObjectPropsOptions,
118127
RawBindings = {},
119128
D = {},
120129
C extends ComputedOptions = {},
121130
M extends MethodOptions = {},
131+
E extends EmitsOptions = Record<string, any>,
132+
EE extends string = string,
122133
Props = Readonly<ExtractPropTypes<PropsOptions>>
123-
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
134+
> = ComponentOptionsBase<Props, RawBindings, D, C, M, E, EE> & {
124135
props: PropsOptions
125-
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M>>
136+
} & ThisType<ComponentPublicInstance<Props, RawBindings, D, C, M, E>>
126137

127138
export type ComponentOptions =
128139
| ComponentOptionsWithoutProps<any, any, any, any, any>
@@ -138,6 +149,8 @@ export interface MethodOptions {
138149
[key: string]: Function
139150
}
140151

152+
export type EmitsOptions = Record<string, any> | string[]
153+
141154
export type ExtractComputedReturns<T extends any> = {
142155
[key in keyof T]: T[key] extends { get: Function }
143156
? ReturnType<T[key]['get']>
@@ -162,7 +175,6 @@ type ComponentInjectOptions =
162175

163176
export interface LegacyOptions<
164177
Props,
165-
RawBindings,
166178
D,
167179
C extends ComputedOptions,
168180
M extends MethodOptions

packages/runtime-core/src/component.ts

+30-10
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from './errorHandling'
2222
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
2323
import { Directive, validateDirectiveName } from './directives'
24-
import { applyOptions, ComponentOptions } from './apiOptions'
24+
import { applyOptions, ComponentOptions, EmitsOptions } from './apiOptions'
2525
import {
2626
EMPTY_OBJ,
2727
isFunction,
@@ -52,9 +52,13 @@ export interface SFCInternalOptions {
5252
__hmrUpdated?: boolean
5353
}
5454

55-
export interface FunctionalComponent<P = {}> extends SFCInternalOptions {
56-
(props: P, ctx: SetupContext): VNodeChild
55+
export interface FunctionalComponent<
56+
P = {},
57+
E extends EmitsOptions = Record<string, any>
58+
> extends SFCInternalOptions {
59+
(props: P, ctx: SetupContext<E>): any
5760
props?: ComponentPropsOptions<P>
61+
emits?: E | (keyof E)[]
5862
inheritAttrs?: boolean
5963
displayName?: string
6064
}
@@ -92,12 +96,29 @@ export const enum LifecycleHooks {
9296
ERROR_CAPTURED = 'ec'
9397
}
9498

95-
export type Emit = (event: string, ...args: unknown[]) => any[]
96-
97-
export interface SetupContext {
99+
type UnionToIntersection<U> = (U extends any
100+
? (k: U) => void
101+
: never) extends ((k: infer I) => void)
102+
? I
103+
: never
104+
105+
export type Emit<
106+
Options = Record<string, any>,
107+
Event extends keyof Options = keyof Options
108+
> = Options extends any[]
109+
? (event: Options[0], ...args: any[]) => unknown[]
110+
: UnionToIntersection<
111+
{
112+
[key in Event]: Options[key] extends ((...args: infer Args) => any)
113+
? (event: key, ...args: Args) => unknown[]
114+
: (event: key, ...args: any[]) => unknown[]
115+
}[Event]
116+
>
117+
118+
export interface SetupContext<E = Record<string, any>> {
98119
attrs: Data
99120
slots: Slots
100-
emit: Emit
121+
emit: Emit<E>
101122
}
102123

103124
export type RenderFunction = {
@@ -248,7 +269,7 @@ export function createComponentInstance(
248269
rtc: null,
249270
ec: null,
250271

251-
emit: (event, ...args): any[] => {
272+
emit: (event: string, ...args: any[]): any[] => {
252273
const props = instance.vnode.props || EMPTY_OBJ
253274
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
254275
if (!handler && event.indexOf('update:') === 0) {
@@ -303,9 +324,8 @@ export function setupComponent(
303324
isSSR = false
304325
) {
305326
isInSSRComponentSetup = isSSR
306-
const propsOptions = instance.type.props
307327
const { props, children, shapeFlag } = instance.vnode
308-
resolveProps(instance, props, propsOptions)
328+
resolveProps(instance, props)
309329
resolveSlots(instance, children)
310330

311331
// setup stateful logic

0 commit comments

Comments
 (0)