diff --git a/src/app/examples/grid-autoheight.component.html b/src/app/examples/grid-autoheight.component.html
index ea20ec73c..8e66a0ae4 100644
--- a/src/app/examples/grid-autoheight.component.html
+++ b/src/app/examples/grid-autoheight.component.html
@@ -15,8 +15,15 @@
{{title}}
-
+
diff --git a/src/app/examples/grid-autoheight.component.ts b/src/app/examples/grid-autoheight.component.ts
index e899d4dda..d3bc70cf4 100644
--- a/src/app/examples/grid-autoheight.component.ts
+++ b/src/app/examples/grid-autoheight.component.ts
@@ -3,7 +3,6 @@ import {
AngularGridInstance,
Column,
FieldType,
- FilterCallbackArg,
Formatters,
GridOption,
OperatorString,
@@ -32,7 +31,7 @@ export class GridAutoHeightComponent implements OnInit {
columnDefinitions: Column[];
gridOptions: GridOption;
dataset: any[];
- operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>'];
+ operatorList: OperatorString[] = ['=', '<', '<=', '>', '>=', '<>', 'StartsWith', 'EndsWith'];
selectedOperator = '=';
searchValue = '';
selectedColumn: Column;
@@ -132,26 +131,16 @@ export class GridAutoHeightComponent implements OnInit {
// -- if any of the Search form input changes, we'll call the updateFilter() method
//
- updateFilter() {
- if (this.selectedColumn && this.selectedOperator) {
- const fieldName = this.selectedColumn.field;
- const filter = {};
- const filterArg: FilterCallbackArg = {
- columnDef: this.selectedColumn,
- operator: this.selectedOperator as OperatorString, // or fix one yourself like '='
- searchTerms: [this.searchValue || '']
- };
-
- if (this.searchValue) {
- // pass a columnFilter object as an object which it's property name must be a column field name (e.g.: 'duration': {...} )
- filter[fieldName] = filterArg;
- }
+ cleargridSearchInput() {
+ this.searchValue = '';
+ this.updateFilter();
+ }
- this.angularGrid.dataView.setFilterArgs({
- columnFilters: filter,
- grid: this.angularGrid.slickGrid
- });
- this.angularGrid.dataView.refresh();
- }
+ updateFilter() {
+ this.angularGrid.filterService.updateSingleFilter({
+ columnId: `${this.selectedColumn.id || ''}`,
+ operator: this.selectedOperator as OperatorString,
+ searchTerms: [this.searchValue || '']
+ });
}
}
diff --git a/src/app/modules/angular-slickgrid/services/__tests__/filter.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/filter.service.spec.ts
index 83a6f0440..edd53f862 100644
--- a/src/app/modules/angular-slickgrid/services/__tests__/filter.service.spec.ts
+++ b/src/app/modules/angular-slickgrid/services/__tests__/filter.service.spec.ts
@@ -1184,6 +1184,96 @@ describe('FilterService', () => {
});
});
+ describe('updateSingleFilter method', () => {
+ let mockColumn1: Column;
+ let mockColumn2: Column;
+ let mockArgs1;
+ let mockArgs2;
+
+ beforeEach(() => {
+ gridOptionMock.enableFiltering = true;
+ gridOptionMock.backendServiceApi = undefined;
+ mockColumn1 = { id: 'firstName', name: 'firstName', field: 'firstName', };
+ mockColumn2 = { id: 'isActive', name: 'isActive', field: 'isActive', type: FieldType.boolean, };
+ mockArgs1 = { grid: gridStub, column: mockColumn1, node: document.getElementById(DOM_ELEMENT_ID) };
+ mockArgs2 = { grid: gridStub, column: mockColumn2, node: document.getElementById(DOM_ELEMENT_ID) };
+ sharedService.allColumns = [mockColumn1, mockColumn2];
+ });
+
+ 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', () => {
+ const expectation = {
+ firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
+ };
+ const emitSpy = jest.spyOn(service, 'emitFilterChanged');
+ const setFilterArgsSpy = jest.spyOn(dataViewStub, 'setFilterArgs');
+ const refreshSpy = jest.spyOn(dataViewStub, 'refresh');
+ service.init(gridStub);
+ service.bindLocalOnFilter(gridStub);
+ gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
+ gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
+ service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' });
+
+ expect(setFilterArgsSpy).toHaveBeenCalledWith({ columnFilters: expectation, grid: gridStub });
+ expect(refreshSpy).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith('local');
+ expect(service.getColumnFilters()).toEqual({
+ firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
+ });
+ });
+
+ 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', () => {
+ const expectation = {
+ firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
+ };
+ gridOptionMock.backendServiceApi = {
+ filterTypingDebounce: 0,
+ service: backendServiceStub,
+ process: () => new Promise((resolve) => resolve(jest.fn())),
+ };
+ const emitSpy = jest.spyOn(service, 'emitFilterChanged');
+ const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
+ const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
+
+ service.init(gridStub);
+ service.bindBackendOnFilter(gridStub);
+ gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
+ gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
+ service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' });
+
+ expect(emitSpy).toHaveBeenCalledWith('remote');
+ expect(backendProcessSpy).not.toHaveBeenCalled();
+ expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true);
+ expect(service.getColumnFilters()).toEqual(expectation);
+ expect(mockRefreshBackendDataset).toHaveBeenCalledWith(gridOptionMock);
+ });
+
+ 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', () => {
+ const expectation = {
+ firstName: { columnId: 'firstName', columnDef: mockColumn1, searchTerms: ['Jane'], operator: 'StartsWith', type: FieldType.string },
+ };
+ gridOptionMock.backendServiceApi = {
+ filterTypingDebounce: 0,
+ service: backendServiceStub,
+ process: () => new Promise((resolve) => resolve(jest.fn())),
+ };
+ const emitSpy = jest.spyOn(service, 'emitFilterChanged');
+ const backendUpdateSpy = jest.spyOn(backendServiceStub, 'updateFilters');
+ const backendProcessSpy = jest.spyOn(backendServiceStub, 'processOnFilterChanged');
+
+ service.init(gridStub);
+ service.bindBackendOnFilter(gridStub);
+ gridStub.onHeaderRowCellRendered.notify(mockArgs1 as any, new Slick.EventData(), gridStub);
+ gridStub.onHeaderRowCellRendered.notify(mockArgs2 as any, new Slick.EventData(), gridStub);
+ service.updateSingleFilter({ columnId: 'firstName', searchTerms: ['Jane'], operator: 'StartsWith' }, false, false);
+
+ expect(backendProcessSpy).not.toHaveBeenCalled();
+ expect(emitSpy).not.toHaveBeenCalled();
+ expect(mockRefreshBackendDataset).not.toHaveBeenCalled();
+ expect(backendUpdateSpy).toHaveBeenCalledWith(expectation, true);
+ expect(service.getColumnFilters()).toEqual(expectation);
+ });
+ });
+
describe('disableFilterFunctionality method', () => {
beforeEach(() => {
gridOptionMock.enableFiltering = true;
diff --git a/src/app/modules/angular-slickgrid/services/filter.service.ts b/src/app/modules/angular-slickgrid/services/filter.service.ts
index f648a03f3..7ec3828e8 100644
--- a/src/app/modules/angular-slickgrid/services/filter.service.ts
+++ b/src/app/modules/angular-slickgrid/services/filter.service.ts
@@ -832,6 +832,55 @@ export class FilterService {
}
}
+ /**
+ * Update a Single Filter dynamically just by providing (columnId, operator and searchTerms)
+ * You can also choose emit (default) a Filter Changed event that will be picked by the Grid State Service.
+ *
+ * Also for backend service only, you can choose to trigger a backend query (default) or not if you wish to do it later,
+ * this could be useful when using updateFilters & updateSorting and you wish to only send the backend query once.
+ * @param filters array
+ * @param triggerEvent defaults to True, do we want to emit a filter changed event?
+ */
+ updateSingleFilter(filter: CurrentFilter, emitChangedEvent = true, triggerBackendQuery = true) {
+ const columnDef = this.sharedService.allColumns.find(col => col.id === filter.columnId);
+ if (columnDef && filter.columnId) {
+ this._columnFilters = {};
+ if (Array.isArray(filter.searchTerms) && (filter.searchTerms.length > 1 || (filter.searchTerms.length === 1 && filter.searchTerms[0] !== ''))) {
+ // pass a columnFilter object as an object which it's property name must be a column field name (e.g.: 'duration': {...} )
+ this._columnFilters[filter.columnId] = {
+ columnId: filter.columnId,
+ operator: filter.operator,
+ searchTerms: filter.searchTerms,
+ columnDef,
+ type: columnDef.type || FieldType.string,
+ };
+ }
+
+ const backendApi = this._gridOptions && this._gridOptions.backendServiceApi;
+
+ if (backendApi) {
+ const backendApiService = backendApi && backendApi.service;
+ if (backendApiService && backendApiService.updateFilters) {
+ backendApiService.updateFilters(this._columnFilters, true);
+ if (triggerBackendQuery) {
+ refreshBackendDataset(this._gridOptions);
+ }
+ }
+ } else {
+ this._dataView.setFilterArgs({
+ columnFilters: this._columnFilters,
+ grid: this._grid
+ });
+ this._dataView.refresh();
+ }
+
+ if (emitChangedEvent) {
+ const emitterType = backendApi ? EmitterType.remote : EmitterType.local;
+ this.emitFilterChanged(emitterType);
+ }
+ }
+ }
+
// --
// protected functions
// -------------------
diff --git a/test/cypress/integration/example23.spec.js b/test/cypress/integration/example23.spec.js
index f232fd961..05fcf4616 100644
--- a/test/cypress/integration/example23.spec.js
+++ b/test/cypress/integration/example23.spec.js
@@ -1,7 +1,12 @@
///
+function removeExtraSpaces(textS) {
+ return `${textS}`.replace(/\s+/g, ' ').trim();
+}
+
describe('Example 23 - Grid AutoHeight', () => {
const fullTitles = ['Title', 'Duration (days)', '% Complete', 'Start', 'Finish', 'Effort Driven'];
+ const GRID_ROW_HEIGHT = 35;
it('should display Example title', () => {
cy.visit(`${Cypress.config('baseExampleUrl')}/autoheight`);
@@ -50,4 +55,42 @@ describe('Example 23 - Grid AutoHeight', () => {
expect(+$child.text()).to.be.lt(50);
});
});
+
+ it('should search for Title ending with text "5" expect rows to be (Task 5, 15, 25, ...)', () => {
+ cy.get('[data-test="clear-search-value"]')
+ .click();
+
+ cy.get('[data-test="search-column-list"]')
+ .select('Title');
+
+ cy.get('[data-test="search-operator-list"]')
+ .select('EndsWith');
+
+ cy.get('[data-test="search-value-input"]')
+ .type('5');
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0)`).should('contain', 'Task 5');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0)`).should('contain', 'Task 15');
+ });
+
+ it('should type a filter which returns an empty dataset', () => {
+ cy.get('[data-test="search-value-input"]')
+ .clear()
+ .type('zzz');
+
+ cy.get('.slick-empty-data-warning:visible')
+ .contains('No data to display.');
+ });
+
+ it('should clear search input and expect empty dataset warning to go away and also expect data back (Task 0, 1, 2, ...)', () => {
+ cy.get('[data-test="clear-search-value"]')
+ .click();
+
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0)`).should('contain', 'Task 1');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0)`).should('contain', 'Task 2');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(0)`).should('contain', 'Task 3');
+ cy.get(`[style="top:${GRID_ROW_HEIGHT * 4}px"] > .slick-cell:nth(0)`).should('contain', 'Task 4');
+
+ });
});