Skip to content

Commit

Permalink
feat: add few pubsub events to help with big dataset
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed May 11, 2021
1 parent bbcd4fd commit 360c62c
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ <h6 class="title is-6 italic">
<button onclick.delegate="logHierarchicalStructure()" class="button is-small">
<span>Log Hierarchical Structure</span>
</button>
<span class.bind="loadingClass"></span>
</div>
</div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions examples/webpack-demo-vanilla-bundle/src/examples/example05.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BindingEventService,
Column,
FieldType,
Filters,
Expand All @@ -14,11 +15,17 @@ import './example05.scss';
const NB_ITEMS = 500;

export class Example5 {
private _bindingEventService: BindingEventService;
columnDefinitions: Column[];
gridOptions: GridOption;
dataset: any[];
sgb: SlickVanillaGridBundle;
durationOrderByCount = false;
loadingClass = '';

constructor() {
this._bindingEventService = new BindingEventService();
}

attached() {
this.initializeGrid();
Expand All @@ -28,6 +35,17 @@ export class Example5 {
this.sgb = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions });
this.dataset = this.loadData(NB_ITEMS);
// this.sgb.dataset = this.dataset;

// with large dataset you maybe want to show spinner before/after these events: sorting/filtering/collapsing/expanding
const spinnerClass = 'mdi mdi-load mdi-spin-1s mdi-22px';
this._bindingEventService.bind(gridContainerElm, 'onbeforefilterchanged', () => this.loadingClass = spinnerClass);
this._bindingEventService.bind(gridContainerElm, 'onfilterchanged', () => this.loadingClass = '');
this._bindingEventService.bind(gridContainerElm, 'onbeforefilterclear', () => this.loadingClass = spinnerClass);
this._bindingEventService.bind(gridContainerElm, 'onfiltercleared', () => this.loadingClass = '');
this._bindingEventService.bind(gridContainerElm, 'onbeforesortchange', () => this.loadingClass = spinnerClass);
this._bindingEventService.bind(gridContainerElm, 'onsortchanged', () => this.loadingClass = '');
this._bindingEventService.bind(gridContainerElm, 'onbeforetoggletreecollapse', () => this.loadingClass = spinnerClass);
this._bindingEventService.bind(gridContainerElm, 'ontoggletreecollapsed', () => this.loadingClass = '');
}

dispose() {
Expand Down
14 changes: 14 additions & 0 deletions packages/common/src/services/__tests__/filter.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ describe('FilterService', () => {
expect(spyBackendChange).toHaveBeenCalledWith(expect.anything(), mockSearchArgs);
setTimeout(() => {
expect(spyCurrentFilters).toHaveBeenCalled();
expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterChange`, [{ columnId: 'isActive', operator: 'EQ', searchTerms: ['John'] }]);
expect(pubSubSpy).toHaveBeenCalledWith(`onFilterChanged`, [{ columnId: 'isActive', operator: 'EQ', searchTerms: ['John'] }]);
done();
});
Expand Down Expand Up @@ -280,6 +281,7 @@ describe('FilterService', () => {
service.bindLocalOnFilter(gridStub);
service.onSearchChange!.notify(mockArgs as any, new Slick.EventData(), gridStub);

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterChange`, []);
expect(pubSubSpy).toHaveBeenCalledWith(`onFilterChanged`, []);
});

Expand Down Expand Up @@ -308,6 +310,7 @@ describe('FilterService', () => {
gridStub.onHeaderRowCellRendered.notify(mockHeaderArgs as any, new Slick.EventData(), gridStub);
service.onSearchChange!.notify(mockSearchArgs, new Slick.EventData(), gridStub);

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterChange`, [{ columnId: 'firstName', operator: 'EQ', searchTerms: [true] }]);
expect(pubSubSpy).toHaveBeenCalledWith(`onFilterChanged`, [{ columnId: 'firstName', operator: 'EQ', searchTerms: [true] }]);
});
});
Expand Down Expand Up @@ -467,11 +470,13 @@ describe('FilterService', () => {
const spyClear = jest.spyOn(service.getFiltersMetadata()[0], 'clear');
const spyFilterChange = jest.spyOn(service, 'onBackendFilterChange');
const spyEmitter = jest.spyOn(service, 'emitFilterChanged');
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');

const filterCountBefore = Object.keys(service.getColumnFilters()).length;
service.clearFilterByColumnId(newEvent, 'firstName');
const filterCountAfter = Object.keys(service.getColumnFilters()).length;

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterClear`, { columnId: 'firstName' });
expect(spyClear).toHaveBeenCalled();
expect(spyFilterChange).toHaveBeenCalledWith(newEvent, { grid: gridStub, columnFilters: { lastName: filterExpectation } });
expect(filterCountBefore).toBe(2);
Expand All @@ -487,11 +492,13 @@ describe('FilterService', () => {
const spyClear = jest.spyOn(service.getFiltersMetadata()[2], 'clear');
const spyFilterChange = jest.spyOn(service, 'onBackendFilterChange');
const spyEmitter = jest.spyOn(service, 'emitFilterChanged');
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');

const filterCountBefore = Object.keys(service.getColumnFilters()).length;
service.clearFilterByColumnId(newEvent, 'age');
const filterCountAfter = Object.keys(service.getColumnFilters()).length;

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterClear`, { columnId: 'age' });
expect(spyClear).toHaveBeenCalled();
expect(spyFilterChange).not.toHaveBeenCalled();
expect(filterCountBefore).toBe(2);
Expand Down Expand Up @@ -541,6 +548,7 @@ describe('FilterService', () => {
service.clearFilters();

setTimeout(() => {
expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterClear`, true);
expect(pubSubSpy).toHaveBeenCalledWith(`onFilterCleared`, true);
expect(spyOnError).toHaveBeenCalledWith(errorExpected);
done();
Expand All @@ -561,6 +569,7 @@ describe('FilterService', () => {
service.clearFilters();

setTimeout(() => {
expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterClear`, true);
expect(pubSubSpy).toHaveBeenCalledWith(`onFilterCleared`, true);
expect(spyOnError).toHaveBeenCalledWith(errorExpected);
done();
Expand Down Expand Up @@ -594,11 +603,13 @@ describe('FilterService', () => {
it('should clear the filter by passing a column id as argument on a local grid', () => {
const spyClear = jest.spyOn(service.getFiltersMetadata()[0], 'clear');
const spyEmitter = jest.spyOn(service, 'emitFilterChanged');
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');

const filterCountBefore = Object.keys(service.getColumnFilters()).length;
service.clearFilterByColumnId(new CustomEvent(`mouseup`), 'firstName');
const filterCountAfter = Object.keys(service.getColumnFilters()).length;

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterClear`, { columnId: 'firstName' });
expect(spyClear).toHaveBeenCalled();
expect(filterCountBefore).toBe(2);
expect(filterCountAfter).toBe(1);
Expand Down Expand Up @@ -1603,6 +1614,7 @@ describe('FilterService', () => {
service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['map'] }], true, true, true);
const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters });

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterChange`, [{ columnId: 'file', operator: 'Contains', searchTerms: ['map',] }]);
expect(pubSubSpy).toHaveBeenCalledWith(`onFilterChanged`, [{ columnId: 'file', operator: 'Contains', searchTerms: ['map',] }]);
expect(output).toBe(true);
expect(preFilterSpy).toHaveBeenCalledWith(dataset, columnFilters);
Expand All @@ -1627,6 +1639,7 @@ describe('FilterService', () => {
service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['map'] }], true, true, true);
const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters });

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterChange`, [{ columnId: 'file', operator: 'Contains', searchTerms: ['map'] }]);
expect(pubSubSpy).toHaveBeenCalledWith(`onFilterChanged`, [{ columnId: 'file', operator: 'Contains', searchTerms: ['map'] }]);
expect(output).toBe(false);
expect(preFilterSpy).toHaveBeenCalledWith(dataset, columnFilters);
Expand All @@ -1651,6 +1664,7 @@ describe('FilterService', () => {
service.updateFilters([{ columnId: 'file', operator: '', searchTerms: ['unknown'] }], true, true, true);
const output = service.customLocalFilter(mockItem1, { dataView: dataViewStub, grid: gridStub, columnFilters });

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeFilterChange`, [{ columnId: 'file', operator: 'Contains', searchTerms: ['unknown'] }]);
expect(pubSubSpy).toHaveBeenCalledWith(`onFilterChanged`, [{ columnId: 'file', operator: 'Contains', searchTerms: ['unknown'] }]);
expect(output).toBe(false);
expect(preFilterSpy).toHaveBeenCalledWith(dataset, { ...columnFilters, file: { ...columnFilters.file, operator: 'Contains', parsedSearchTerms: ['unknown'], type: 'string' } }); // it will use Contains by default
Expand Down
19 changes: 18 additions & 1 deletion packages/common/src/services/__tests__/treeData.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Column, SlickDataView, GridOption, SlickEventHandler, SlickGrid, SlickNamespace, BackendService } from '../../interfaces/index';
import { PubSubService } from '../pubSub.service';
import { SharedService } from '../shared.service';
import { SortService } from '../sort.service';
import { TreeDataService } from '../treeData.service';
Expand Down Expand Up @@ -47,6 +48,13 @@ const gridStub = {
setSortColumns: jest.fn(),
} as unknown as SlickGrid;

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

const sortServiceStub = {
clearSorting: jest.fn(),
sortHierarchicalDataset: jest.fn(),
Expand All @@ -65,7 +73,7 @@ describe('TreeData Service', () => {
gridOptionsMock.treeDataOptions = {
columnId: 'file'
};
service = new TreeDataService(sharedService, sortServiceStub);
service = new TreeDataService(pubSubServiceStub, sharedService, sortServiceStub);
slickgridEventHandler = service.eventHandler;
jest.spyOn(gridStub, 'getData').mockReturnValue(dataViewStub);
});
Expand Down Expand Up @@ -235,10 +243,13 @@ describe('TreeData Service', () => {
it('should collapse all items when calling the method with collapsing True', () => {
const dataGetItemsSpy = jest.spyOn(dataViewStub, 'getItems').mockReturnValue(itemsMock);
const dataSetItemsSpy = jest.spyOn(dataViewStub, 'setItems');
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');

service.init(gridStub);
service.toggleTreeDataCollapse(true);

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeToggleTreeCollapse`, { collapsing: true });
expect(pubSubSpy).toHaveBeenCalledWith(`onToggleTreeCollapsed`, { collapsing: true });
expect(dataGetItemsSpy).toHaveBeenCalled();
expect(dataSetItemsSpy).toHaveBeenCalledWith([
{ __collapsed: true, file: 'myFile.txt', size: 0.5, },
Expand All @@ -250,10 +261,13 @@ describe('TreeData Service', () => {
gridOptionsMock.treeDataOptions!.collapsedPropName = 'customCollapsed';
const dataGetItemsSpy = jest.spyOn(dataViewStub, 'getItems').mockReturnValue(itemsMock);
const dataSetItemsSpy = jest.spyOn(dataViewStub, 'setItems');
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');

service.init(gridStub);
service.toggleTreeDataCollapse(true);

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeToggleTreeCollapse`, { collapsing: true });
expect(pubSubSpy).toHaveBeenCalledWith(`onToggleTreeCollapsed`, { collapsing: true });
expect(dataGetItemsSpy).toHaveBeenCalled();
expect(dataSetItemsSpy).toHaveBeenCalledWith([
{ customCollapsed: true, file: 'myFile.txt', size: 0.5, },
Expand All @@ -264,10 +278,13 @@ describe('TreeData Service', () => {
it('should expand all items when calling the method with collapsing False', () => {
const dataGetItemsSpy = jest.spyOn(dataViewStub, 'getItems').mockReturnValue(itemsMock);
const dataSetItemsSpy = jest.spyOn(dataViewStub, 'setItems');
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');

service.init(gridStub);
service.toggleTreeDataCollapse(false);

expect(pubSubSpy).toHaveBeenCalledWith(`onBeforeToggleTreeCollapse`, { collapsing: false });
expect(pubSubSpy).toHaveBeenCalledWith(`onToggleTreeCollapsed`, { collapsing: false });
expect(dataGetItemsSpy).toHaveBeenCalled();
expect(dataSetItemsSpy).toHaveBeenCalledWith([
{ __collapsed: false, file: 'myFile.txt', size: 0.5, },
Expand Down
53 changes: 42 additions & 11 deletions packages/common/src/services/filter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ export class FilterService {
if (this._onSearchChange) {
const onSearchChangeHandler = this._onSearchChange;
(this._eventHandler as SlickEventHandler<GetSlickEventType<typeof onSearchChangeHandler>>).subscribe(this._onSearchChange, (_e, args) => {
const isClearFilterEvent = args?.clearFilterTriggered ?? false;

// emit an onBeforeFilterChange event when it's not called by a clear filter
if (!isClearFilterEvent) {
this.emitFilterChanged(EmitterType.local, true);
}

// When using Tree Data, we need to do it in 2 steps
// step 1. we need to prefilter (search) the data prior, the result will be an array of IDs which are the node(s) and their parent nodes when necessary.
// step 2. calling the DataView.refresh() is what triggers the final filtering, with "customLocalFilter()" which will decide which rows should persist
Expand All @@ -213,7 +220,7 @@ export class FilterService {
}

// emit an onFilterChanged event when it's not called by a clear filter
if (args && !args.clearFilterTriggered) {
if (!isClearFilterEvent) {
this.emitFilterChanged(EmitterType.local);
}
});
Expand All @@ -227,6 +234,11 @@ export class FilterService {
}

clearFilterByColumnId(event: Event, columnId: number | string) {
this.pubSubService.publish('onBeforeFilterClear', { columnId });

const isBackendApi = this._gridOptions?.backendServiceApi ?? false;
const emitter = isBackendApi ? EmitterType.remote : EmitterType.local;

// get current column filter before clearing, this allow us to know if the filter was empty prior to calling the clear filter
const currentFilterColumnIds = Object.keys(this._columnFilters);
let currentColFilter: string | undefined;
Expand All @@ -240,12 +252,8 @@ export class FilterService {
colFilter.clear(true);
}

let emitter: EmitterType = EmitterType.local;
const isBackendApi = this._gridOptions?.backendServiceApi ?? false;

// when using a backend service, we need to manually trigger a filter change but only if the filter was previously filled
if (isBackendApi) {
emitter = EmitterType.remote;
if (currentColFilter !== undefined) {
this.onBackendFilterChange(event as KeyboardEvent, { grid: this._grid, columnFilters: this._columnFilters });
}
Expand All @@ -257,6 +265,11 @@ export class FilterService {

/** Clear the search filters (below the column titles) */
clearFilters(triggerChange = true) {
// emit an event before the process start
if (triggerChange) {
this.pubSubService.publish('onBeforeFilterClear', true);
}

this._filtersMetadata.forEach((filter: Filter) => {
if (filter?.clear) {
// clear element but don't trigger individual clear change,
Expand Down Expand Up @@ -618,20 +631,28 @@ export class FilterService {
* Other services, like Pagination, can then subscribe to it.
* @param caller
*/
emitFilterChanged(caller: EmitterType) {
emitFilterChanged(caller: EmitterType, isBeforeExecution = false) {
const eventName = isBeforeExecution ? 'onBeforeFilterChange' : 'onFilterChanged';

if (caller === EmitterType.remote && this._gridOptions?.backendServiceApi) {
let currentFilters: CurrentFilter[] = [];
const backendService = this._gridOptions.backendServiceApi.service;
if (backendService?.getCurrentFilters) {
currentFilters = backendService.getCurrentFilters() as CurrentFilter[];
}
this.pubSubService.publish('onFilterChanged', currentFilters);
this.pubSubService.publish(eventName, currentFilters);
} else if (caller === EmitterType.local) {
this.pubSubService.publish('onFilterChanged', this.getCurrentLocalFilters());
this.pubSubService.publish(eventName, this.getCurrentLocalFilters());
}
}

async onBackendFilterChange(event: KeyboardEvent, args: any) {
const isTriggeringQueryEvent = args?.shouldTriggerQuery;

if (isTriggeringQueryEvent) {
this.emitFilterChanged(EmitterType.remote, true);
}

if (!args || !args.grid) {
throw new Error('Something went wrong when trying to bind the "onBackendFilterChange(event, args)" function, it seems that "args" is not populated correctly');
}
Expand All @@ -651,7 +672,7 @@ export class FilterService {
}

// query backend, except when it's called by a ClearFilters then we won't
if (args?.shouldTriggerQuery) {
if (isTriggeringQueryEvent) {
const query = await backendApi.service.processOnFilterChanged(event, args);
const totalItems = this._gridOptions?.pagination?.totalItems ?? 0;
this.backendUtilities?.executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitFilterChanged.bind(this), this.httpCancelRequests$);
Expand Down Expand Up @@ -797,6 +818,12 @@ export class FilterService {
});

const backendApi = this._gridOptions?.backendServiceApi;
const emitterType = backendApi ? EmitterType.remote : EmitterType.local;

// trigger the onBeforeFilterChange event before the process
if (emitChangedEvent) {
this.emitFilterChanged(emitterType, true);
}

// refresh the DataView and trigger an event after all filters were updated and rendered
this._dataView.refresh();
Expand All @@ -812,7 +839,6 @@ export class FilterService {
}

if (emitChangedEvent) {
const emitterType = backendApi ? EmitterType.remote : EmitterType.local;
this.emitFilterChanged(emitterType);
}
}
Expand Down Expand Up @@ -848,6 +874,12 @@ export class FilterService {
}

const backendApi = this._gridOptions?.backendServiceApi;
const emitterType = backendApi ? EmitterType.remote : EmitterType.local;

// trigger the onBeforeFilterChange event before the process
if (emitChangedEvent) {
this.emitFilterChanged(emitterType, true);
}

if (backendApi) {
const backendApiService = backendApi?.service;
Expand All @@ -870,7 +902,6 @@ export class FilterService {
}

if (emitChangedEvent) {
const emitterType = backendApi ? EmitterType.remote : EmitterType.local;
this.emitFilterChanged(emitterType);
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/services/sort.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export class SortService {
const datasetIdPropertyName = this._gridOptions?.datasetIdPropertyName ?? 'id';
const isTreeDataEnabled = this._gridOptions?.enableTreeData ?? false;
const dataView = grid.getData?.() as SlickDataView;
this.pubSubService.publish('onBeforeSortChange', { sortColumns });

if (grid && dataView) {
if (forceReSort && !isTreeDataEnabled) {
Expand Down
Loading

0 comments on commit 360c62c

Please sign in to comment.