Skip to content

Commit

Permalink
feat(PubSub): allow multiple subcribe events for a single callback
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Sep 26, 2024
1 parent 247c375 commit 6d39c2c
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 19 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/core/slickCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type Handler<ArgType = any> = (e: SlickEventData<ArgType>, args: ArgType)

export interface BasePubSub {
publish<ArgType = any>(_eventName: string | any, _data?: ArgType, delay?: number, assignEventCallback?: any): any;
subscribe<ArgType = any>(_eventName: string | Function, _callback: (data: ArgType) => void): any;
subscribe<ArgType = any>(_eventName: string | string[] | Function, _callback: (data: ArgType) => void): any;
}
type PubSubPublishType<ArgType = any> = { args: ArgType; eventData?: SlickEventData<ArgType>; nativeEvent?: Event; };

Expand Down
7 changes: 4 additions & 3 deletions packages/common/src/services/groupingAndColspan.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ export class GroupingAndColspanService {

// for both picker (columnPicker/gridMenu) we also need to re-create after hiding/showing columns
this._subscriptions.push(
this.pubSubService.subscribe(`onColumnPickerColumnsChanged`, () => this.renderPreHeaderRowGroupingTitles()),
this.pubSubService.subscribe(
['onColumnPickerColumnsChanged', 'onGridMenuColumnsChanged', 'onGridMenuMenuClose'],
() => this.renderPreHeaderRowGroupingTitles()
),
this.pubSubService.subscribe('onHeaderMenuHideColumns', () => this.delayRenderPreHeaderRowGroupingTitles(0)),
this.pubSubService.subscribe(`onGridMenuColumnsChanged`, () => this.renderPreHeaderRowGroupingTitles()),
this.pubSubService.subscribe(`onGridMenuMenuClose`, () => this.renderPreHeaderRowGroupingTitles()),
);

// we also need to re-create after a grid resize
Expand Down
7 changes: 3 additions & 4 deletions packages/common/src/services/pagination.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ export class PaginationService {
}

// Subscribe to Filter Clear & Changed and go back to page 1 when that happen
this._subscriptions.push(this.pubSubService.subscribe('onFilterChanged', () => this.resetPagination()));
this._subscriptions.push(this.pubSubService.subscribe('onFilterCleared', () => this.resetPagination()));
this._subscriptions.push(this.pubSubService.subscribe(['onFilterChanged', 'onFilterCleared'], () => this.resetPagination()));

// when using Infinite Scroll (only), we also need to reset pagination when sorting
if (backendServiceApi?.options?.infiniteScroll) {
Expand All @@ -153,8 +152,8 @@ export class PaginationService {
// Subscribe to any dataview row count changed so that when Adding/Deleting item(s) through the DataView
// that would trigger a refresh of the pagination numbers
if (this.dataView) {
this._subscriptions.push(this.pubSubService.subscribe<any | any[]>(`onItemAdded`, items => this.processOnItemAddedOrRemoved(items, true)));
this._subscriptions.push(this.pubSubService.subscribe<any | any[]>(`onItemDeleted`, items => this.processOnItemAddedOrRemoved(items, false)));
this._subscriptions.push(this.pubSubService.subscribe<any | any[]>('onItemAdded', items => this.processOnItemAddedOrRemoved(items, true)));
this._subscriptions.push(this.pubSubService.subscribe<any | any[]>('onItemDeleted', items => this.processOnItemAddedOrRemoved(items, false)));
}

this.refreshPagination(false, false, true);
Expand Down
37 changes: 36 additions & 1 deletion packages/event-pub-sub/src/eventPubSub.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,37 @@ describe('EventPubSub Service', () => {
expect(service.subscribedEvents.length).toBe(0);
});

it('should unsubscribeAll events', () => {
it('should be able to provide an array of event to subscribe and be able to unsubscribeAll events', () => {
const removeEventSpy = vi.spyOn(divContainer, 'removeEventListener');
const getEventNameSpy = vi.spyOn(service, 'getEventNameByNamingConvention');
const unsubscribeSpy = vi.spyOn(service, 'unsubscribe');
const mockCallback = vi.fn();

service.subscribe(['onClick', 'onDblClick'], mockCallback);
divContainer.dispatchEvent(new CustomEvent('onClick', { detail: { name: 'John' } }));

expect(getEventNameSpy).toHaveBeenCalledWith('onClick', '');
expect(getEventNameSpy).toHaveBeenCalledWith('onDblClick', '');
expect(service.subscribedEventNames).toEqual(['onClick', 'onDblClick']);
expect(service.subscribedEvents.length).toBe(2);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith({ name: 'John' });

divContainer.dispatchEvent(new CustomEvent('onClick', { detail: { name: 'John' } }));
divContainer.dispatchEvent(new CustomEvent('onDblClick', { detail: { name: 'Jane' } }));

expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith({ name: 'John' });
expect(mockCallback).toHaveBeenCalledWith({ name: 'Jane' });

service.unsubscribeAll();
expect(removeEventSpy).toHaveBeenCalledWith('onClick', mockCallback);
expect(removeEventSpy).toHaveBeenCalledWith('onDblClick', mockCallback);
expect(unsubscribeSpy).toHaveBeenCalledTimes(2);
expect(service.subscribedEvents.length).toBe(0);
});

it('should be able to subscribe to multiple event and be able to unsubscribeAll events', () => {
const removeEventSpy = vi.spyOn(divContainer, 'removeEventListener');
const getEventNameSpy = vi.spyOn(service, 'getEventNameByNamingConvention');
const unsubscribeSpy = vi.spyOn(service, 'unsubscribe');
Expand All @@ -234,6 +264,11 @@ describe('EventPubSub Service', () => {
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith({ name: 'John' });

divContainer.dispatchEvent(new CustomEvent('onDblClick', { detail: { name: 'Jane' } }));

expect(mockDblCallback).toHaveBeenCalledTimes(1);
expect(mockDblCallback).toHaveBeenCalledWith({ name: 'Jane' });

service.unsubscribeAll();
expect(removeEventSpy).toHaveBeenCalledWith('onClick', mockCallback);
expect(removeEventSpy).toHaveBeenCalledWith('onDblClick', mockDblCallback);
Expand Down
24 changes: 15 additions & 9 deletions packages/event-pub-sub/src/eventPubSub.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,23 @@ export class EventPubSubService implements BasePubSubService {
* @param callback The callback to be invoked when the specified message is published.
* @return possibly a Subscription
*/
subscribe<T = any>(eventName: string, callback: (data: T) => void): Subscription {
const eventNameByConvention = this.getEventNameByNamingConvention(eventName, '');
subscribe<T = any>(eventNames: string | string[], callback: (data: T) => void): Subscription {
eventNames = Array.isArray(eventNames) ? eventNames : [eventNames];
const subscriptions: Array<() => void> = [];

eventNames.forEach(eventName => {
const eventNameByConvention = this.getEventNameByNamingConvention(eventName, '');

// the event listener will return the data in the "event.detail", so we need to return its content to the final callback
// basically we substitute the "data" with "event.detail" so that the user ends up with only the "data" result
this._elementSource.addEventListener(eventNameByConvention, (event: CustomEventInit<T>) => callback.call(null, event.detail as T));
this._subscribedEvents.push({ name: eventNameByConvention, listener: callback });
// the event listener will return the data in the "event.detail", so we need to return its content to the final callback
// basically we substitute the "data" with "event.detail" so that the user ends up with only the "data" result
this._elementSource.addEventListener(eventNameByConvention, (event: CustomEventInit<T>) => callback.call(null, event.detail as T));
this._subscribedEvents.push({ name: eventNameByConvention, listener: callback });
subscriptions.push(() => this.unsubscribe(eventNameByConvention, callback as never));
});

// return a subscription that we can unsubscribe
// return a subscription(s) that we can unsubscribed
return {
unsubscribe: () => this.unsubscribe(eventNameByConvention, callback as never)
unsubscribe: () => subscriptions.forEach(unsub => unsub())
};
}

Expand All @@ -146,7 +152,7 @@ export class EventPubSubService implements BasePubSubService {
this._elementSource.addEventListener(eventNameByConvention, listener);
this._subscribedEvents.push({ name: eventNameByConvention, listener });

// return a subscription that we can unsubscribe
// return a subscription that we can unsubscribed
return {
unsubscribe: () => this.unsubscribe(eventNameByConvention, listener as never)
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface BasePubSubService {
* @param callback The callback to be invoked when the specified message is published.
* @return possibly a Subscription
*/
subscribe<T = any>(_eventName: string | Function, _callback: (data: T) => void): EventSubscription | any;
subscribe<T = any>(_eventName: string | string[] | Function, _callback: (data: T) => void): EventSubscription | any;

/**
* Subscribes to a custom event message channel or message type.
Expand Down

0 comments on commit 6d39c2c

Please sign in to comment.