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'); + + }); });