Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(filter): refactor Filter Service by adding a debounce fn #7

Merged
merged 4 commits into from
Jul 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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