Skip to content

Commit 4846b41

Browse files
committed
feat: add emits mixin helper
1 parent 79b1bf3 commit 4846b41

File tree

5 files changed

+119
-35
lines changed

5 files changed

+119
-35
lines changed

src/helpers.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { ComponentOptions, SetupContext, UnwrapRef, ComponentObjectPropsOptions, ExtractPropTypes } from 'vue'
2-
import { Vue, VueBase, VueMixin } from './vue'
1+
import { ComponentOptions, UnwrapRef, ComponentObjectPropsOptions, ExtractPropTypes } from 'vue'
2+
import { EmitsOptions, ObjectEmitsOptions, Vue, VueConstructor, VueMixin } from './vue'
33

44
export function Options<V extends Vue>(
55
options: ComponentOptions & ThisType<V>
6-
): <VC extends VueBase>(target: VC) => VC {
6+
): <VC extends VueConstructor>(target: VC) => VC {
77
return (Component) => {
88
Component.__vccBase = options
99
return Component
@@ -12,7 +12,7 @@ export function Options<V extends Vue>(
1212

1313
export interface VueDecorator {
1414
// Class decorator
15-
(Ctor: VueBase): void
15+
(Ctor: VueConstructor): void
1616

1717
// Property decorator
1818
(target: Vue, key: string): void
@@ -24,9 +24,9 @@ export interface VueDecorator {
2424
export function createDecorator(
2525
factory: (options: ComponentOptions, key: string, index: number) => void
2626
): VueDecorator {
27-
return (target: Vue | VueBase, key?: any, index?: any) => {
27+
return (target: Vue | VueConstructor, key?: any, index?: any) => {
2828
const Ctor =
29-
typeof target === 'function' ? target : (target.constructor as VueBase)
29+
typeof target === 'function' ? target : (target.constructor as VueConstructor)
3030
if (!Ctor.__vccDecorators) {
3131
Ctor.__vccDecorators = []
3232
}
@@ -52,40 +52,51 @@ export type UnionToIntersection<U> = (
5252
export type ExtractInstance<T> = T extends VueMixin<infer V> ? V : never
5353

5454
export type MixedVueBase<Mixins extends VueMixin[]> = Mixins extends (infer T)[]
55-
? VueBase<UnionToIntersection<ExtractInstance<T>> & Vue> & PropsMixin
55+
? VueConstructor<UnionToIntersection<ExtractInstance<T>> & Vue> & PropsMixin
5656
: never
5757

5858
export function mixins<T extends VueMixin[]>(...Ctors: T): MixedVueBase<T>
59-
export function mixins(...Ctors: VueMixin[]): VueBase {
60-
return class MixedVue<Props> extends Vue<Props> {
59+
export function mixins(...Ctors: VueMixin[]): VueConstructor {
60+
return class MixedVue extends Vue {
6161
static __vccExtend(options: ComponentOptions) {
6262
Ctors.forEach((Ctor) => Ctor.__vccExtend(options))
6363
}
6464

65-
constructor(props: Props, ctx: SetupContext) {
66-
super(props, ctx)
65+
constructor(...args: any[]) {
66+
super(...args)
6767

6868
Ctors.forEach((Ctor) => {
69-
const data = new (Ctor as any)(props, ctx)
69+
const data = new (Ctor as VueConstructor)(...args)
7070
Object.keys(data).forEach((key) => {
71-
;(this as any)[key] = data[key]
71+
;(this as any)[key] = (data as any)[key]
7272
})
7373
})
7474
}
7575
}
7676
}
7777

78-
export function props<PropNames extends string, Props = Readonly<{ [key in PropNames]?: any }>>(propNames: PropNames[]): VueBase<Vue<Props> & Props>
79-
export function props<PropsOptions extends ComponentObjectPropsOptions, Props = Readonly<ExtractPropTypes<PropsOptions>>>(propsOptions: PropsOptions): VueBase<Vue<Props> & Props>
80-
export function props(propsOptions: string[] | ComponentObjectPropsOptions): VueBase {
81-
class PropsMixin<Props> extends Vue<Props> {
78+
export function props<PropNames extends string, Props = Readonly<{ [key in PropNames]?: any }>>(propNames: PropNames[]): VueConstructor<Vue<Props> & Props>
79+
export function props<PropsOptions extends ComponentObjectPropsOptions, Props = Readonly<ExtractPropTypes<PropsOptions>>>(propsOptions: PropsOptions): VueConstructor<Vue<Props> & Props>
80+
export function props(propsOptions: string[] | ComponentObjectPropsOptions): VueConstructor {
81+
class PropsMixin extends Vue {
8282
static __vccExtend(options: ComponentOptions) {
8383
options.props = propsOptions
8484
}
8585
}
8686
return PropsMixin
8787
}
8888

89+
export function emits<EmitNames extends string>(emitNames: EmitNames[]): VueConstructor<Vue<unknown, EmitNames[]>>
90+
export function emits<EmitsOptions extends ObjectEmitsOptions>(emitsOptions: EmitsOptions): VueConstructor<Vue<unknown, EmitsOptions>>
91+
export function emits(emitsOptions: EmitsOptions): VueConstructor {
92+
class EmitsMixin extends Vue {
93+
static __vccExtend(options: ComponentOptions) {
94+
options.emits = emitsOptions
95+
}
96+
}
97+
return EmitsMixin
98+
}
99+
89100
export function setup<R>(setupFn: () => R): UnwrapRef<R> {
90101
// Hack to delay the invocation of setup function.
91102
// Will be called after dealing with class properties.

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
export { Vue, ClassComponentHooks } from './vue'
66

7-
export { Options, createDecorator, mixins, props, setup } from './helpers'
7+
export { Options, createDecorator, mixins, props, emits, setup } from './helpers'
88

99
/**
1010
* Other types
1111
*/
1212

13-
export { VueBase, VueMixin, VueStatic, VueConstructor } from './vue'
13+
export { VueBase, VueMixin, VueStatic, VueConstructor, EmitsOptions, ObjectEmitsOptions } from './vue'
1414

1515
export {
1616
VueDecorator,

src/vue.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,9 @@ export interface VueStatic {
8484
registerHooks(keys: string[]): void
8585
}
8686

87-
export type VueMixin<V extends Vue = Vue> = VueStatic & { prototype: V }
87+
export type VueBase = Vue<unknown, never[]>
8888

89-
export type VueBase<V extends Vue = Vue> = VueMixin<V> &
90-
(new (...args: any[]) => V)
89+
export type VueMixin<V extends VueBase = Vue> = VueStatic & { prototype: V }
9190

9291
export interface ClassComponentHooks {
9392
// To be extended on user land
@@ -108,19 +107,22 @@ export interface ClassComponentHooks {
108107
serverPrefetch?(): Promise<unknown>
109108
}
110109

111-
export type Vue<Props = unknown> = ComponentPublicInstance<
110+
export type ObjectEmitsOptions = Record<string, ((...args: any[]) => any) | null>
111+
export type EmitsOptions = ObjectEmitsOptions | string[]
112+
113+
export type Vue<Props = unknown, Emits extends EmitsOptions = {}> = ComponentPublicInstance<
112114
{},
113115
{},
114116
{},
115117
{},
116118
{},
117-
Record<string, any>,
119+
Emits,
118120
Props
119121
> &
120122
ClassComponentHooks
121123

122-
export interface VueConstructor extends VueStatic {
123-
new <Props = unknown>(prop: Props, ctx: SetupContext): Vue<Props>
124+
export interface VueConstructor<V extends VueBase = Vue> extends VueStatic {
125+
new (...args: any[]): V
124126
}
125127

126128
class VueImpl {

test/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { createApp, App } from 'vue'
2-
import { VueBase } from '../src/vue'
2+
import { VueConstructor } from '../src/vue'
33

44
const wrapper = document.createElement('div')
55

6-
export function mount<T extends VueBase>(
6+
export function mount<T extends VueConstructor>(
77
Component: T,
88
props?: Record<string, any>
99
) {

test/specs/test.spec.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'reflect-metadata'
22
import { h, resolveComponent, ref, onMounted, Ref, watch, toRef } from 'vue'
3-
import { Options, createDecorator, mixins, Vue, setup, props } from '../../src'
3+
import { Options, createDecorator, mixins, Vue, setup, props, emits } from '../../src'
44
import { mount, unmount } from '../helpers'
55

66
describe('vue-class-component', () => {
@@ -59,15 +59,11 @@ describe('vue-class-component', () => {
5959
}
6060

6161
it('data: $props should be available', () => {
62-
interface Props {
63-
foo: number
64-
}
65-
6662
@Options({
6763
props: ['foo'],
6864
})
69-
class MyComp extends Vue<Props> {
70-
message = 'answer is ' + this.$props.foo
65+
class MyComp extends Vue {
66+
message = 'answer is ' + (this.$props as any).foo
7167
}
7268

7369
const { root } = mount(MyComp, { foo: 42 })
@@ -391,6 +387,81 @@ describe('vue-class-component', () => {
391387
expect(root.baz).toBe('The answer is: 42')
392388
})
393389

390+
it('emits mixin: emit names', () => {
391+
const Emits = emits(['foo', 'bar'])
392+
393+
class Child extends Emits {
394+
mounted() {
395+
this.$emit('foo', 42)
396+
this.$emit('bar', 'Hello', 'World')
397+
}
398+
399+
render() {
400+
return h('span')
401+
}
402+
}
403+
404+
class App extends Vue {
405+
fooResult: number | null = null
406+
barResult: string | null = null
407+
408+
render() {
409+
return h(Child, {
410+
onFoo: (result: number) => {
411+
this.fooResult = result
412+
},
413+
414+
onBar: (res1: string, res2: string) => {
415+
this.barResult = res1 + res2
416+
}
417+
})
418+
}
419+
}
420+
421+
const { root } = mount(App)
422+
expect(root.fooResult).toBe(42)
423+
expect(root.barResult).toBe('HelloWorld')
424+
})
425+
426+
it('emits mixin: emits options object', () => {
427+
const Emits = emits({
428+
foo: (_n: number) => true,
429+
bar: (_n1: string, _n2: string) => true
430+
})
431+
432+
class Child extends Emits {
433+
mounted() {
434+
this.$emit('foo', 42)
435+
this.$emit('bar', 'Hello', 'World')
436+
}
437+
438+
render() {
439+
return h('span')
440+
}
441+
}
442+
443+
class App extends Vue {
444+
fooResult: number | null = null
445+
barResult: string | null = null
446+
447+
render() {
448+
return h(Child, {
449+
onFoo: (result: number) => {
450+
this.fooResult = result
451+
},
452+
453+
onBar: (res1: string, res2: string) => {
454+
this.barResult = res1 + res2
455+
}
456+
})
457+
}
458+
}
459+
460+
const { root } = mount(App)
461+
expect(root.fooResult).toBe(42)
462+
expect(root.barResult).toBe('HelloWorld')
463+
})
464+
394465
it('uses composition functions', () => {
395466
function useCounter() {
396467
const count = ref(0)

0 commit comments

Comments
 (0)