Skip to content

Commit

Permalink
feat: use PubSub Service singleton to subscribe to any SlickEvent (#1248
Browse files Browse the repository at this point in the history
)

* feat: use PubSub Service singleton to subscribe to any SlickEvent
- by modifying the SlickEvent class to also publish an event when a PubSub Service is provided to a SlickEvent, we can avoid having our previous monkey patch of subscribing to **all** SlickGrid/DataView events to then dispatch event, this acted like a middleware, we can avoid all of that middleware with this new approach and publish right away via the PubSub when provided
- also removed `defaultComponentEventPrefix`, `defaultSlickgridEventPrefix` grid options since they will not be used anymore
  • Loading branch information
ghiscoding committed Dec 6, 2023
1 parent 74fa5b5 commit 388bd11
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 132 deletions.
47 changes: 47 additions & 0 deletions packages/common/src/core/__tests__/slickCore.spec.ts
@@ -1,7 +1,16 @@
import 'jest-extended';
import { BasePubSubService } from '@slickgrid-universal/event-pub-sub';

import { EditController } from '../../interfaces';
import { SlickEditorLock, SlickEvent, SlickEventData, SlickEventHandler, SlickGroup, SlickGroupTotals, SlickRange, Utils } from '../slickCore';

const pubSubServiceStub = {
publish: jest.fn(),
subscribe: jest.fn(),
unsubscribe: jest.fn(),
unsubscribeAll: jest.fn(),
} as BasePubSubService;

