Skip to content

Commit fa8c887

Browse files
authored
Merge pull request #11841 from IgniteUI/PMiteva/combo-filtering
Expose combo filtering strategy
2 parents b4c1881 + 30ed48b commit fa8c887

12 files changed

+285
-82
lines changed

CHANGELOG.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ All notable changes for each version of this project will be documented in this
1111
{{ value.member }}
1212
</ng-template>
1313
```
14+
### New Features
15+
- `IgxCombo` and `IgxSimpleComboComponent`
16+
- `filterFunction` input is added. The new property allows changing of the way filtering is done in the combos. By default filtering is made over the values in combo's data when it is a collection of primitive values, or over the values as defined in `displayKey` of the combo. If custom filtering function is provided filtering will be done as specified in the provided function.
17+
- `filteringOptions` are extended and now contains `filterable` and `filteringKey` properties. Setting `filterable` determines whether combo will be filterable. By default filtering is done over the data value when they are primitive, or over the field of the values equal to `displayKey`. `filteringKey` allows to filter data by any data related key.
18+
- **Breaking Changes** - `filterable` property of `IgxComboComponent` is now deprecated and will be removed in future version. Use `filteringOptions.filterable` instead.
1419
1520
## 14.0.0
1621
- `IgxDatePicker` and `IgxDateRangePicker` now expose a `weekStart` input property like the `IgxCalendar`
@@ -20,7 +25,8 @@ All notable changes for each version of this project will be documented in this
2025
- The `label` attribute has been changed to `aria-labelledby` and can be set by a latterly added input property `labelledBy`.
2126
2227
### New Features
23-
28+
- `IgxCombo` and `IgxSimpleComboComponent`
29+
- `filterFunction` input is added. The new property allows changing of the way filtering is done in the combos. By default filtering is made over the values in combo's data when it is a collection of primitive values, or over the values as defined in `displayKey` of the combo. If custom filtering function is provided filtering will be done as specified in the provided function.
2430
2531
### General
2632
- Updating dependency to Angular 14

projects/igniteui-angular/src/lib/combo/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,8 @@ Setting `[displayDensity]` affects the control's items' and inputs' css properti
322322
| `valid` | gets if control is valid, when used in a form | boolean |
323323
| `overlaySettings` | gets/sets the custom overlay settings that control how the drop-down list displays | OverlaySettings |
324324
| `autoFocusSearch` | controls whether the search input should be focused when the combo is opened | boolean |
325+
| `filteringOptions` | Configures the way combo items will be filtered | IComboFilteringOptions |
326+
| `filterFunction` | Gets/Sets the custom filtering function of the combo | `(collection: any[], searchValue: any, caseSensitive: boolean) => any[]` |
325327

326328
### Getters
327329
| Name | Description | Type |

projects/igniteui-angular/src/lib/combo/combo.common.ts

+45-9
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ import {
3939
IgxComboAddItemDirective, IgxComboClearIconDirective, IgxComboEmptyDirective,
4040
IgxComboFooterDirective, IgxComboHeaderDirective, IgxComboHeaderItemDirective, IgxComboItemDirective, IgxComboToggleIconDirective
4141
} from './combo.directives';
42-
import {
43-
IComboFilteringOptions, IComboItemAdditionEvent, IComboSearchInputEventArgs
44-
} from './public_api';
42+
import { IComboItemAdditionEvent, IComboSearchInputEventArgs } from './public_api';
4543

4644
export const IGX_COMBO_COMPONENT = new InjectionToken<IgxComboBase>('IgxComboComponentToken');
4745

@@ -114,6 +112,16 @@ export enum IgxComboState {
114112
INVALID = IgxInputState.INVALID
115113
}
116114

115+
/** The filtering criteria to be applied on data search */
116+
export interface IComboFilteringOptions {
117+
/** Defines filtering case-sensitivity */
118+
caseSensitive: boolean;
119+
/** Defines whether filtering is allowed */
120+
filterable: boolean;
121+
/** Defines optional key to filter against complex list items. Default to displayKey if provided.*/
122+
filteringKey?: string;
123+
}
124+
117125
@Directive()
118126
export abstract class IgxComboBaseDirective extends DisplayDensityBase implements IgxComboBase, OnInit, DoCheck,
119127
AfterViewInit, OnDestroy, ControlValueAccessor {
@@ -386,6 +394,17 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement
386394
this._groupSortingDirection = val;
387395
}
388396

397+
/**
398+
* Gets/Sets the custom filtering function of the combo.
399+
*
400+
* @example
401+
* ```html
402+
* <igx-comb #combo [data]="localData" [filterFunction]="filterFunction"></igx-combo>
403+
* ```
404+
*/
405+
@Input()
406+
public filterFunction: (collection: any[], searchValue: any, filteringOptions: IComboFilteringOptions) => any[];
407+
389408
/**
390409
* An @Input property that set aria-labelledby attribute
391410
* ```html
@@ -841,13 +860,29 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement
841860
/** @hidden @internal */
842861
public defaultFallbackGroup = 'Other';
843862
/** @hidden @internal */
844-
public filteringOptions: IComboFilteringOptions = {
845-
caseSensitive: false
846-
};
847-
/** @hidden @internal */
848863
public activeDescendant = '';
849864

