Skip to content
This repository was archived by the owner on Jun 1, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');

});
});