Skip to content

Commit 677beb4

Browse files
authored
feat(filters): add updateSingleFilter for a single external filter (#699)
1 parent c89db7e commit 677beb4

File tree

5 files changed

+202
-24
lines changed

5 files changed

+202
-24
lines changed

src/app/examples/grid-autoheight.component.html

+9-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ <h2>{{title}}</h2>
1515
<option [ngValue]="operator" *ngFor="let operator of operatorList">{{operator}}</option>
1616
</select>
1717

18-
<input type="text" class="form-control" data-test="search-value-input" name="searchValue"
19-
placeholder="search value" autocomplete="off" (input)="updateFilter()" [(ngModel)]="searchValue">
18+
<div class="input-group">
19+
<input type="text" class="form-control" data-test="search-value-input" name="searchValue"
20+
placeholder="search value" autocomplete="off" (input)="updateFilter()" [(ngModel)]="searchValue">
21+
<div class="input-group-btn">
22+
<button class="btn btn-default" data-test="clear-search-value" (click)="cleargridSearchInput()">
23+
<span class="icon fa fa-times"></span>
24+
</button>
25+
</div>
26+
</div>
2027
</div>
2128
</form>
2229

src/app/examples/grid-autoheight.component.ts

+11-22
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
AngularGridInstance,
44
Column,
55
FieldType,
6-
FilterCallbackArg,
76
Formatters,
87
GridOption,
98
OperatorString,
@@ -32,7 +31,7 @@ export class GridAutoHeightComponent implements OnInit {
3231
columnDefinitions: Column[];
3332
gridOptions: GridOption;
3433
dataset: any[];
35-
operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>'];
34+
operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith'];
3635
selectedOperator = '=';
3736
searchValue = '';
3837
selectedColumn: Column;
@@ -132,26 +131,16 @@ export class GridAutoHeightComponent implements OnInit {
132131
// -- if any of the Search form input changes, we'll call the updateFilter() method
133132
//
134133

135-
updateFilter() {
136-
if (this.selectedColumn && this.selectedOperator) {
137-
const fieldName = this.selectedColumn.field;
138-
const filter = {};
139-
const filterArg: FilterCallbackArg = {
140-
columnDef: this.selectedColumn,
141-
operator: this.selectedOperator as OperatorString, // or fix one yourself like '='
142-
searchTerms: [this.searchValue || '']
143-
};
144-
145-
if (this.searchValue) {
146-
// pass a columnFilter object as an object which it's property name must be a column field name (e.g.: 'duration': {...} )
147-
filter[fieldName] = filterArg;
148-
}
134+
cleargridSearchInput() {
135+
this.searchValue = '';
136+
this.updateFilter();
137+
}
149138

150-
this.angularGrid.dataView.setFilterArgs({
151-
columnFilters: filter,
152-
grid: this.angularGrid.slickGrid
153-
});
154-
this.angularGrid.dataView.refresh();
155-
}
139+
updateFilter() {
140+
this.angularGrid.filterService.updateSingleFilter({
141+
columnId: `${this.selectedColumn.id || ''}`,
142+
operator: this.selectedOperator as OperatorString,
143+
searchTerms: [this.searchValue || '']
144+
});
156145
}
157146
}

src/app/modules/angular-slickgrid/services/__tests__/filter.service.spec.ts

+90
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,96 @@ describe('FilterService', () => {
11841184
});
11851185
});
11861186

1187+
describe('updateSingleFilter method', () => {
1188+
let mockColumn1: Column;
1189+
let mockColumn2: Column;
1190+
let mockArgs1;
1191+
let mockArgs2;
1192+
1193+
beforeEach(() => {
1194+
gridOptionMock.enableFiltering = true;
1195+
gridOptionMock.backendServiceApi = undefined;
1196+
mockColumn1 = { id: 'firstName', name: 'firstName', field: 'firstName', };
1197+
mockColumn2 = { id: 'isActive', name: 'isActive', field: 'isActive', type: FieldType.boolean, };
1198+
mockArgs1 = { grid: gridStub, column: mockColumn1, node: document.getElementById(DOM_ELEMENT_ID) };
1199+
mockArgs2 = { grid: gridStub, column: mockColumn2, node: document.getElementById(DOM_ELEMENT_ID) };
1200+
sharedService.allColumns = [mockColumn1, mockColumn2];
1201+
});
1202+
1203+
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', () => {
1204+
const expectation = {
1205+
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
1206+
};
1207+
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
1208+
const setFilterArgsSpy = jest.spyOn(dataViewStub, 'setFilterArgs');
1209+
const refreshSpy = jest.spyOn(dataViewStub, 'refresh');
1210+
service.init(gridStub);
1211+
service.bindLocalOnFilter(gridStub);
1212+
gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
1213+
gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
1214+
service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' });
1215+
1216+
expect(setFilterArgsSpy).toHaveBeenCalledWith({ columnFilters: expectation, grid: gridStub });
1217+
expect(refreshSpy).toHaveBeenCalled();
1218+
expect(emitSpy).toHaveBeenCalledWith('local');
1219+
expect(service.getColumnFilters()).toEqual({
1220+
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
1221+
});
1222+
});
1223+
1224+
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', () => {
1225+
const expectation = {
1226+
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
1227+
};
1228+
gridOptionMock.backendServiceApi = {
1229+
filterTypingDebounce: 0,
1230+
service: backendServiceStub,
1231+
process: () => new Promise((resolve) => resolve(jest.fn())),
1232+
};
1233+
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
1234+
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
1235+
const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
1236+
1237+
service.init(gridStub);
1238+
service.bindBackendOnFilter(gridStub);
1239+
gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
1240+
gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
1241+
service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' });
1242+
1243+
expect(emitSpy).toHaveBeenCalledWith('remote');
1244+
expect(backendProcessSpy).not.toHaveBeenCalled();
1245+
expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true);
1246+
expect(service.getColumnFilters()).toEqual(expectation);
1247+
expect(mockRefreshBackendDataset).toHaveBeenCalledWith(gridOptionMock);
1248+
});
1249+
1250+
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', () => {
1251+
const expectation = {
1252+
firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
1253+
};
1254+
gridOptionMock.backendServiceApi = {
1255+
filterTypingDebounce: 0,
1256+
service: backendServiceStub,
1257+
process: () => new Promise((resolve) => resolve(jest.fn())),
1258+
};
1259+
const emitSpy = jest.spyOn(service, 'emitFilterChanged');
1260+
const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
1261+
const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
1262+
1263+
service.init(gridStub);
1264+
service.bindBackendOnFilter(gridStub);
1265+
gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
1266+
gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
1267+
service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' }, false, false);
1268+
1269+
expect(backendProcessSpy).not.toHaveBeenCalled();
1270+
expect(emitSpy).not.toHaveBeenCalled();
1271+
expect(mockRefreshBackendDataset).not.toHaveBeenCalled();
1272+
expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true);
1273+
expect(service.getColumnFilters()).toEqual(expectation);
1274+
});
1275+
});
1276+
11871277
describe('disableFilterFunctionality method', () => {
11881278
beforeEach(() => {
11891279
gridOptionMock.enableFiltering = true;

src/app/modules/angular-slickgrid/services/filter.service.ts

+49
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,55 @@ export class FilterService {
832832
}
833833
}
834834

835+
/**
836+
* Update a Single Filter dynamically just by providing (columnId, operator and searchTerms)
837+
* You can also choose emit (default) a Filter Changed event that will be picked by the Grid State Service.
838+
*
839+
* Also for backend service only, you can choose to trigger a backend query (default) or not if you wish to do it later,
840+
* this could be useful when using updateFilters & updateSorting and you wish to only send the backend query once.
841+
* @param filters array
842+
* @param triggerEvent defaults to True, do we want to emit a filter changed event?
843+
*/
844+
updateSingleFilter(filter: CurrentFilter, emitChangedEvent = true, triggerBackendQuery = true) {
845+
const columnDef = this.sharedService.allColumns.find(col => col.id === filter.columnId);
846+
if (columnDef && filter.columnId) {
847+
this._columnFilters = {};
848+
if (Array.isArray(filter.searchTerms) && (filter.searchTerms.length > 1 || (filter.searchTerms.length === 1 && filter.searchTerms[0] !== ''))) {
849+
// pass a columnFilter object as an object which it's property name must be a column field name (e.g.: 'duration': {...} )
850+
this._columnFilters[filter.columnId] = {
851+
columnId: filter.columnId,
852+
operator: filter.operator,
853+
searchTerms: filter.searchTerms,
854+
columnDef,
855+
type: columnDef.type || FieldType.string,
856+
};
857+
}
858+
859+
const backendApi = this._gridOptions && this._gridOptions.backendServiceApi;
860+
861+
if (backendApi) {
862+
const backendApiService = backendApi && backendApi.service;
863+
if (backendApiService && backendApiService.updateFilters) {
864+
backendApiService.updateFilters(this._columnFilters, true);
865+
if (triggerBackendQuery) {
866+
refreshBackendDataset(this._gridOptions);
867+
}
868+
}
869+
} else {
870+
this._dataView.setFilterArgs({
871+
columnFilters: this._columnFilters,
872+
grid: this._grid
873+
});
874+
this._dataView.refresh();
875+
}
876+
877+
if (emitChangedEvent) {
878+
const emitterType = backendApi ? EmitterType.remote : EmitterType.local;
879+
this.emitFilterChanged(emitterType);
880+
}
881+
}
882+
}
883+
835884
// --
836885
// protected functions
837886
// -------------------

test/cypress/integration/example23.spec.js

+43
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/// <reference types="cypress" />
22

3+
function removeExtraSpaces(textS) {
4+
return `${textS}`.replace(/\s+/g, ' ').trim();
5+
}
6+
37
describe('Example 23 - Grid AutoHeight', () => {
48
const fullTitles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
9+
const GRID_ROW_HEIGHT = 35;
510

611
it('should display Example title', () => {
712
cy.visit(`${Cypress.config('baseExampleUrl')}/autoheight`);
@@ -50,4 +55,42 @@ describe('Example 23 - Grid AutoHeight', () => {
5055
expect(+$child.text()).to.be.lt(50);
5156
});
5257
});
58+
59+
it('should search for Title ending with text "5" expect rows to be (Task 5, 15, 25, ...)', () => {
60+
cy.get('[data-test="clear-search-value"]')
61+
.click();
62+
63+
cy.get('[data-test="search-column-list"]')
64+
.select('Title');
65+
66+
cy.get('[data-test="search-operator-list"]')
67+
.select('EndsWith');
68+
69+
cy.get('[data-test="search-value-input"]')
70+
.type('5');
71+
72+
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0)`).should('contain', 'Task 5');
73+
cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0)`).should('contain', 'Task 15');
74+
});
75+
76+
it('should type a filter which returns an empty dataset', () => {
77+
cy.get('[data-test="search-value-input"]')
78+
.clear()
79+
.type('zzz');
80+
81+
cy.get('.slick-empty-data-warning:visible')
82+
.contains('No data to display.');
83+
});
84+
85+
it('should clear search input and expect empty dataset warning to go away and also expect data back (Task 0, 1, 2, ...)', () => {
86+
cy.get('[data-test="clear-search-value"]')
87+
.click();
88+
89+
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
90+
cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0)`).should('contain', 'Task 1');
91+
cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0)`).should('contain', 'Task 2');
92+
cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(0)`).should('contain', 'Task 3');
93+
cy.get(`[style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(0)`).should('contain', 'Task 4');
94+
95+
});
5396
});

0 commit comments

Comments
 (0)