Skip to content

Expose combo filtering strategy 14.0.x #11844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 12, 2022
Merged
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

All notable changes for each version of this project will be documented in this file.

## 14.0.6

### New Features
- `IgxCombo` and `IgxSimpleComboComponent`
- `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.

## 14.0.0

- `IgxDatePicker` and `IgxDateRangePicker` now expose a `weekStart` input property like the `IgxCalendar`

### New Features

### General
- Updating dependency to Angular 14
- `Migrations`
Expand Down
1 change: 1 addition & 0 deletions projects/igniteui-angular/src/lib/combo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ Setting `[displayDensity]` affects the control's items' and inputs' css properti
| `valid` | gets if control is valid, when used in a form | boolean |
| `overlaySettings` | gets/sets the custom overlay settings that control how the drop-down list displays | OverlaySettings |
| `autoFocusSearch` | controls whether the search input should be focused when the combo is opened | boolean |
| `filterFunction` | Gets/Sets the custom filtering function of the combo | `(collection: any[], searchValue: any, caseSensitive: boolean) => any[]` |

### Getters
| Name | Description | Type |
Expand Down
11 changes: 11 additions & 0 deletions projects/igniteui-angular/src/lib/combo/combo.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,17 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement
this._groupSortingDirection = val;
}

/**
* Gets/Sets the custom filtering function of the combo.
*
* @example
* ```html
* <igx-comb #combo [data]="localData" [filterFunction]="filterFunction"></igx-combo>
* ```
*/
@Input()
public filterFunction: (collection: any[], searchValue: any, caseSensitive: boolean) => any[];

/**
* An @Input property that set aria-labelledby attribute
* ```html
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
[style.maxHeight.px]="itemsMaxHeight" [igxDropDownItemNavigation]="dropdown" (focus)="dropdown.onFocus()"
[tabindex]="dropdown.collapsed ? -1 : 0" role="listbox" [attr.id]="dropdown.id">
<igx-combo-item role="option" [itemHeight]='itemHeight' *igxFor="let item of data
| comboFiltering:filterValue:displayKey:filteringOptions:filterable
| comboFiltering:filterValue:displayKey:filteringOptions:filterable:filterFunction
| comboGrouping:groupKey:valueKey:groupSortingDirection;
index as rowIndex; containerSize: itemsMaxHeight; scrollOrientation: 'vertical'; itemSize: itemHeight"
[value]="item" [isHeader]="item.isHeader" [index]="rowIndex">
Expand Down
116 changes: 116 additions & 0 deletions projects/igniteui-angular/src/lib/combo/combo.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2693,6 +2693,122 @@ describe('igxCombo', () => {
expect(combo.searchValue).toEqual('Test');
cancelSub.unsubscribe();
});
it('Should filter the data when custom filterFunction is provided', fakeAsync(() => {
combo.open();
tick();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.searchValue = 'new england';
combo.handleInputChange();
fixture.detectChanges();
expect(combo.dropdown.items.length).toEqual(0);

combo.close();
tick();
fixture.detectChanges();
// set filter function to search only on valueKyes
combo.filterFunction = (collection: any[], searchValue: any): any[] => {
if (!collection) return [];
if (!searchValue) return collection;
const searchTerm = combo.filteringOptions.caseSensitive ? searchValue.trim() : searchValue.toLowerCase().trim();
return collection.filter(i => combo.filteringOptions.caseSensitive ?
i[combo.displayKey]?.includes(searchTerm) || i[combo.groupKey]?.includes(searchTerm) :
i[combo.displayKey]?.toString().toLowerCase().includes(searchTerm) || i[combo.groupKey]?.toString().toLowerCase().includes(searchTerm))
}
combo.open();
tick();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.searchValue = 'new england';
combo.handleInputChange();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.filterFunction = undefined;
fixture.detectChanges();
expect(combo.dropdown.items.length).toEqual(0);
}));
it('Should update filtering when custom filterFunction is provided and filteringOptions are changed', fakeAsync(() => {
combo.open();
tick();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.searchValue = 'new england';
combo.handleInputChange();
fixture.detectChanges();
expect(combo.dropdown.items.length).toEqual(0);

combo.close();
tick();
fixture.detectChanges();
// set filter function to search only on valueKyes
combo.filterFunction = (collection: any[], searchValue: any): any[] => {
if (!collection) return [];
if (!searchValue) return collection;
const searchTerm = combo.filteringOptions.caseSensitive ? searchValue.trim() : searchValue.toLowerCase().trim();
return collection.filter(i => combo.filteringOptions.caseSensitive ?
i[combo.displayKey]?.includes(searchTerm) || i[combo.groupKey]?.includes(searchTerm) :
i[combo.displayKey]?.toString().toLowerCase().includes(searchTerm) || i[combo.groupKey]?.toString().toLowerCase().includes(searchTerm))
}
combo.open();
tick();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.searchValue = 'new england';
combo.handleInputChange();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.filteringOptions = { caseSensitive: true };
fixture.detectChanges();
expect(combo.dropdown.items.length).toEqual(0);
}));
it('Should update filtering when custom filterFunction is provided and filterable is changed', fakeAsync(() => {
combo.open();
tick();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.searchValue = 'new england';
combo.handleInputChange();
fixture.detectChanges();
expect(combo.dropdown.items.length).toEqual(0);

combo.close();
tick();
fixture.detectChanges();
// set filter function to search only on valueKyes
combo.filterFunction = (collection: any[], searchValue: any): any[] => {
if (!collection) return [];
if (!searchValue) return collection;
const searchTerm = combo.filteringOptions.caseSensitive ? searchValue.trim() : searchValue.toLowerCase().trim();
return collection.filter(i => combo.filteringOptions.caseSensitive ?
i[combo.displayKey]?.includes(searchTerm) || i[combo.groupKey]?.includes(searchTerm) :
i[combo.displayKey]?.toString().toLowerCase().includes(searchTerm) || i[combo.groupKey]?.toString().toLowerCase().includes(searchTerm))
}
combo.open();
tick();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.searchValue = 'new england';
combo.handleInputChange();
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);

combo.searchValue = 'value not in the list';
combo.handleInputChange();
fixture.detectChanges();
expect(combo.dropdown.items.length).toEqual(0);

combo.filterable = false;
fixture.detectChanges();
expect(combo.dropdown.items.length).toBeGreaterThan(0);
}));
});
describe('Form control tests: ', () => {
describe('Reactive form tests: ', () => {
Expand Down
51 changes: 32 additions & 19 deletions projects/igniteui-angular/src/lib/combo/combo.pipes.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
import { Inject, Pipe, PipeTransform } from '@angular/core';
import { cloneArray } from '../core/utils';
import { DataUtil } from '../data-operations/data-util';
import { IGX_COMBO_COMPONENT, IgxComboBase } from './combo.common';
import { DefaultSortingStrategy, SortingDirection } from '../data-operations/sorting-strategy';
import { IgxComboBase, IGX_COMBO_COMPONENT } from './combo.common';
import { IComboFilteringOptions } from './combo.component';

/** @hidden */
@Pipe({
name: 'comboClean'
})
@Pipe({ name: 'comboClean' })
export class IgxComboCleanPipe implements PipeTransform {
public transform(collection: any[]) {
return collection.filter(e => !!e);
}
}

/** @hidden */
@Pipe({
name: 'comboFiltering'
})
@Pipe({ name: 'comboFiltering' })
export class IgxComboFilteringPipe implements PipeTransform {
public transform(collection: any[], searchValue: any, displayKey: any,
filteringOptions: IComboFilteringOptions, shouldFilter = false) {
private displayKey: any;

public transform (
collection: any[],
searchValue: any,
displayKey: any,
filteringOptions: IComboFilteringOptions,
filterable: boolean,
filterFunction: (collection: any[], searchValue: any, caseSensitive: boolean) => any[] = defaultFilterFunction) {
if (!collection) {
return [];
}
if (!searchValue || !shouldFilter) {
if (!filterable) {
return collection;
} else {
const searchTerm = filteringOptions.caseSensitive ? searchValue.trim() : searchValue.toLowerCase().trim();
if (displayKey != null) {
return collection.filter(e => filteringOptions.caseSensitive ? e[displayKey]?.includes(searchTerm) :
e[displayKey]?.toString().toLowerCase().includes(searchTerm));
} else {
return collection.filter(e => filteringOptions.caseSensitive ? e.includes(searchTerm) :
e.toString().toLowerCase().includes(searchTerm));
}
}
this.displayKey = displayKey;
return filterFunction.call(this, collection, searchValue, filteringOptions.caseSensitive);
}
}

Expand Down Expand Up @@ -78,3 +74,20 @@ export class IgxComboGroupingPipe implements PipeTransform {
return data;
}
}

function defaultFilterFunction (collection: any[], searchValue: any, matchCase: boolean): any[] {
if (!searchValue) {
return collection;
}
const searchTerm = matchCase ? searchValue.trim() : searchValue.toLowerCase().trim();
if (this.displayKey != null) {
return collection.filter(e => matchCase ?
e[this.displayKey]?.includes(searchTerm) :
e[this.displayKey]?.toString().toLowerCase().includes(searchTerm));
} else {
return collection.filter(e => matchCase ?
e.includes(searchTerm) :
e.toString().toLowerCase().includes(searchTerm));
}
}

3 changes: 2 additions & 1 deletion projects/igniteui-angular/src/lib/simple-combo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ Setting `[displayDensity]` affects the control's items' and inputs' css properti
| `ariaLabelledBy` | Defines label ID related to combo. | `boolean` |
| `valid` | gets if control is valid, when used in a form. | `boolean` |
| `overlaySettings` | Controls how the dropdown is displayed. | `OverlaySettings` |
| `selected` | Get current selection state. | `Array<any>` |
| `selected` | Get current selection state. | `Array<any>` |
| `filterFunction` | Gets/Sets the custom filtering function of the combo | `(collection: any[], searchValue: any, caseSensitive: boolean) => any[]` |


### Methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
(keydown)="handleItemKeyDown($event)">
<igx-combo-item role="option" [singleMode]="true" [itemHeight]='itemHeight' (click)="handleItemClick()" *igxFor="let item of data
| comboClean
| comboFiltering:filterValue:displayKey:filteringOptions:true
| comboFiltering:filterValue:displayKey:filteringOptions:true:filterFunction
| comboGrouping:groupKey:valueKey:groupSortingDirection;
index as rowIndex; containerSize: itemsMaxHeight; scrollOrientation: 'vertical'; itemSize: itemHeight"
[value]="item" [isHeader]="item.isHeader" [index]="rowIndex">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AfterViewInit, ChangeDetectorRef, Component, DebugElement, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { ReactiveFormsModule, FormsModule, NgForm } from '@angular/forms';
import { FormsModule, NgForm, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { IgxComboDropDownComponent } from '../combo/combo-dropdown.component';
Expand All @@ -12,7 +12,7 @@ import { IgxSelectionAPIService } from '../core/selection';
import { IBaseCancelableBrowserEventArgs, PlatformUtil } from '../core/utils';
import { IgxToggleModule } from '../directives/toggle/toggle.directive';
import { IgxIconComponent, IgxIconModule, IgxIconService } from '../icon/public_api';
import { IgxInputDirective, IgxInputState } from '../input-group/public_api';
import { IgxInputState } from '../input-group/public_api';
import { AbsoluteScrollStrategy, ConnectedPositioningStrategy } from '../services/public_api';
import { configureTestSuite } from '../test-utils/configure-suite';
import { UIInteractions, wait } from '../test-utils/ui-interactions.spec';
Expand Down
3 changes: 2 additions & 1 deletion src/app/combo/combo.sample.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<div>
<h5 class="sample-title">Simple Combo in Template Form</h5>
<form>
<igx-simple-combo class="input-container" [placeholder]="'Locations'" name="anyName"
<igx-simple-combo #simpleCombo class="input-container" [placeholder]="'Locations'" name="anyName"
#comboModel="ngModel" [(ngModel)]="singleValue" minlength="2" required [data]="items"
[displayKey]="valueKeyVar" [disabled]="isDisabled" [valueKey]="valueKeyVar"
[groupKey]="valueKeyVar ? 'region' : ''" [width]="'100%'">
Expand Down Expand Up @@ -99,6 +99,7 @@ <h5 class="sample-title">Reactive Form</h5>
<button igxButton="raised" igxRipple (click)="toggleItem('Connecticut')">Toggle "Connecticut"
</button>
<button igxButton="raised" igxRipple (click)="changeItemTemplate()">Change Item Template</button>
<button igxButton="raised" igxRipple (click)="changeFiltering()">Change Filtering</button>
</div>
</div>
<ng-template #customItemTemplate let-display let-key="valueKey">
Expand Down
27 changes: 26 additions & 1 deletion src/app/combo/combo.sample.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, ViewChild, OnInit, TemplateRef, AfterViewInit, ElementRef } from '@angular/core';
import { IgxComboComponent, IComboSelectionChangingEventArgs,
DisplayDensity, OverlaySettings, VerticalAlignment, HorizontalAlignment, GlobalPositionStrategy,
scaleInCenter, scaleOutCenter, ElasticPositionStrategy, ConnectedPositioningStrategy
scaleInCenter, scaleOutCenter, ElasticPositionStrategy, ConnectedPositioningStrategy, IgxSimpleComboComponent
} from 'igniteui-angular';
import { ButtonGroupAlignment } from 'igniteui-angular';
import { take } from 'rxjs/operators';
Expand All @@ -21,6 +21,9 @@ export class ComboSampleComponent implements OnInit, AfterViewInit {
private comboRef: ElementRef;
@ViewChild('customItemTemplate', { read: TemplateRef, static: true })
private customItemTemplate;
@ViewChild('simpleCombo', { read: IgxSimpleComboComponent, static: true })
private simpleCombo;
private hasCustomFilter = false;

public alignment: ButtonGroupAlignment = ButtonGroupAlignment.vertical;
public toggleItemState = false;
Expand Down Expand Up @@ -181,4 +184,26 @@ export class ComboSampleComponent implements OnInit, AfterViewInit {
public handleSelectionChange(event: IComboSelectionChangingEventArgs) {
console.log(event);
}

public changeFiltering() {
if (this.hasCustomFilter) {
this.igxCombo.filterFunction = undefined;
this.simpleCombo.filterFunction = undefined;
} else {
this.igxCombo.filterFunction = this.customFilterFunction;
this.simpleCombo.filterFunction = this.customFilterFunction;
}

this.hasCustomFilter = !this.hasCustomFilter;
}

private customFilterFunction = (collection: any[], filterValue: any) => {
if (!filterValue) {
return collection;
}
const searchTerm = this.igxCombo.filteringOptions.caseSensitive ? filterValue.trim() : filterValue.toLowerCase().trim();
return collection.filter(i => this.igxCombo.filteringOptions.caseSensitive ?
i[this.igxCombo.displayKey]?.includes(searchTerm) || i[this.igxCombo.groupKey]?.includes(searchTerm) :
i[this.igxCombo.displayKey]?.toString().toLowerCase().includes(searchTerm) || i[this.igxCombo.groupKey]?.toString().toLowerCase().includes(searchTerm))
}
}