865+
/**
866+
* Configures the way combo items will be filtered.
867+
*
868+
* ```typescript
869+
* // get
870+
* let myFilteringOptions = this.combo.filteringOptions;
871+
* ```
872+
*
873+
* ```html
874+
* <!--set-->
875+
* <igx-combo [filteringOptions]='myFilteringOptions'></igx-combo>
876+
* ```
877+
*/
850878

879+
@Input()
880+
public get filteringOptions(): IComboFilteringOptions {
881+
return this._filteringOptions || this._defaultFilteringOptions;
882+
}
883+
public set filteringOptions(value: IComboFilteringOptions) {
884+
this._filteringOptions = value;
885+
}
851886
protected _data = [];
852887
protected _value = '';
853888
protected _groupKey = '';
@@ -867,7 +902,8 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement
867902
private _itemsMaxHeight = null;
868903
private _overlaySettings: OverlaySettings;
869904
private _groupSortingDirection: SortingDirection = SortingDirection.Asc;
870-
905+
private _filteringOptions: IComboFilteringOptions;
906+
private _defaultFilteringOptions: IComboFilteringOptions = { caseSensitive: false, filterable: true };
871907
public abstract dropdown: IgxComboDropDownComponent;
872908

873909
public abstract selectionChanging: EventEmitter<any>;
@@ -1143,7 +1179,7 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement
11431179

11441180
/** @hidden @internal */
11451181
public toggleCaseSensitive() {
1146-
this.filteringOptions = { caseSensitive: !this.filteringOptions.caseSensitive };
1182+
this.filteringOptions = Object.assign({}, this.filteringOptions, { caseSensitive: !this.filteringOptions.caseSensitive });
11471183
}
11481184

