Skip to content

Commit 8204afb

Browse files
authored
Merge pull request #8953 from IgniteUI/astaev/formatFilterStrategy
fix(grid): Add FormattedFilteringStrategy #8009
2 parents 40cd8c6 + dfbb4a0 commit 8204afb

File tree

14 files changed

+356
-81
lines changed

14 files changed

+356
-81
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ All notable changes for each version of this project will be documented in this
1010
- Added support for exporting grouped data.
1111
- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`
1212
- Support for `currency` type columns is added in the grid.
13+
- Added support for filtering based on the formatted cell values using the `FormattedValuesFilteringStrategy` for `IgxGrid`/`IgxHierarchicalGrid` and `TreeGridFormattedValuesFilteringStrategy` for `IgxTreeGrid`.
1314
- `IgxPaginator`
1415
- `paging` and `pagingDone` events are now emitted.
1516
- `IgxInput` now supports `type="file"` and its styling upon all themes.

projects/igniteui-angular/src/lib/data-operations/filtering-strategy.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ export class NoopFilteringStrategy implements IFilteringStrategy {
1919
return this._instance || (this._instance = new NoopFilteringStrategy());
2020
}
2121

22-
public filter(data: any[], expressionsTree: IFilteringExpressionsTree, advancedExpressionsTree?: IFilteringExpressionsTree): any[] {
22+
public filter(data: any[], _: IFilteringExpressionsTree, __?: IFilteringExpressionsTree): any[] {
2323
return data;
2424
}
2525
}
2626

2727
export abstract class BaseFilteringStrategy implements IFilteringStrategy {
28-
public findMatchByExpression(rec: any, expr: IFilteringExpression, isDate?: boolean): boolean {
28+
public findMatchByExpression(rec: any, expr: IFilteringExpression, isDate?: boolean, grid?: GridType): boolean {
2929
const cond = expr.condition;
30-
const val = this.getFieldValue(rec, expr.fieldName, isDate);
30+
const val = this.getFieldValue(rec, expr.fieldName, isDate, grid);
3131
return cond.logic(val, expr.searchVal, expr.ignoreCase);
3232
}
3333

@@ -61,7 +61,7 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy {
6161
const expression = expressions as IFilteringExpression;
6262
const isDate = grid && grid.getColumnByName(expression.fieldName) ?
6363
grid.getColumnByName(expression.fieldName).dataType === DateType : false;
64-
return this.findMatchByExpression(rec, expression, isDate);
64+
return this.findMatchByExpression(rec, expression, isDate, grid);
6565
}
6666
}
6767

@@ -71,7 +71,7 @@ export abstract class BaseFilteringStrategy implements IFilteringStrategy {
7171
public abstract filter(data: any[], expressionsTree: IFilteringExpressionsTree,
7272
advancedExpressionsTree?: IFilteringExpressionsTree, grid?: GridType): any[];
7373

74-
protected abstract getFieldValue(rec: any, fieldName: string, isDate: boolean): any;
74+
protected abstract getFieldValue(rec: any, fieldName: string, isDate?: boolean, grid?: GridType): any;
7575
}
7676

7777
export class FilteringStrategy extends BaseFilteringStrategy {
@@ -91,6 +91,7 @@ export class FilteringStrategy extends BaseFilteringStrategy {
9191
let rec;
9292
const len = data.length;
9393
const res: T[] = [];
94+
9495
if ((FilteringExpressionsTree.empty(expressionsTree) && FilteringExpressionsTree.empty(advancedExpressionsTree)) || !len) {
9596
return data;
9697
}
@@ -109,3 +110,30 @@ export class FilteringStrategy extends BaseFilteringStrategy {
109110
return value;
110111
}
111112
}
113+
export class FormattedValuesFilteringStrategy extends FilteringStrategy {
114+
/**
115+
* Creates a new instance of FormattedValuesFilteringStrategy.
116+
*
117+
* @param fields An array of column field names that should be formatted.
118+
* If omitted the values of all columns which has formatter will be formatted.
119+
*/
120+
constructor(private fields?: string[]) {
121+
super();
122+
}
123+
124+
/** @hidden */
125+
public shouldApplyFormatter(fieldName: string): boolean {
126+
return !this.fields || this.fields.length === 0 || this.fields.some(f => f === fieldName);
127+
}
128+
129+
protected getFieldValue(rec: any, fieldName: string, isDate: boolean = false, grid?: GridType): any {
130+
const column = grid.getColumnByName(fieldName);
131+
let value = resolveNestedPath(rec, fieldName);
132+
133+
value = column.formatter && this.shouldApplyFormatter(fieldName) ?
134+
column.formatter(value) :
135+
value && isDate ? parseDate(value) : value;
136+
137+
return value;
138+
}
139+
}

projects/igniteui-angular/src/lib/grids/api.service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,17 @@ export class GridBaseAPIService <T extends IgxGridBaseDirective & GridType> {
605605
public remove_grouping_expression(fieldName) {
606606
}
607607

608+
public filterDataByExpressions(expressionsTree: IFilteringExpressionsTree): any[] {
609+
let data = this.get_all_data();
610+
611+
if (expressionsTree.filteringOperands.length) {
612+
const state = { expressionsTree, strategy: this.grid.filterStrategy };
613+
data = DataUtil.filter(cloneArray(data), state, this.grid);
614+
}
615+
616+
return data;
617+
}
618+
608619
/**
609620
* Updates related row of provided grid's data source with provided new row value
610621
*

projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ import {
1919
import { IgxOverlayService } from '../../../services/public_api';
2020
import { IgxFilteringService, ExpressionUI } from '../grid-filtering.service';
2121
import { FilteringExpressionsTree, IFilteringExpressionsTree } from '../../../data-operations/filtering-expressions-tree';
22-
import { cloneArray, KEYS, resolveNestedPath, parseDate, uniqueDates } from '../../../core/utils';
23-
import { DataType, DataUtil } from '../../../data-operations/data-util';
22+
import { KEYS, resolveNestedPath, parseDate, uniqueDates } from '../../../core/utils';
23+
import { DataType } from '../../../data-operations/data-util';
2424
import { Subscription, Subject } from 'rxjs';
2525
import { takeUntil } from 'rxjs/operators';
2626
import { IgxColumnComponent } from '../../columns/column.component';
2727
import { IgxGridBaseDirective } from '../../grid-base.directive';
2828
import { DisplayDensity } from '../../../core/density';
2929
import { GridSelectionMode } from '../../common/enums';
3030
import { GridBaseAPIService } from '../../api.service';
31+
import { FormattedValuesFilteringStrategy } from '../../../data-operations/filtering-strategy';
32+
import { TreeGridFormattedValuesFilteringStrategy } from '../../tree-grid/tree-grid.filtering.strategy';
3133
import { getLocaleCurrencyCode } from '@angular/common';
3234

3335
/**
@@ -536,22 +538,28 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy {
536538
});
537539
}
538540

541+
private shouldFormatValues() {
542+
return this.column.formatter &&
543+
(this.grid.filterStrategy instanceof FormattedValuesFilteringStrategy ||
544+
this.grid.filterStrategy instanceof TreeGridFormattedValuesFilteringStrategy) &&
545+
this.grid.filterStrategy.shouldApplyFormatter(this.column.field);
546+
}
547+
539548
private renderColumnValuesFromData() {
540-
let data = this.column.gridAPI.get_all_data((this.grid as any).id);
541549
const expressionsTree = this.getColumnFilterExpressionsTree();
550+
const data = this.column.gridAPI.filterDataByExpressions(expressionsTree);
542551

543-
if (expressionsTree.filteringOperands.length) {
544-
const state = { expressionsTree };
545-
data = DataUtil.filter(cloneArray(data), state, this.grid);
546-
}
547-
552+
const shouldFormatValues = this.shouldFormatValues();
548553
const columnField = this.column.field;
549554
const columnValues = (this.column.dataType === DataType.Date) ?
550555
data.map(record => {
551556
const value = (resolveNestedPath(record, columnField));
552557
const label = this.getFilterItemLabel(value);
553558
return { label, value };
554-
}) : data.map(record => resolveNestedPath(record, columnField));
559+
}) : data.map(record => {
560+
const value = resolveNestedPath(record, columnField);
561+
return shouldFormatValues ? this.column.formatter(value) : value;
562+
});
555563

556564
this.renderValues(columnValues);
557565
}
@@ -675,6 +683,8 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy {
675683
this.containsNullOrEmpty = false;
676684
this.selectAllIndeterminate = false;
677685

686+
const applyFormatter = !this.shouldFormatValues();
687+
678688
this.uniqueValues.forEach(element => {
679689
const hasValue = (element !== undefined && element !== null && element !== '' && this.column.dataType !== DataType.Date)
680690
|| !!(element && element.label);
@@ -701,7 +711,7 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy {
701711
}
702712

703713
filterListItem.value = this.getFilterItemValue(element);
704-
filterListItem.label = this.getFilterItemLabel(element);
714+
filterListItem.label = this.getFilterItemLabel(element, applyFormatter);
705715
filterListItem.indeterminate = false;
706716
this.listData.push(filterListItem);
707717
}
@@ -760,26 +770,26 @@ export class IgxGridExcelStyleFilteringComponent implements OnDestroy {
760770
}
761771
}
762772

763-
private getFilterItemLabel(element: any) {
773+
private getFilterItemLabel(element: any, applyFormatter: boolean = true) {
764774
if (this.column.dataType === DataType.Date) {
765775
return element && element.label ? element.label : this.column.formatter ?
766-
this.column.formatter(element) :
776+
applyFormatter ? this.column.formatter(element) : element :
767777
this.grid.datePipe.transform(element, this.column.pipeArgs.format, this.column.pipeArgs.timezone,
768778
this.grid.locale);
769779
}
770780
if (this.column.dataType === DataType.Number) {
771781
return this.column.formatter ?
772-
this.column.formatter(element) :
782+
applyFormatter ? this.column.formatter(element) : element :
773783
this.grid.decimalPipe.transform(element, this.column.pipeArgs.digitsInfo, this.grid.locale);
774784
}
775785
if (this.column.dataType === DataType.Currency) {
776-
return this.column.formatter ?
777-
this.column.formatter(element) :
778-
this.grid.currencyPipe.transform(element, this.column.pipeArgs.currencyCode ?
779-
this.column.pipeArgs.currencyCode : getLocaleCurrencyCode(this.grid.locale),
780-
this.column.pipeArgs.display, this.column.pipeArgs.digitsInfo, this.grid.locale);
786+
return this.column.formatter ?
787+
applyFormatter ? this.column.formatter(element) : element :
788+
this.grid.currencyPipe.transform(element, this.column.pipeArgs.currencyCode ?
789+
this.column.pipeArgs.currencyCode : getLocaleCurrencyCode(this.grid.locale),
790+
this.column.pipeArgs.display, this.column.pipeArgs.digitsInfo, this.grid.locale);
781791
}
782-
return this.column.formatter ?
792+
return this.column.formatter && applyFormatter ?
783793
this.column.formatter(element) :
784794
element;
785795
}

projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
IgxGridAdvancedFilteringBindingComponent
1919
} from '../../test-utils/grid-samples.spec';
2020
import { ControlsFunction } from '../../test-utils/controls-functions.spec';
21+
import { FormattedValuesFilteringStrategy } from '../../data-operations/filtering-strategy';
2122

2223
const ADVANCED_FILTERING_OPERATOR_LINE_AND_CSS_CLASS = 'igx-filter-tree__line--and';
2324
const ADVANCED_FILTERING_OPERATOR_LINE_OR_CSS_CLASS = 'igx-filter-tree__line--or';
@@ -1759,6 +1760,48 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => {
17591760
'incorrect horizontal position of operator dropdown');
17601761
}));
17611762

1763+
it('Should filter by cells formatted data when using FormattedValuesFilteringStrategy', fakeAsync(() => {
1764+
const formattedStrategy = new FormattedValuesFilteringStrategy(['Downloads']);
1765+
grid.filterStrategy = formattedStrategy;
1766+
const downloadsFormatter = (val: number): number => {
1767+
if (!val || val > 0 && val < 100) {
1768+
return 1;
1769+
} else if (val >= 100 && val < 500) {
1770+
return 2;
1771+
} else {
1772+
return 3;
1773+
}
1774+
};
1775+
grid.columns[2].formatter = downloadsFormatter;
1776+
fix.detectChanges();
1777+
1778+
grid.openAdvancedFilteringDialog();
1779+
tick(200);
1780+
fix.detectChanges();
1781+
1782+
// Add root group.
1783+
GridFunctions.clickAdvancedFilteringInitialAddGroupButton(fix, 0);
1784+
tick(100);
1785+
fix.detectChanges();
1786+
1787+
// Add a new expression
1788+
selectColumnInEditModeExpression(fix, 2); // Select 'ProductName' column.
1789+
selectOperatorInEditModeExpression(fix, 0); // Select 'Contains' operator.
1790+
const input = GridFunctions.getAdvancedFilteringValueInput(fix).querySelector('input');
1791+
UIInteractions.clickAndSendInputElementValue(input, '1', fix); // Type filter value.
1792+
1793+
// Commit the populated expression.
1794+
GridFunctions.clickAdvancedFilteringExpressionCommitButton(fix);
1795+
tick(100);
1796+
fix.detectChanges();
1797+
GridFunctions.clickAdvancedFilteringApplyButton(fix);
1798+
tick(100);
1799+
fix.detectChanges();
1800+
1801+
const rows = GridFunctions.getRows(fix);
1802+
expect(rows.length).toEqual(3, 'Wrong filtered rows count');
1803+
}));
1804+
17621805
describe('Context Menu - ', () => {
17631806
it('Should discard added group when clicking its operator line without having a single expression.', fakeAsync(() => {
17641807
// Open Advanced Filtering dialog.

projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
import { GridSelectionMode, FilterMode } from '../common/enums';
4848
import { ControlsFunction } from '../../test-utils/controls-functions.spec';
4949
import localeFR from '@angular/common/locales/fr';
50+
import { FormattedValuesFilteringStrategy } from '../../data-operations/filtering-strategy';
5051

5152
const DEBOUNCETIME = 30;
5253
const FILTER_UI_ROW = 'igx-grid-filtering-row';
@@ -2151,6 +2152,34 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => {
21512152
// Verify filtered data
21522153
expect(grid.filteredData).toBeNull();
21532154
}));
2155+
2156+
it('Should filter by cells formatted data when using FormattedValuesFilteringStrategy', fakeAsync(() => {
2157+
const formattedStrategy = new FormattedValuesFilteringStrategy(['Downloads']);
2158+
grid.filterStrategy = formattedStrategy;
2159+
const downloadsFormatter = (val: number): number => {
2160+
if (!val || val > 0 && val < 100) {
2161+
return 1;
2162+
} else if (val >= 100 && val < 500) {
2163+
return 2;
2164+
} else {
2165+
return 3;
2166+
}
2167+
};
2168+
grid.columns[2].formatter = downloadsFormatter;
2169+
fix.detectChanges();
2170+
2171+
GridFunctions.clickFilterCellChipUI(fix, 'Downloads');
2172+
fix.detectChanges();
2173+
2174+
GridFunctions.typeValueInFilterRowInput('3', fix);
2175+
tick(DEBOUNCETIME);
2176+
GridFunctions.closeFilterRow(fix);
2177+
fix.detectChanges();
2178+
2179+
// const cells = GridFunctions.getColumnCells(fix, 'Downloads');
2180+
const rows = GridFunctions.getRows(fix);
2181+
expect(rows.length).toEqual(2);
2182+
}));
21542183
});
21552184

21562185
describe('Integration scenarios', () => {
@@ -5105,6 +5134,50 @@ describe('IgxGrid - Filtering actions - Excel style filtering #grid', () => {
51055134
expect(inputNativeElement.type).toBe('number', 'input type of number column is not number');
51065135

51075136
}));
5137+
5138+
it('Should filter cell by its formatted data when using FormattedValueFilteringStrategy', async () => {
5139+
const formattedFilterStrategy = new FormattedValuesFilteringStrategy();
5140+
grid.filterStrategy = formattedFilterStrategy;
5141+
const productNameFormatter = (value: string): string => {
5142+
const val = value ? value.toLowerCase() : '';
5143+
if (val.includes('script')) {
5144+
return 'Web';
5145+
} else if (val.includes('netadvantage')) {
5146+
return 'Desktop';
5147+
} else {
5148+
return 'Other';
5149+
}
5150+
};
5151+
grid.columns[1].formatter = productNameFormatter;
5152+
5153+
GridFunctions.clickExcelFilterIcon(fix, grid.columns[1].field);
5154+
await wait(200);
5155+
fix.detectChanges();
5156+
5157+
const searchComponent = GridFunctions.getExcelStyleSearchComponent(fix);
5158+
const inputNativeElement = GridFunctions.getExcelStyleSearchComponentInput(fix, searchComponent);
5159+
UIInteractions.clickAndSendInputElementValue(inputNativeElement, 'script', fix);
5160+
await wait(100);
5161+
fix.detectChanges();
5162+
5163+
let items = GridFunctions.getExcelStyleSearchComponentListItems(fix);
5164+
expect(items.length).toBe(0);
5165+
5166+
UIInteractions.clickAndSendInputElementValue(inputNativeElement, 'web', fix);
5167+
await wait(100);
5168+
fix.detectChanges();
5169+
items = GridFunctions.getExcelStyleSearchComponentListItems(fix);
5170+
expect(items.length).toBe(3);
5171+
verifyExcelStyleFilterAvailableOptions(fix,
5172+
['Select all search results', 'Add current selection to filter', 'Web'],
5173+
[true, false, true]);
5174+
5175+
inputNativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
5176+
await wait(100);
5177+
fix.detectChanges();
5178+
const cellValues = GridFunctions.getColumnCells(fix, 'ProductName').map(c => c.nativeElement.innerText).sort();
5179+
expect(cellValues).toEqual(['Web', 'Web']);
5180+
});
51085181
});
51095182

51105183
describe('Templates: ', () => {

projects/igniteui-angular/src/lib/grids/tree-grid/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './tree-grid-api.service';
55
export * from './tree-grid-row.component';
66
export * from './tree-cell.component';
77
export * from './tree-grid.interfaces';
8+
export * from './tree-grid.filtering.strategy';

0 commit comments

Comments
 (0)