Skip to content

Commit 20564a3

Browse files
authored
feat(filters): add updateSingleFilter for a single external filter (#265)
1 parent c234013 commit 20564a3

6 files changed

Lines changed: 168 additions & 23 deletions

File tree

examples/webpack-demo-vanilla-bundle/src/examples/example08.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
Column,
33
GridOption,
44
FieldType,
5-
FilterCallbackArg,
65
OperatorString,
76
} from '@slickgrid-universal/common';
87
import { ExcelExportService } from '@slickgrid-universal/excel-export';
@@ -103,6 +102,7 @@ export class Example08 {
103102
explicitInitialization: true,
104103
frozenColumn: 2,
105104
rowHeight: 33,
105+
showCustomFooter: true,
106106
gridMenu: { hideClearFrozenColumnsCommand: false },
107107
headerMenu: { hideFreezeColumnsCommand: false },
108108

@@ -184,7 +184,7 @@ export class Example08 {
184184
selectOption.label = columnDef.name;
185185
columnSelect.appendChild(selectOption);
186186
}
187-
this.grid2SearchSelectedColumn = this.columnDefinitions2[0];
187+
this.grid2SearchSelectedColumn = this.columnDefinitions2.find(col => col.id === 'title');
188188
}
189189

190190
populategrid2SearchOperatorDropdown() {
@@ -215,25 +215,10 @@ export class Example08 {
215215
}
216216

217217
updateFilter() {
218-
const columnId = this.grid2SearchSelectedColumn?.id;
219-
const filter = {};
220-
const filterArg: FilterCallbackArg = {
221-
columnDef: this.grid2SearchSelectedColumn,
222-
operator: this.grid2SelectedOperator as OperatorString, // or fix one yourself like '='
218+
this.sgb2.filterService.updateSingleFilter({
219+
columnId: `${this.grid2SearchSelectedColumn?.id ?? ''}`,
220+
operator: this.grid2SelectedOperator as OperatorString,
223221
searchTerms: [this.grid2SearchValue || '']
224-
};
225-
if (this.grid2SearchValue) {
226-
// pass a columnFilter object as an object which it's property name must be a column field name (e.g.: 'duration': {...} )
227-
filter[columnId] = filterArg;
228-
}
229-
230-
// const currentFilter = { columnId, operator: this.grid2SelectedOperator as OperatorString, searchTerms: this.grid2SearchValue || '' } as CurrentFilter;
231-
// this.sgb2.filterService.updateSingleFilter(currentFilter);
232-
this.sgb2.dataView.setFilterArgs({
233-
columnFilters: filter,
234-
grid: this.sgb2.slickGrid
235222
});
236-
this.sgb2.dataView.refresh();
237-
this.sgb2.slickGrid.invalidate();
238223
}
239224
}

packages/common/src/services/__tests__/filter.service.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,96 @@ describe('FilterService', () => {
11591159
});
11601160
});
11611161