describe('SlickCore file', () => {
describe('SlickEventData class', () => {
it('should call isPropagationStopped() and expect truthy when event propagation is stopped by calling stopPropagation()', () => {
Expand Down Expand Up @@ -97,6 +106,44 @@ describe('SlickCore file', () => {

expect(spy1).toHaveBeenCalledWith(ed, { hello: 'world' });
});

it('should be able to add a PubSub instance to the SlickEvent call notify() and expect PubSub .publish() to be called as well', () => {
const ed = new SlickEventData();
const onClick = new SlickEvent('onClick', pubSubServiceStub);

onClick.notify({ hello: 'world' }, ed);

expect(pubSubServiceStub.publish).toHaveBeenCalledWith('onClick', { eventData: ed, args: { hello: 'world' } });
});

it('should be able to mix a PubSub with regular SlickEvent subscribe and expect both to be triggered by the SlickEvent call notify()', () => {
const spy1 = jest.fn();
const spy2 = jest.fn();
const ed = new SlickEventData();
const onClick = new SlickEvent('onClick', pubSubServiceStub);
onClick.subscribe(spy1);
onClick.subscribe(spy2);

expect(onClick.subscriberCount).toBe(2);

onClick.notify({ hello: 'world' }, ed);

expect(spy1).toHaveBeenCalledWith(ed, { hello: 'world' });
expect(pubSubServiceStub.publish).toHaveBeenCalledWith('onClick', { eventData: ed, args: { hello: 'world' } });
});

it('should be able to call addSlickEventPubSubWhenDefined() and expect PubSub to be available in SlickEvent', () => {
const ed = new SlickEventData();
const onClick = new SlickEvent('onClick');
const scope = { onClick };
const setPubSubSpy = jest.spyOn(onClick, 'setPubSubService');

Utils.addSlickEventPubSubWhenDefined(pubSubServiceStub, scope);
onClick.notify({ hello: 'world' }, ed);

expect(setPubSubSpy).toHaveBeenCalledWith(pubSubServiceStub);
expect(pubSubServiceStub.publish).toHaveBeenCalledWith('onClick', { eventData: ed, args: { hello: 'world' } });
});
});

describe('SlickEventHandler class', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/core/__tests__/slickGrid.spec.ts
Expand Up @@ -20,7 +20,7 @@ describe('SlickGrid core file', () => {
it('should be able to instantiate SlickGrid without DataView', () => {
const columns = [{ id: 'firstName', field: 'firstName', name: 'First Name' }] as Column[];
const options = { enableCellNavigation: true } as GridOption;
grid = new SlickGrid<any, Column>('#myGrid', [], columns, options, true);
grid = new SlickGrid<any, Column>('#myGrid', [], columns, options, undefined, true);
grid.init();

expect(grid).toBeTruthy();
Expand All @@ -31,7 +31,7 @@ describe('SlickGrid core file', () => {
const columns = [{ id: 'firstName', field: 'firstName', name: 'First Name' }] as Column[];
const options = { enableCellNavigation: true } as GridOption;
const dv = new SlickDataView({});
grid = new SlickGrid<any, Column>('#myGrid', dv, columns, options, true);
grid = new SlickGrid<any, Column>('#myGrid', dv, columns, options, undefined, true);
grid.init();

expect(grid).toBeTruthy();
Expand Down
70 changes: 53 additions & 17 deletions packages/common/src/core/slickCore.ts
Expand Up @@ -14,6 +14,11 @@ import type { CSSStyleDeclarationWritable, EditController } from '../interfaces'

export type Handler<ArgType = any> = (e: any, args: ArgType) => void;

export interface BasePubSub {
publish<ArgType = any>(_eventName: string | any, _data?: ArgType): any;
subscribe<ArgType = any>(_eventName: string | Function, _callback: (data: ArgType) => void): any;
}

/**
* An event object for passing data to event handlers and letting them control propagation.
* <p>This is pretty much identical to how W3C and jQuery implement events.</p>
Expand Down Expand Up @@ -125,60 +130,75 @@ export class SlickEventData<ArgType = any> {
* @constructor
*/
export class SlickEvent<ArgType = any> {
protected handlers: Handler<ArgType>[] = [];
protected _handlers: Handler<ArgType>[] = [];
protected _pubSubService?: BasePubSub;

get subscriberCount() {
return this.handlers.length;
return this._handlers.length;
}

/**
* Constructor
* @param {String} [eventName] - event name that could be used for dispatching CustomEvent (when enabled)
* @param {BasePubSub} [pubSubService] - event name that could be used for dispatching CustomEvent (when enabled)
*/
constructor(protected readonly eventName?: string, protected readonly pubSub?: BasePubSub) {
this._pubSubService = pubSub;
}

/**
* Adds an event handler to be called when the event is fired.
* <p>Event handler will receive two arguments - an <code>EventData</code> and the <code>data</code>
* object the event was fired with.<p>
* @method subscribe
* @param fn {Function} Event handler.
* @param {Function} fn - Event handler.
*/
subscribe(fn: Handler<ArgType>) {
this.handlers.push(fn);
this._handlers.push(fn);
}

/**
* Removes an event handler added with <code>subscribe(fn)</code>.
* @method unsubscribe
* @param fn {Function} Event handler to be removed.
* @param {Function} [fn] - Event handler to be removed.
*/
unsubscribe(fn?: Handler<ArgType>) {
for (let i = this.handlers.length - 1; i >= 0; i--) {
if (this.handlers[i] === fn) {
this.handlers.splice(i, 1);
for (let i = this._handlers.length - 1; i >= 0; i--) {
if (this._handlers[i] === fn) {
this._handlers.splice(i, 1);
}
}
}

/**
* Fires an event notifying all subscribers.
* @method notify
* @param args {Object} Additional data object to be passed to all handlers.
* @param e {EventData}
* Optional.
* An <code>EventData</code> object to be passed to all handlers.
* @param {Object} args Additional data object to be passed to all handlers.
* @param {EventData} [event] - An <code>EventData</code> object to be passed to all handlers.
* For DOM events, an existing W3C event object can be passed in.
* @param scope {Object}
* Optional.
* The scope ("this") within which the handler will be executed.
* @param {Object} [scope] - The scope ("this") within which the handler will be executed.
* If not specified, the scope will be set to the <code>Event</code> instance.
*/
notify(args: ArgType, evt?: SlickEventData | Event | MergeTypes<SlickEventData, Event> | null, scope?: any) {
const sed = evt instanceof SlickEventData ? evt : new SlickEventData(evt, args);
scope = scope || this;

for (let i = 0; i < this.handlers.length && !(sed.isPropagationStopped() || sed.isImmediatePropagationStopped()); i++) {
const returnValue = this.handlers[i].call(scope, sed as SlickEvent | SlickEventData, args);
for (let i = 0; i < this._handlers.length && !(sed.isPropagationStopped() || sed.isImmediatePropagationStopped()); i++) {
const returnValue = this._handlers[i].call(scope, sed as SlickEvent | SlickEventData, args);
sed.addReturnValue(returnValue);
}

// user can optionally add a global PubSub Service which makes it easy to publish/subscribe to events
if (typeof this._pubSubService?.publish === 'function' && this.eventName) {
const ret = this._pubSubService.publish<{ args: ArgType; eventData?: Event | SlickEventData; nativeEvent?: Event; }>(this.eventName, { args, eventData: sed });
sed.addReturnValue(ret);
}
return sed;
}

setPubSubService(pubSub: BasePubSub) {
this._pubSubService = pubSub;
}
}

export class SlickEventHandler<ArgType = any> {
Expand Down Expand Up @@ -730,6 +750,22 @@ export class Utils {
}
}
}

/**
* User could optionally add PubSub Service to SlickEvent
* When it is defined then a SlickEvent `notify()` call will also dispatch it by using the PubSub publish() method
* @param {BasePubSub} [pubSubService]
* @param {*} scope
*/
public static addSlickEventPubSubWhenDefined<T = any>(pubSub?: BasePubSub, scope?: T) {
if (pubSub) {
for (const prop in scope) {
if (scope[prop] instanceof SlickEvent && typeof (scope[prop] as SlickEvent).setPubSubService === 'function') {
(scope[prop] as SlickEvent).setPubSubService(pubSub);
}
}
}
}
}

export const SlickGlobalEditorLock = new SlickEditorLock();
Expand Down
33 changes: 22 additions & 11 deletions packages/common/src/core/slickDataview.ts
Expand Up @@ -19,6 +19,7 @@ import type {
} from '../interfaces';
import { CssStyleHash, CustomDataView } from '../interfaces/gridOption.interface';
import {
type BasePubSub,
SlickEvent,
SlickEventData,
SlickGroup,
Expand Down Expand Up @@ -114,17 +115,27 @@ export class SlickDataView<TData extends SlickDataItem = any> implements CustomD
protected _options: DataViewOption;

// public events
onBeforePagingInfoChanged = new SlickEvent<PagingInfo>();
onGroupExpanded = new SlickEvent<OnGroupExpandedEventArgs>();
onGroupCollapsed = new SlickEvent<OnGroupCollapsedEventArgs>();
onPagingInfoChanged = new SlickEvent<PagingInfo>();
onRowCountChanged = new SlickEvent<OnRowCountChangedEventArgs>();
onRowsChanged = new SlickEvent<OnRowsChangedEventArgs>();
onRowsOrCountChanged = new SlickEvent<OnRowsOrCountChangedEventArgs>();
onSelectedRowIdsChanged = new SlickEvent<OnSelectedRowIdsChangedEventArgs>();
onSetItemsCalled = new SlickEvent<OnSetItemsCalledEventArgs>();

constructor(options: Partial<DataViewOption>) {
onBeforePagingInfoChanged: SlickEvent<PagingInfo>;
onGroupExpanded: SlickEvent<OnGroupExpandedEventArgs>;
onGroupCollapsed: SlickEvent<OnGroupCollapsedEventArgs>;
onPagingInfoChanged: SlickEvent<PagingInfo>;
onRowCountChanged: SlickEvent<OnRowCountChangedEventArgs>;
onRowsChanged: SlickEvent<OnRowsChangedEventArgs>;
onRowsOrCountChanged: SlickEvent<OnRowsOrCountChangedEventArgs>;
onSelectedRowIdsChanged: SlickEvent<OnSelectedRowIdsChangedEventArgs>;
onSetItemsCalled: SlickEvent<OnSetItemsCalledEventArgs>;

constructor(options: Partial<DataViewOption>, protected externalPubSub?: BasePubSub) {
this.onBeforePagingInfoChanged = new SlickEvent<PagingInfo>('onBeforePagingInfoChanged', externalPubSub);
this.onGroupExpanded = new SlickEvent<OnGroupExpandedEventArgs>('onGroupExpanded', externalPubSub);
this.onGroupCollapsed = new SlickEvent<OnGroupCollapsedEventArgs>('onGroupCollapsed', externalPubSub);
this.onPagingInfoChanged = new SlickEvent<PagingInfo>('onPagingInfoChanged', externalPubSub);
this.onRowCountChanged = new SlickEvent<OnRowCountChangedEventArgs>('onRowCountChanged', externalPubSub);
this.onRowsChanged = new SlickEvent<OnRowsChangedEventArgs>('onRowsChanged', externalPubSub);
this.onRowsOrCountChanged = new SlickEvent<OnRowsOrCountChangedEventArgs>('onRowsOrCountChanged', externalPubSub);
this.onSelectedRowIdsChanged = new SlickEvent<OnSelectedRowIdsChangedEventArgs>('onSelectedRowIdsChanged', externalPubSub);
this.onSetItemsCalled = new SlickEvent<OnSetItemsCalledEventArgs>('onSetItemsCalled', externalPubSub);

this._options = Utils.extend(true, {}, this.defaults, options);
}

Expand Down

0 comments on commit 388bd11

Please sign in to comment.