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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy