Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding-SE committed Jul 9, 2020
2 parents 7d4b701 + 3ba243c commit eb8aee7
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 21 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ npm run test:watch
- [x] Aggregators (6)
- [x] Editors (11)
- [x] Filters (17)
- [ ] Add optional debounce filter delay on local grid
- [x] Formatters (31)
- [ ] Extensions
- [x] AutoTooltip
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/interfaces/slickEventData.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface SlickEventData extends Event {
export interface SlickEventData extends KeyboardEvent, MouseEvent, Event {
/** Stops event from propagating up the DOM tree. */
stopPropagation: () => void;

Expand Down
22 changes: 14 additions & 8 deletions packages/common/src/services/__tests__/filter.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,10 @@ describe('FilterService', () => {
mockArgs = { grid: gridStub, column: mockColumn, node: document.getElementById(DOM_ELEMENT_ID) };
});

it('should execute the callback normally when a keyup event is triggered and searchTerms are defined', () => {
it('should execute the search callback normally when a keyup event is triggered and searchTerms are defined', () => {
const expectationColumnFilter = { columnDef: mockColumn, columnId: 'firstName', operator: 'EQ', searchTerms: ['John'] };
const spySearchChange = jest.spyOn(service.onSearchChange, 'notify');
const spyEmit = jest.spyOn(service, 'emitFilterChanged');

service.init(gridStub);
service.bindLocalOnFilter(gridStub, dataViewStub);
Expand All @@ -316,6 +317,7 @@ describe('FilterService', () => {
searchTerms: ['John'],
grid: gridStub
}, expect.anything());
expect(spyEmit).toHaveBeenCalledWith('local');
});

it('should execute the callback normally when a keyup event is triggered and the searchTerm comes from this event.target', () => {
Expand Down Expand Up @@ -473,7 +475,7 @@ describe('FilterService', () => {
it('should execute the "onError" method when the Promise throws an error', (done) => {
const errorExpected = 'promise error';
gridOptionMock.backendServiceApi.process = () => Promise.reject(errorExpected);
gridOptionMock.backendServiceApi.onError = (e) => jest.fn();
gridOptionMock.backendServiceApi.onError = () => jest.fn();
const pubSubSpy = jest.spyOn(pubSubServiceStub, 'publish');
const spyOnError = jest.spyOn(gridOptionMock.backendServiceApi, 'onError');
jest.spyOn(gridOptionMock.backendServiceApi, 'process');
Expand Down Expand Up @@ -839,8 +841,9 @@ describe('FilterService', () => {
expect(spy).toHaveBeenCalled();
});

it('should execute "processOnFilterChanged" method when "shouldTriggerQuery" is set to True and "debounceTypingDelay" is bigger than 0', (done) => {
gridOptionMock.backendServiceApi.filterTypingDebounce = 1;
it('should execute "processOnFilterChanged" method when "shouldTriggerQuery" is set to True and "debounceTypingDelay" is bigger than 0', () => {
jest.useFakeTimers();
gridOptionMock.backendServiceApi.filterTypingDebounce = 50;
const spy = jest.spyOn(gridOptionMock.backendServiceApi.service, 'processOnFilterChanged').mockReturnValue('backend query');

service.init(gridStub);
Expand All @@ -850,10 +853,13 @@ describe('FilterService', () => {
// @ts-ignore
service.onBackendFilterChange(mockEvent, { grid: gridStub, shouldTriggerQuery: true });

setTimeout(() => {
expect(spy).toHaveBeenCalled();
done();
}, 1);
expect(spy).not.toHaveBeenCalled();

jest.runTimersToTime(49);
expect(spy).not.toHaveBeenCalled();

jest.runTimersToTime(50);
expect(spy).toHaveBeenCalled();
});
});

Expand Down
20 changes: 20 additions & 0 deletions packages/common/src/services/__tests__/utilities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
convertHierarchicalViewToParentChildArray,
convertParentChildArrayToHierarchicalView,
decimalFormatted,
debounce,
deepCopy,
findItemInHierarchicalStructure,
findOrDefault,
Expand Down Expand Up @@ -340,6 +341,25 @@ describe('Service/Utilies', () => {
});
});

describe('debounce method', () => {
it('should execute a function after a given waiting time', () => {
jest.useFakeTimers();

const func = jest.fn();
const debouncedFunction = debounce(func, 100);

debouncedFunction();
expect(func).not.toBeCalled();

jest.runTimersToTime(50);
expect(func).not.toBeCalled();

jest.runTimersToTime(100);
expect(func).toBeCalled();
expect(func.mock.calls.length).toBe(1);
});
});

describe('deepCopy method', () => {
it('should return original input when it is not an object neither an array', () => {
const msg = 'hello world';
Expand Down
31 changes: 20 additions & 11 deletions packages/common/src/services/filter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,26 @@ import {
SlickNamespace,
} from './../interfaces/index';
import { executeBackendCallback, refreshBackendDataset } from './backend-utilities';
import { getDescendantProperty } from './utilities';
import { debounce, getDescendantProperty } from './utilities';
import { PubSubService } from '../services/pubSub.service';
import { SharedService } from './shared.service';

// using external non-typed js libraries
declare const Slick: SlickNamespace;

// timer for keeping track of user typing waits
let timer: any;
const DEFAULT_FILTER_TYPING_DEBOUNCE = 500;
const DEFAULT_BACKEND_FILTER_TYPING_DEBOUNCE = 500;

interface OnSearchChangeEvent {
clearFilterTriggered?: boolean;
shouldTriggerQuery?: boolean;
columnId: string | number;
columnDef: Column;
columnFilters: ColumnFilters;
operator: OperatorType | OperatorString | undefined;
searchTerms: any[] | undefined;
grid: SlickGrid;
}

export class FilterService {
private _eventHandler: SlickEventHandler;
Expand All @@ -50,7 +60,7 @@ export class FilterService {
private _columnFilters: ColumnFilters = {};
private _dataView: SlickDataView;
private _grid: SlickGrid;
private _onSearchChange: SlickEvent;
private _onSearchChange: SlickEvent<OnSearchChangeEvent>;
private _tmpPreFilteredData: number[];

constructor(private filterFactory: FilterFactory, private pubSubService: PubSubService, private sharedService: SharedService) {
Expand Down Expand Up @@ -176,7 +186,7 @@ export class FilterService {

// bind any search filter change (e.g. input filter keyup event)
const onSearchChangeHandler = this._onSearchChange;
(this._eventHandler as SlickEventHandler<GetSlickEventType<typeof onSearchChangeHandler>>).subscribe(this._onSearchChange, (e, args) => {
(this._eventHandler as SlickEventHandler<GetSlickEventType<typeof onSearchChangeHandler>>).subscribe(this._onSearchChange, (_e, args) => {
const isGridWithTreeData = this._gridOptions?.enableTreeData ?? false;

// When using Tree Data, we need to do it in 2 steps
Expand Down Expand Up @@ -562,19 +572,18 @@ export class FilterService {
const eventType = event && event.type;
const eventKeyCode = event && event.keyCode;
if (!isTriggeredByClearFilter && eventKeyCode !== KeyCode.ENTER && (eventType === 'input' || eventType === 'keyup' || eventType === 'keydown')) {
debounceTypingDelay = backendApi.hasOwnProperty('filterTypingDebounce') ? backendApi.filterTypingDebounce as number : DEFAULT_FILTER_TYPING_DEBOUNCE;
debounceTypingDelay = backendApi?.filterTypingDebounce ?? DEFAULT_BACKEND_FILTER_TYPING_DEBOUNCE;
}

// query backend, except when it's called by a ClearFilters then we won't
if (args && args.shouldTriggerQuery) {
// call the service to get a query back
if (debounceTypingDelay > 0) {
clearTimeout(timer);
timer = setTimeout(() => {
debounce(() => {
const query = backendApi.service.processOnFilterChanged(event, args);
const totalItems = this._gridOptions && this._gridOptions.pagination && this._gridOptions.pagination.totalItems || 0;
executeBackendCallback(backendApi, query, args, startTime, totalItems, this.emitFilterChanged.bind(this));
}, debounceTypingDelay);
}, debounceTypingDelay)();
} else {
const query = backendApi.service.processOnFilterChanged(event, args);
const totalItems = this._gridOptions && this._gridOptions.pagination && this._gridOptions.pagination.totalItems || 0;
Expand Down Expand Up @@ -655,7 +664,7 @@ export class FilterService {
uiFilter.setValues(newFilter.searchTerms, newOperator);

if (triggerOnSearchChangeEvent) {
this.callbackSearchEvent(null, { columnDef: uiFilter.columnDef, operator: newOperator, searchTerms: newFilter.searchTerms, shouldTriggerQuery: true });
this.callbackSearchEvent(undefined, { columnDef: uiFilter.columnDef, operator: newOperator, searchTerms: newFilter.searchTerms, shouldTriggerQuery: true });
}
}
});
Expand Down Expand Up @@ -739,7 +748,7 @@ export class FilterService {
* Callback method that is called and executed by the individual Filter (DOM element),
* for example when user type in a word to search (which uses InputFilter), this Filter will execute the callback from a keyup event.
*/
private callbackSearchEvent(event: any, args: FilterCallbackArg) {
private callbackSearchEvent(event: SlickEventData | undefined, args: FilterCallbackArg) {
if (args) {
const searchTerm = ((event && event.target) ? (event.target as HTMLInputElement).value : undefined);
const searchTerms = (args.searchTerms && Array.isArray(args.searchTerms)) ? args.searchTerms : (searchTerm ? [searchTerm] : undefined);
Expand Down
16 changes: 16 additions & 0 deletions packages/common/src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,22 @@ export function convertHierarchicalViewToParentChildArrayByReference<T = any>(hi
}
}

/**
* Debounce create a new function g, which when called will delay the invocation of the original function f until n milliseconds after it was last called.
* @param func: function to debounce
* @param waitFor: waiting time in milliseconds
*/
export function debounce<F extends ((...args: any[]) => any | void)>(func: F, waitFor: number) {
const timeout = 0;

const debounced = (...args: any) => {
clearTimeout(timeout);
setTimeout(() => func(...args), waitFor);
};

return debounced as (...args: Parameters<F>) => ReturnType<F>;
}

/**
* Create an immutable clone of an array or object
* (c) 2019 Chris Ferdinandi, MIT License, https://gomakethings.com
Expand Down

0 comments on commit eb8aee7

Please sign in to comment.