1162+
describe('updateSingleFilter method', () => {
1163+
let mockColumn1: Column;
1164+
let mockColumn2: Column;
1165+
let mockArgs1;
1166+
let mockArgs2;
1167+
1168+
beforeEach(() => {
1169+
gridOptionMock.enableFiltering = true;
1170+
gridOptionMock.backendServiceApi = undefined;
1171+
mockColumn1 = { id: 'firstName', name: 'firstName', field: 'firstName', };
1172+
mockColumn2 = { id: 'isActive', name: 'isActive', field: 'isActive', type: FieldType.boolean, };
1173+
mockArgs1 = { grid: gridStub, column: mockColumn1, node: document.getElementById(DOM_ELEMENT_ID) };
1174+
mockArgs2 = { grid: gridStub, column: mockColumn2, node: document.getElementById(DOM_ELEMENT_ID) };
1175+
sharedService.allColumns = [mockColumn1, mockColumn2];
1176+
});
1177+
1178+
it('should call "updateSingleFilter" method and expect event "emitFilterChanged" to be trigged local when using "bindLocalOnFilter" and also expect filters to be set in dataview', () => {
1179+
const expectation = {
1180+
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
1181+
};
1182+
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
1183+
const setFilterArgsSpy = jest.spyOn(dataViewStub, 'setFilterArgs');
1184+
const refreshSpy = jest.spyOn(dataViewStub, 'refresh');
1185+
service.init(gridStub);
1186+
service.bindLocalOnFilter(gridStub);
1187+
gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
1188+
gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
1189+
service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' });
1190+
1191+
expect(setFilterArgsSpy).toHaveBeenCalledWith({ columnFilters: expectation, grid: gridStub });
1192+
expect(refreshSpy).toHaveBeenCalled();
1193+
expect(emitSpy).toHaveBeenCalledWith('local');
1194+
expect(service.getColumnFilters()).toEqual({
1195+
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
1196+
});
1197+
});
1198+
1199+
it('should call "updateSingleFilter" method and expect event "emitFilterChanged" to be trigged local when using "bindBackendOnFilter" and also expect filters to be set in dataview', () => {
1200+
const expectation = {
1201+
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
1202+
};
1203+
gridOptionMock.backendServiceApi = {
1204+
filterTypingDebounce: 0,
1205+
service: backendServiceStub,
1206+
process: () => new Promise((resolve) => resolve(jest.fn())),
1207+
};
1208+
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
1209+
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
1210+
const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
1211+
1212+
service.init(gridStub);
1213+
service.bindBackendOnFilter(gridStub);
1214+
gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
1215+
gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
1216+
service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' });
1217+
1218+
expect(emitSpy).toHaveBeenCalledWith('remote');
1219+
expect(backendProcessSpy).not.toHaveBeenCalled();
1220+
expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true);
1221+
expect(service.getColumnFilters()).toEqual(expectation);
1222+
expect(mockRefreshBackendDataset).toHaveBeenCalledWith(gridOptionMock);
1223+
});
1224+
1225+
it('should expect filter to be sent to the backend when using "bindBackendOnFilter" without triggering a filter changed event neither a backend query when both flag arguments are set to false', () => {
1226+
const expectation = {
1227+
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
1228+
};
1229+
gridOptionMock.backendServiceApi = {
1230+
filterTypingDebounce: 0,
1231+
service: backendServiceStub,
1232+
process: () => new Promise((resolve) => resolve(jest.fn())),
1233+
};
1234+
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
1235+
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
1236+
const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
1237+
1238+
service.init(gridStub);
1239+
service.bindBackendOnFilter(gridStub);
1240+
gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
1241+
gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
1242+
service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' }, false, false);
1243+
1244+
expect(backendProcessSpy).not.toHaveBeenCalled();
1245+
expect(emitSpy).not.toHaveBeenCalled();
1246+
expect(mockRefreshBackendDataset).not.toHaveBeenCalled();
1247+
expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true);
1248+
expect(service.getColumnFilters()).toEqual(expectation);
1249+
});
1250+
});
1251+
11621252
describe('disableFilterFunctionality method', () => {
11631253
beforeEach(() => {
11641254
gridOptionMock.enableFiltering = true;

packages/common/src/services/__tests__/groupingAndColspan.service.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,9 @@ describe('GroupingAndColspanService', () => {
200200
jest.runAllTimers(); // fast-forward timer
201201

202202
expect(spy).toHaveBeenCalledTimes(2);
203-
expect(setTimeout).toHaveBeenCalledTimes(1);
204-
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 75);
203+
expect(setTimeout).toHaveBeenCalledTimes(2);
204+
expect(setTimeout).toHaveBeenNthCalledWith(1, expect.any(Function), 75);
205+
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 0);
205206
});
206207

207208
it('should call the "renderPreHeaderRowGroupingTitles" after triggering a grid resize', () => {

packages/common/src/services/filter.service.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,55 @@ export class FilterService {
813813
}
814814
}
815815

816+
/**
817+
* Update a Single Filter dynamically just by providing (columnId, operator and searchTerms)
818+
* You can also choose emit (default) a Filter Changed event that will be picked by the Grid State Service.
819+
*
820+
* Also for backend service only, you can choose to trigger a backend query (default) or not if you wish to do it later,
821+
* this could be useful when using updateFilters & updateSorting and you wish to only send the backend query once.
822+
* @param filters array
823+
* @param triggerEvent defaults to True, do we want to emit a filter changed event?
824+
*/
825+
updateSingleFilter(filter: CurrentFilter, emitChangedEvent = true, triggerBackendQuery = true) {
826+
const columnDef = this.sharedService.allColumns.find(col => col.id === filter.columnId);
827+
if (columnDef && filter.columnId) {
828+
this._columnFilters = {};
829+
if (Array.isArray(filter.searchTerms) && (filter.searchTerms.length > 1 || (filter.searchTerms.length === 1 && filter.searchTerms[0] !== ''))) {
830+
// pass a columnFilter object as an object which it's property name must be a column field name (e.g.: 'duration': {...} )
831+
this._columnFilters[filter.columnId] = {
832+
columnId: filter.columnId,
833+
operator: filter.operator,
834+
searchTerms: filter.searchTerms,
835+
columnDef,
836+
type: columnDef.type ?? FieldType.string,
837+
};
838+
}
839+
840+
const backendApi = this._gridOptions && this._gridOptions.backendServiceApi;
841+
842+
if (backendApi) {
843+
const backendApiService = backendApi && backendApi.service;
844+
if (backendApiService && backendApiService.updateFilters) {
845+
backendApiService.updateFilters(this._columnFilters, true);
846+
if (triggerBackendQuery) {
847+
refreshBackendDataset(this._gridOptions);
848+
}
849+
}
850+
} else {
851+
this._dataView.setFilterArgs({
852+
columnFilters: this._columnFilters,
853+
grid: this._grid
854+
});
855+
this._dataView.refresh();
856+
}
857+
858+
if (emitChangedEvent) {
859+
const emitterType = backendApi ? EmitterType.remote : EmitterType.local;
860+
this.emitFilterChanged(emitterType);
861+
}
862+
}
863+
}
864+
816865
// --
817866
// protected functions
818867
// -------------------

packages/common/src/services/groupingAndColspan.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class GroupingAndColspanService {
6565
this._eventHandler.subscribe(grid.onSort, () => this.renderPreHeaderRowGroupingTitles());
6666
this._eventHandler.subscribe(grid.onColumnsResized, () => this.renderPreHeaderRowGroupingTitles());
6767
this._eventHandler.subscribe(grid.onColumnsReordered, () => this.renderPreHeaderRowGroupingTitles());
68-
this._eventHandler.subscribe(this._dataView.onRowCountChanged, () => this.renderPreHeaderRowGroupingTitles());
68+
this._eventHandler.subscribe(this._dataView.onRowCountChanged, () => this.delayRenderPreHeaderRowGroupingTitles(0));
6969

7070
// for both picker (columnPicker/gridMenu) we also need to re-create after hiding/showing columns
7171
const columnPickerExtension = this.extensionService.getExtensionByName<SlickColumnPicker>(ExtensionName.columnPicker);

test/cypress/integration/example08.spec.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
/// <reference types="cypress" />
22

3+
function removeExtraSpaces(textS) {
4+
return `${textS}`.replace(/\s+/g, ' ').trim();
5+
}
6+
37
describe('Example 08 - Column Span & Header Grouping', () => {
48
// NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after clear frozen columns
59
const fullPreTitles = ['', 'Common Factor', 'Period', 'Analysis'];
@@ -135,6 +139,14 @@ describe('Example 08 - Column Span & Header Grouping', () => {
135139
cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', 'Task 25');
136140
cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(1)`).should('contain', 'Task 35');
137141
cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(1)`).should('contain', 'Task 45');
142+
143+
cy.get('.grid2')
144+
.find('.slick-custom-footer')
145+
.find('.right-footer')
146+
.should($span => {
147+
const text = removeExtraSpaces($span.text()); // remove all white spaces
148+
expect(text).to.eq(`50 of 500 items`);
149+
});
138150
});
139151

140152
it('should search for "% Complete" below 50 and expect rows to be that', () => {
@@ -180,5 +192,13 @@ describe('Example 08 - Column Span & Header Grouping', () => {
180192
cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('contain', 'Task 2');
181193
cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(1)`).should('contain', 'Task 3');
182194
cy.get(`.grid2 .grid-canvas-left > [style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(1)`).should('contain', 'Task 4');
195+
196+
cy.get('.grid2')
197+
.find('.slick-custom-footer')
198+
.find('.right-footer')
199+
.should($span => {
200+
const text = removeExtraSpaces($span.text()); // remove all white spaces
201+
expect(text).to.eq(`500 of 500 items`);
202+
});
183203
});
184204
});

0 commit comments

Comments
 (0)