Skip to content

Commit

Permalink
feat(filters): add updateSingleFilter for a single external filter (#699
Browse files Browse the repository at this point in the history
)
  • Loading branch information
ghiscoding committed Feb 16, 2021
1 parent c89db7e commit 677beb4
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 24 deletions.
11 changes: 9 additions & 2 deletions src/app/examples/grid-autoheight.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@ <h2>{{title}}</h2>
<option [ngValue]="operator" *ngFor="let operator of operatorList">{{operator}}</option>
</select>

<input type="text" class="form-control" data-test="search-value-input" name="searchValue"
placeholder="search value" autocomplete="off" (input)="updateFilter()" [(ngModel)]="searchValue">
<div class="input-group">
<input type="text" class="form-control" data-test="search-value-input" name="searchValue"
placeholder="search value" autocomplete="off" (input)="updateFilter()" [(ngModel)]="searchValue">
<div class="input-group-btn">
<button class="btn btn-default" data-test="clear-search-value" (click)="cleargridSearchInput()">
<span class="icon fa fa-times"></span>
</button>
</div>
</div>
</div>
</form>

Expand Down
33 changes: 11 additions & 22 deletions src/app/examples/grid-autoheight.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
AngularGridInstance,
Column,
FieldType,
FilterCallbackArg,
Formatters,
GridOption,
OperatorString,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 || '']
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
49 changes: 49 additions & 0 deletions src/app/modules/angular-slickgrid/services/filter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// -------------------
Expand Down
43 changes: 43 additions & 0 deletions test/cypress/integration/example23.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/// <reference types="cypress" />

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`);
Expand Down Expand Up @@ -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');

});
});

0 comments on commit 677beb4

Please sign in to comment.