Skip to content

Commit

Permalink
feat(services): add extra features to EventPubSub Service
Browse files Browse the repository at this point in the history
- `subscribe` now returns a Subscription that user can now optionally call `unsubscribe` directly instead of just `unsubscribeAll`
- `unsubscribe` and `unsubscribeAll` should decrease the `subscribedEvents` array (it wasn't before, but now it does decrease it)
- add Generics to `subscribe` for easier type inference on `data` argument
- add optional GETTER/SETTER on the `elementSource`
  • Loading branch information
ghiscoding committed Dec 23, 2021
1 parent 98aae2a commit 9bd02b5
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 23 deletions.
10 changes: 5 additions & 5 deletions packages/common/src/services/gridState.service.ts
Expand Up @@ -392,7 +392,7 @@ export class GridStateService {
subscribeToAllGridChanges(grid: SlickGrid) {
// Subscribe to Event Emitter of Filter changed
this._subscriptions.push(
this.pubSubService.subscribe('onFilterChanged', (currentFilters: CurrentFilter[]) => {
this.pubSubService.subscribe<CurrentFilter[]>('onFilterChanged', currentFilters => {
this.resetRowSelectionWhenRequired();
this.pubSubService.publish('onGridStateChanged', { change: { newValues: currentFilters, type: GridStateType.filter }, gridState: this.getCurrentGridState({ requestRefreshRowFilteredRow: !this.hasRowSelectionEnabled() }) });

Expand All @@ -412,7 +412,7 @@ export class GridStateService {

// Subscribe to Event Emitter of Sort changed
this._subscriptions.push(
this.pubSubService.subscribe('onSortChanged', (currentSorters: CurrentSorter[]) => {
this.pubSubService.subscribe<CurrentSorter[]>('onSortChanged', currentSorters => {
this.resetRowSelectionWhenRequired();
this.pubSubService.publish('onGridStateChanged', { change: { newValues: currentSorters, type: GridStateType.sorter }, gridState: this.getCurrentGridState() });
})
Expand Down Expand Up @@ -441,22 +441,22 @@ export class GridStateService {

// subscribe to HeaderMenu (hide column)
this._subscriptions.push(
this.pubSubService.subscribe('onHeaderMenuHideColumns', (visibleColumns: Column[]) => {
this.pubSubService.subscribe<Column[]>('onHeaderMenuHideColumns', visibleColumns => {
const currentColumns: CurrentColumn[] = this.getAssociatedCurrentColumns(visibleColumns);
this.pubSubService.publish('onGridStateChanged', { change: { newValues: currentColumns, type: GridStateType.columns }, gridState: this.getCurrentGridState() });
})
);

// subscribe to Tree Data toggle items changes
this._subscriptions.push(
this.pubSubService.subscribe('onTreeItemToggled', (toggleChange: TreeToggleStateChange) => {
this.pubSubService.subscribe<TreeToggleStateChange>('onTreeItemToggled', toggleChange => {
this.pubSubService.publish('onGridStateChanged', { change: { newValues: toggleChange, type: GridStateType.treeData }, gridState: this.getCurrentGridState() });
})
);

// subscribe to Tree Data full tree toggle changes
this._subscriptions.push(
this.pubSubService.subscribe('onTreeFullToggleEnd', (toggleChange: Omit<TreeToggleStateChange, 'fromItemId'>) => {
this.pubSubService.subscribe<Omit<TreeToggleStateChange, 'fromItemId'>>('onTreeFullToggleEnd', toggleChange => {
this.pubSubService.publish('onGridStateChanged', { change: { newValues: toggleChange, type: GridStateType.treeData }, gridState: this.getCurrentGridState() });
})
);
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/services/pagination.service.ts
Expand Up @@ -125,8 +125,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(`onItemAdded`, (items: any | any[]) => this.processOnItemAddedOrRemoved(items, true)));
this._subscriptions.push(this.pubSubService.subscribe(`onItemDeleted`, (items: any | any[]) => 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
47 changes: 42 additions & 5 deletions packages/event-pub-sub/src/eventPubSub.service.spec.ts
Expand Up @@ -17,6 +17,15 @@ describe('EventPubSub Service', () => {

it('should create the service', () => {
expect(service).toBeTruthy();
expect(service.elementSource).toBeTruthy();
});

it('should be able to change the service element source afterward', () => {
const div = document.createElement('div');
div.className = 'test';
service.elementSource = div;

expect(service.elementSource).toEqual(div);
});

describe('publish method', () => {
Expand Down Expand Up @@ -137,7 +146,7 @@ describe('EventPubSub Service', () => {
const mockCallback = jest.fn();

service.eventNamingStyle = EventNamingStyle.kebabCase;
service.subscribeEvent('onClick', mockCallback);
const subscription = service.subscribeEvent('onClick', mockCallback);
divContainer.dispatchEvent(new CustomEvent('on-click', { composed: true, detail: { name: 'John' } }));

expect(getEventNameSpy).toHaveBeenCalledWith('onClick', '');
Expand All @@ -146,6 +155,9 @@ describe('EventPubSub Service', () => {
expect(addEventSpy).toHaveBeenCalledWith('on-click', expect.any(Function));
expect(mockCallback).toHaveBeenCalledTimes(1);
// expect(mockCallback).toHaveBeenCalledWith({ detail: { name: 'John' } });

subscription.unsubscribe();
expect(service.subscribedEvents.length).toBe(0);
});
});

Expand All @@ -157,14 +169,35 @@ describe('EventPubSub Service', () => {

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

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

service.unsubscribe('onClick', mockCallback);
expect(removeEventSpy).toHaveBeenCalledWith('onClick', mockCallback);
expect(service.subscribedEvents.length).toBe(0);
});

it('should be able to unsubscribe directly from the subscription', () => {
const removeEventSpy = jest.spyOn(divContainer, 'removeEventListener');
const getEventNameSpy = jest.spyOn(service, 'getEventNameByNamingConvention');
const mockCallback = jest.fn();

const subscription = service.subscribe('onClick', mockCallback);
divContainer.dispatchEvent(new CustomEvent('onClick', { detail: { name: 'John' } }));

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

subscription.unsubscribe();
expect(removeEventSpy).toHaveBeenCalledWith('onClick', mockCallback);
expect(service.subscribedEvents.length).toBe(0);
});

it('should unsubscribeAll events', () => {
Expand All @@ -177,17 +210,19 @@ describe('EventPubSub Service', () => {
service.subscribe('onClick', mockCallback);
service.subscribe('onDblClick', mockDblCallback);
divContainer.dispatchEvent(new CustomEvent('onClick', { detail: { name: 'John' } }));
service.unsubscribeAll();

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' });

service.unsubscribeAll();
expect(removeEventSpy).toHaveBeenCalledWith('onClick', mockCallback);
expect(removeEventSpy).toHaveBeenCalledWith('onDblClick', mockDblCallback);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(unsubscribeSpy).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenCalledWith({ name: 'John' });
expect(service.subscribedEvents.length).toBe(0);
});

it('should unsubscribe all PubSub by disposing all of them', () => {
Expand All @@ -201,6 +236,7 @@ describe('EventPubSub Service', () => {

expect(mockDispose1).toHaveBeenCalledTimes(1);
expect(mockDispose2).toHaveBeenCalledTimes(1);
expect(service.subscribedEvents.length).toBe(0);
});

it('should unsubscribe all PubSub by unsubscribing all of them', () => {
Expand All @@ -214,6 +250,7 @@ describe('EventPubSub Service', () => {

expect(mockUnsubscribe1).toHaveBeenCalledTimes(1);
expect(mockUnsubscribe2).toHaveBeenCalledTimes(1);
expect(service.subscribedEvents.length).toBe(0);
});
});
});
45 changes: 37 additions & 8 deletions packages/event-pub-sub/src/eventPubSub.service.ts
@@ -1,8 +1,8 @@
import { EventNamingStyle, EventSubscription, PubSubService, titleCase, toKebabCase } from '@slickgrid-universal/common';
import { EventNamingStyle, EventSubscription, PubSubService, Subscription, titleCase, toKebabCase } from '@slickgrid-universal/common';

interface PubSubEvent {
interface PubSubEvent<T = any> {
name: string;
listener: (event: CustomEventInit) => void;
listener: (event: T | CustomEventInit<T>) => void;
}

export class EventPubSubService implements PubSubService {
Expand All @@ -11,6 +11,13 @@ export class EventPubSubService implements PubSubService {

eventNamingStyle = EventNamingStyle.camelCase;

get elementSource() {
return this._elementSource;
}
set elementSource(element: Element) {
this._elementSource = element;
}

get subscribedEvents(): PubSubEvent[] {
return this._subscribedEvents;
}
Expand Down Expand Up @@ -54,13 +61,18 @@ export class EventPubSubService implements PubSubService {
* @param callback The callback to be invoked when the specified message is published.
* @return possibly a Subscription
*/
subscribe<T = any>(eventName: string, callback: (data: any) => void): any {
subscribe<T = any>(eventName: string, callback: (data: T) => void): Subscription {
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 });

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

/**
Expand All @@ -70,35 +82,45 @@ export class EventPubSubService implements PubSubService {
* @param callback The callback to be invoked when the specified message is published.
* @return possibly a Subscription
*/
subscribeEvent<T = any>(eventName: string, listener: (event: CustomEventInit<T>) => void): any | void {
subscribeEvent<T = any>(eventName: string, listener: (event: CustomEventInit<T>) => void): Subscription {
const eventNameByConvention = this.getEventNameByNamingConvention(eventName, '');
this._elementSource.addEventListener(eventNameByConvention, listener);
this._subscribedEvents.push({ name: eventNameByConvention, listener });

// return a subscription that we can unsubscribe
return {
unsubscribe: () => this.unsubscribe(eventNameByConvention, listener as never)
};
}

/**
* Unsubscribes a message name
* @param event The event name
* @return possibly a Subscription
*/
unsubscribe(eventName: string, listener: (event: CustomEventInit) => void) {
unsubscribe<T = any>(eventName: string, listener: (event: T | CustomEventInit<T>) => void) {
const eventNameByConvention = this.getEventNameByNamingConvention(eventName, '');
this._elementSource.removeEventListener(eventNameByConvention, listener);
this.removeSubscribedEventWhenFound(eventName, listener);
}

/** Unsubscribes all subscriptions that currently exists */
unsubscribeAll(subscriptions?: EventSubscription[]) {
if (Array.isArray(subscriptions)) {
for (const subscription of subscriptions) {
let subscription = subscriptions.pop();
while (subscription) {
if (subscription?.dispose) {
subscription.dispose();
} else if (subscription?.unsubscribe) {
subscription.unsubscribe();
}
subscription = subscriptions.pop();
}
} else {
for (const pubSubEvent of this._subscribedEvents) {
let pubSubEvent = this._subscribedEvents.pop();
while (pubSubEvent) {
this.unsubscribe(pubSubEvent.name, pubSubEvent.listener);
pubSubEvent = this._subscribedEvents.pop();
}
}
}
Expand Down Expand Up @@ -132,4 +154,11 @@ export class EventPubSubService implements PubSubService {
}
return outputEventName;
}

protected removeSubscribedEventWhenFound<T>(eventName: string, listener: (event: T | CustomEventInit<T>) => void) {
const eventIdx = this._subscribedEvents.findIndex(evt => evt.name === eventName && evt.listener === listener);
if (eventIdx >= 0) {
this._subscribedEvents.splice(eventIdx, 1);
}
}
}
Expand Up @@ -53,7 +53,7 @@ export class SlickPaginationComponent {
// Anytime the pagination is initialized or has changes,
// we'll copy the data into a local object so that we can add binding to this local object
this._subscriptions.push(
this.pubSubService.subscribe('onPaginationRefreshed', (paginationChanges: ServicePagination) => {
this.pubSubService.subscribe<ServicePagination>('onPaginationRefreshed', paginationChanges => {
for (const key of Object.keys(paginationChanges)) {
(this.currentPagination as any)[key] = (paginationChanges as any)[key];
}
Expand Down
Expand Up @@ -1120,8 +1120,8 @@ export class SlickVanillaGridBundle {
this.paginationService.totalItems = this.totalItems;
this.paginationService.init(this.slickGrid, paginationOptions, this.backendServiceApi);
this.subscriptions.push(
this._eventPubSubService.subscribe('onPaginationChanged', (paginationChanges: ServicePagination) => this.paginationChanged(paginationChanges)),
this._eventPubSubService.subscribe('onPaginationVisibilityChanged', (visibility: { visible: boolean }) => {
this._eventPubSubService.subscribe<ServicePagination>('onPaginationChanged', paginationChanges => this.paginationChanged(paginationChanges)),
this._eventPubSubService.subscribe<{ visible: boolean; }>('onPaginationVisibilityChanged', visibility => {
this.showPagination = visibility?.visible ?? false;
if (this.gridOptions?.backendServiceApi) {
this.backendUtilityService?.refreshBackendDataset(this.gridOptions);
Expand Down

0 comments on commit 9bd02b5

Please sign in to comment.