11491185
protected onStatusChanged = () => {

projects/igniteui-angular/src/lib/combo/combo.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
[tabindex]="dropdown.collapsed ? -1 : 0" [attr.id]="dropdown.id" aria-multiselectable="true"
5858
[attr.aria-activedescendant]="this.activeDescendant">
5959
<igx-combo-item [itemHeight]='itemHeight' *igxFor="let item of data
60-
| comboFiltering:filterValue:displayKey:filteringOptions:filterable
60+
| comboFiltering:filterValue:displayKey:filteringOptions:filterFunction
6161
| comboGrouping:groupKey:valueKey:groupSortingDirection;
6262
index as rowIndex; containerSize: itemsMaxHeight; scrollOrientation: 'vertical'; itemSize: itemHeight"
6363
[value]="item" [isHeader]="item.isHeader" [index]="rowIndex" [role]="item.isHeader? 'group' : 'option'">

projects/igniteui-angular/src/lib/combo/combo.component.spec.ts

+130-26
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,33 @@
1-
import { AfterViewInit, ChangeDetectorRef, Component, Injectable, OnInit, ViewChild, OnDestroy, DebugElement } from '@angular/core';
2-
import { TestBed, tick, fakeAsync, ComponentFixture, waitForAsync } from '@angular/core/testing';
1+
import { AfterViewInit, ChangeDetectorRef, Component, DebugElement, Injectable, OnDestroy, OnInit, ViewChild } from '@angular/core';
2+
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
3+
import {
4+
FormsModule, NgControl, NgForm, NgModel, ReactiveFormsModule, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators
5+
} from '@angular/forms';
36
import { By } from '@angular/platform-browser';
47
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
5-
import { UntypedFormGroup, UntypedFormControl, Validators, UntypedFormBuilder, ReactiveFormsModule,
6-
FormsModule, NgControl, NgModel, NgForm } from '@angular/forms';
7-
import {
8-
IgxComboComponent,
9-
IgxComboModule,
10-
IComboSelectionChangingEventArgs,
11-
IComboSearchInputEventArgs,
12-
IComboItemAdditionEvent
13-
} from './combo.component';
14-
import { IgxComboItemComponent } from './combo-item.component';
15-
import { IgxComboDropDownComponent } from './combo-dropdown.component';
16-
import { IgxComboAddItemComponent } from './combo-add-item.component';
17-
import { IgxComboFilteringPipe } from './combo.pipes';
18-
import { IgxInputState } from '../directives/input/input.directive';
19-
import { IForOfState } from '../directives/for-of/for_of.directive';
20-
import { IgxToggleModule } from '../directives/toggle/toggle.directive';
218
import { BehaviorSubject, Observable } from 'rxjs';
229
import { take } from 'rxjs/operators';
23-
import { UIInteractions, wait } from '../test-utils/ui-interactions.spec';
24-
import { configureTestSuite } from '../test-utils/configure-suite';
2510
import { DisplayDensity } from '../core/density';
26-
import { AbsoluteScrollStrategy, ConnectedPositioningStrategy } from '../services/public_api';
2711
import { IgxSelectionAPIService } from '../core/selection';
28-
import { IgxIconService } from '../icon/public_api';
2912
import { IBaseCancelableBrowserEventArgs } from '../core/utils';
3013
import { SortingDirection } from '../data-operations/sorting-strategy';
31-
import { IgxComboState } from './combo.common';
14+
import { IForOfState } from '../directives/for-of/for_of.directive';
15+
import { IgxInputState } from '../directives/input/input.directive';
16+
import { IgxToggleModule } from '../directives/toggle/toggle.directive';
3217
import { IgxDropDownItemBaseDirective } from '../drop-down/public_api';
18+
import { IgxIconService } from '../icon/public_api';
19+
import { AbsoluteScrollStrategy, ConnectedPositioningStrategy } from '../services/public_api';
20+
import { configureTestSuite } from '../test-utils/configure-suite';
21+
import { UIInteractions, wait } from '../test-utils/ui-interactions.spec';
22+
import { IgxComboAddItemComponent } from './combo-add-item.component';
23+
import { IgxComboDropDownComponent } from './combo-dropdown.component';
24+
import { IgxComboItemComponent } from './combo-item.component';
25+
import { IComboFilteringOptions, IgxComboState } from './combo.common';
26+
import {
27+
IComboItemAdditionEvent, IComboSearchInputEventArgs, IComboSelectionChangingEventArgs, IgxComboComponent,
28+
IgxComboModule
29+
} from './combo.component';
30+
import { IgxComboFilteringPipe } from './combo.pipes';
3331

3432
const CSS_CLASS_COMBO = 'igx-combo';
3533
const CSS_CLASS_COMBO_DROPDOWN = 'igx-combo__drop-down';
@@ -49,11 +47,8 @@ const CSS_CLASS_SCROLLBAR_VERTICAL = 'igx-vhelper--vertical';
4947
const CSS_CLASS_INPUTGROUP = 'igx-input-group';
5048
const CSS_CLASS_COMBO_INPUTGROUP = 'igx-input-group__input';
5149
const CSS_CLASS_INPUTGROUP_WRAPPER = 'igx-input-group__wrapper';
52-
const CSS_CLASS_INPUTGROUP_BUNDLE = 'igx-input-group__bundle';
53-
const CSS_CLASS_INPUTGROUP_MAINBUNDLE = 'igx-input-group__bundle-main';
5450
const CSS_CLASS_INPUTGROUP_REQUIRED = 'igx-input-group--required';
5551
const CSS_CLASS_INPUTGROUP_LABEL = 'igx-input-group__label';
56-
const CSS_CLASS_INPUTGROUP_BORDER = 'igx-input-group__border';
5752
const CSS_CLASS_SEARCHINPUT = 'input[name=\'searchInput\']';
5853
const CSS_CLASS_HEADER = 'header-class';
5954
const CSS_CLASS_FOOTER = 'footer-class';
@@ -2678,6 +2673,115 @@ describe('igxCombo', () => {
26782673
expect(combo.searchValue).toEqual('Test');
26792674
cancelSub.unsubscribe();
26802675
});
2676+
it('Should filter the data when custom filterFunction is provided', fakeAsync(() => {
2677+
combo.open();
2678+
tick();
2679+
fixture.detectChanges();
2680+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2681+
2682+
combo.searchValue = 'new england';
2683+
combo.handleInputChange();
2684+
fixture.detectChanges();
2685+
expect(combo.dropdown.items.length).toEqual(0);
2686+
2687+
combo.close();
2688+
tick();
2689+
fixture.detectChanges();
2690+
combo.filteringOptions = { caseSensitive: false, filterable: true, filteringKey: combo.groupKey };
2691+
combo.filterFunction = (collection: any[], searchValue: any, filteringOptions: IComboFilteringOptions): any[] => {
2692+
if (!collection) return [];
2693+
if (!searchValue) return collection;
2694+
const searchTerm = filteringOptions.caseSensitive ? searchValue.trim() : searchValue.toLowerCase().trim();
2695+
return collection.filter(i => filteringOptions.caseSensitive ?
2696+
i[filteringOptions.filteringKey]?.includes(searchTerm) :
2697+
i[filteringOptions.filteringKey]?.toString().toLowerCase().includes(searchTerm))
2698+
}
2699+
combo.open();
2700+
tick();
2701+
fixture.detectChanges();
2702+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2703+
2704+
combo.searchValue = 'new england';
2705+
combo.handleInputChange();
2706+
fixture.detectChanges();
2707+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2708+
2709+
combo.filterFunction = undefined;
2710+
combo.filteringOptions = undefined;
2711+
fixture.detectChanges();
2712+
expect(combo.dropdown.items.length).toEqual(0);
2713+
}));
2714+
it('Should update filtering when custom filterFunction is provided and filteringOptions.caseSensitive is changed', fakeAsync(() => {
2715+
combo.open();
2716+
tick();
2717+
fixture.detectChanges();
2718+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2719+
2720+
combo.searchValue = 'new england';
2721+
combo.handleInputChange();
2722+
fixture.detectChanges();
2723+
expect(combo.dropdown.items.length).toEqual(0);
2724+
2725+
combo.close();
2726+
tick();
2727+
fixture.detectChanges();
2728+
combo.filteringOptions = { caseSensitive: false, filterable: true, filteringKey: combo.groupKey };
2729+
combo.filterFunction = (collection: any[], searchValue: any, filteringOptions: IComboFilteringOptions): any[] => {
2730+
if (!collection) return [];
2731+
if (!searchValue) return collection;
2732+
const searchTerm = filteringOptions.caseSensitive ? searchValue.trim() : searchValue.toLowerCase().trim();
2733+
return collection.filter(i => filteringOptions.caseSensitive ?
2734+
i[filteringOptions.filteringKey]?.includes(searchTerm) :
2735+
i[filteringOptions.filteringKey]?.toString().toLowerCase().includes(searchTerm))
2736+
}
2737+
combo.open();
2738+
tick();
2739+
fixture.detectChanges();
2740+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2741+
2742+
combo.searchValue = 'new england';
2743+
combo.handleInputChange();
2744+
fixture.detectChanges();
2745+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2746+
2747+
combo.filteringOptions = Object.assign({}, combo.filteringOptions, { caseSensitive: true });
2748+
fixture.detectChanges();
2749+
expect(combo.dropdown.items.length).toEqual(0);
2750+
}));
2751+
it('Should update filtering when custom filteringOptions are provided', fakeAsync(() => {
2752+
combo.open();
2753+
tick();
2754+
fixture.detectChanges();
2755+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2756+
2757+
combo.searchValue = 'new england';
2758+
combo.handleInputChange();
2759+
fixture.detectChanges();
2760+
expect(combo.dropdown.items.length).toEqual(0);
2761+
2762+
combo.close();
2763+
tick();
2764+
fixture.detectChanges();
2765+
combo.filteringOptions = { caseSensitive: false, filterable: true, filteringKey: combo.groupKey };
2766+
combo.open();
2767+
tick();
2768+
fixture.detectChanges();
2769+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2770+
2771+
combo.searchValue = 'new england';
2772+
combo.handleInputChange();
2773+
fixture.detectChanges();
2774+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2775+
2776+
combo.searchValue = 'value not in the list';
2777+
combo.handleInputChange();
2778+
fixture.detectChanges();
2779+
expect(combo.dropdown.items.length).toEqual(0);
2780+
2781+
combo.filteringOptions = Object.assign({}, combo.filteringOptions, { filterable: false });
2782+
fixture.detectChanges();
2783+
expect(combo.dropdown.items.length).toBeGreaterThan(0);
2784+
}));
26812785
});
26822786
describe('Form control tests: ', () => {
26832787
describe('Reactive form tests: ', () => {

projects/igniteui-angular/src/lib/combo/combo.component.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ import { IgxComboAPIService } from './combo.api';
3636
import { EditorProvider } from '../core/edit-provider';
3737
import { IgxInputGroupType, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
3838

39-
/** The filtering criteria to be applied on data search */
40-
export interface IComboFilteringOptions {
41-
/** Defines filtering case-sensitivity */
42-
caseSensitive: boolean;
43-
}
44-
4539
/** Event emitted when an igx-combo's selection is changing */
4640
export interface IComboSelectionChangingEventArgs extends IBaseCancelableEventArgs {
4741
/** An array containing the values that are currently selected */
@@ -124,13 +118,20 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
124118
public autoFocusSearch = true;
125119

126120
/**
121+
* @deprecated in version 14.0.0. Use the IComboFilteringOptions.filterable
122+
*
127123
* An @Input property that enabled/disables filtering in the list. The default is `true`.
128124
* ```html
129125
* <igx-combo [filterable]="false">
130126
* ```
131127
*/
132128
@Input()
133-
public filterable = true;
129+
public get filterable(): boolean {
130+
return this.filteringOptions.filterable;
131+
}
132+
public set filterable(value: boolean) {
133+
this.filteringOptions = Object.assign({}, this.filteringOptions, { filterable: value });
134+
}
134135

135136
/**
136137
* Defines the placeholder value for the combo dropdown search field
@@ -171,7 +172,7 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
171172

172173
/** @hidden @internal */
173174
public get filteredData(): any[] | null {
174-
return this.filterable ? this._filteredData : this.data;
175+
return this.filteringOptions.filterable ? this._filteredData : this.data;
175176
}
176177
/** @hidden @internal */
177178
public set filteredData(val: any[] | null) {
@@ -203,7 +204,7 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
203204

204205
/** @hidden @internal */
205206
public get displaySearchInput(): boolean {
206-
return this.filterable || this.allowCustomValues;
207+
return this.filteringOptions.filterable || this.allowCustomValues;
207208
}
208209

209210
/**

0 commit comments

Comments
 (0)