Skip to content

Commit c459f47

Browse files
committed
feat: support nested event methods
1 parent 3c928d2 commit c459f47

File tree

3 files changed

+104
-4
lines changed

3 files changed

+104
-4
lines changed

.nvmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v16

src/event-mixin.spec.ts

+64
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,67 @@ describe('a child class', () => {
7676
expect(eventThreeArgs).toHaveLength(1);
7777
});
7878
});
79+
80+
// New test cases for the path functionality
81+
describe('DeepNestedEvents with path', () => {
82+
class DeepNestedEvents {
83+
eventContainer = {
84+
deeper: {
85+
eventOne: new TypedEvent<(value: number) => void>(),
86+
eventTwo: new TypedEvent<(value: boolean) => void>(),
87+
},
88+
};
89+
90+
fireEventOne() {
91+
this.eventContainer.deeper.eventOne.emit(42);
92+
}
93+
94+
fireEventTwo() {
95+
this.eventContainer.deeper.eventTwo.emit(true);
96+
}
97+
}
98+
99+
const NestedEventsClass = AddEvents<
100+
typeof DeepNestedEvents,
101+
{
102+
eventOne: TypedEvent<(value: number) => void>;
103+
eventTwo: TypedEvent<(value: boolean) => void>;
104+
}
105+
>(DeepNestedEvents, 'eventContainer.deeper');
106+
107+
const myClass = new NestedEventsClass();
108+
it('should notify handlers when nested events are fired', () => {
109+
expect.hasAssertions();
110+
const handlerOneArgs: number[] = [];
111+
const handlerTwoArgs: boolean[] = [];
112+
113+
myClass.on('eventOne', (value: number) => handlerOneArgs.push(value));
114+
myClass.on('eventTwo', (value: boolean) => handlerTwoArgs.push(value));
115+
116+
myClass.fireEventOne();
117+
myClass.fireEventTwo();
118+
119+
expect(handlerOneArgs).toStrictEqual([42]);
120+
121+
expect(handlerTwoArgs).toStrictEqual([true]);
122+
});
123+
124+
it('should throw an error when trying to subscribe to an event with an invalid path', () => {
125+
expect.hasAssertions();
126+
127+
const InvalidPathClass = AddEvents<
128+
typeof DeepNestedEvents,
129+
{
130+
eventOne: TypedEvent<(value: number) => void>;
131+
}
132+
>(DeepNestedEvents, 'eventContainer.unknown');
133+
134+
const instance = new InvalidPathClass();
135+
136+
// Trying to bind an event handler to an invalid path
137+
expect(() => {
138+
// eslint-disable-next-line @typescript-eslint/no-empty-function
139+
instance.on('eventOne', () => {});
140+
}).toThrow(new Error('Event "eventContainer.unknown.eventOne" is not defined'));
141+
});
142+
});

src/event-mixin.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,45 @@ type eventHandlerType<Type> = Type extends TypedEvent<infer X> ? X : never;
2222
* - The values are of typed 'TypedEvent'.
2323
*
2424
* @param Base - The class which will be extended with event subscription methods.
25+
* @param path - The path to the TypedEvent member in the Base class. This is optional and only needed
26+
* if the events are not directly on the Base class.
2527
* @returns A subclass of Base with event subscription methods added.
2628
*/
2729
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
28-
export function AddEvents<TBase extends Constructor, U>(Base: TBase) {
30+
export function AddEvents<TBase extends Constructor, U>(Base: TBase, path?: string) {
31+
/**
32+
* Get the event object from the given instance. If a path is given, the object will be traversed
33+
* using the path.
34+
*
35+
* @param instance - The instance to get the event object from.
36+
* @returns The event object.
37+
*/
38+
function getEventObject(instance: any) {
39+
if (!path) {
40+
return instance;
41+
}
42+
return path.split('.').reduce((obj, key) => (obj ? obj[key] : undefined), instance);
43+
}
44+
45+
/**
46+
* Get the event object from the given instance. If a path is given, the object will be traversed
47+
* using the path. If the event object is not found, an error will be thrown.
48+
*
49+
* @param instance - The instance to get the event object from.
50+
* @param eventName - The name of the event to get.
51+
* @returns The event.
52+
*/
53+
function getEvent<K extends keyof U>(instance: any, eventName: K) {
54+
const eventsObject = getEventObject(instance);
55+
const event = eventsObject?.[eventName];
56+
if (!event) {
57+
// Construct the full path for the error message
58+
const fullPath = path ? `${path}.${String(eventName)}` : String(eventName);
59+
throw new Error(`Event "${fullPath}" is not defined`);
60+
}
61+
return event;
62+
}
63+
2964
// eslint-disable-next-line jsdoc/require-jsdoc
3065
return class WithEvents extends Base {
3166
/**
@@ -37,7 +72,7 @@ export function AddEvents<TBase extends Constructor, U>(Base: TBase) {
3772
on<K extends keyof U, E extends eventHandlerType<U[K]>>(eventName: K, handler: E) {
3873
// Even though we bypass type safety in the call (casting this as any), we've enforced it in the
3974
// method signature above, so it's still safe.
40-
(this as any)[eventName].on(handler);
75+
getEvent(this, eventName).on(handler);
4176
}
4277

4378
/**
@@ -49,7 +84,7 @@ export function AddEvents<TBase extends Constructor, U>(Base: TBase) {
4984
* @param handler - The handler.
5085
*/
5186
once<K extends keyof U, E extends eventHandlerType<U[K]>>(eventName: K, handler: E) {
52-
(this as any)[eventName].once(handler);
87+
getEvent(this, eventName).once(handler);
5388
}
5489

5590
/**
@@ -61,7 +96,7 @@ export function AddEvents<TBase extends Constructor, U>(Base: TBase) {
6196
* @param handler - The handler.
6297
*/
6398
off<K extends keyof U, E extends eventHandlerType<U[K]>>(eventName: K, handler: E) {
64-
(this as any)[eventName].off(handler);
99+
getEvent(this, eventName).off(handler);
65100
}
66101
};
67102
}

0 commit comments

Comments
 (0)