Skip to content

feat(combo): add selectionMode input, #9832 #9901

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

Closed
wants to merge 1 commit into from
Closed
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ All notable changes for each version of this project will be documented in this
- `IgxDateTimeEditor`, `IgxMask`, `IgxDatePicker`, `IgxTimePicker`, `IgxDateRangePicker`
- Added IME input support. When typing in an Asian language input, the control will display input method compositions and candidate lists directly in the control’s editing area, and immediately re-flow surrounding text as the composition ends.

- `IgxCombo`
- Added `selectionMode` `@Input`. Allows to specify whether the selection mode should be 'multiple' (default, current behavior) or 'single' (allowing only one item to be selected).
```html
<igx-combo #single [data]="myData" selectionMode="single">
...
</igx-combo>
```

### General
- `IgxGridComponent`
- The following properties are deprecated:
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 @@ -301,6 +301,7 @@ Setting `[displayDensity]` affects the control's items' and inputs' css properti
| `id` | combo id | string |
| `data` | combo data source | any |
| `allowCustomValue` | enables/disables combo custom value | boolean |
| `selectionMode` | specified whether the combo allows single or multiple selection | string ('multiple' | 'single' ) |
| `filterable` | enables/disables combo drop down filtering - enabled by default | boolean |
| `showSearchCaseIcon` | defines whether the search case-sensitive icon should be displayed - disabled by default| boolean |
| `valueKey` | combo value data source property | string |
Expand Down
82 changes: 76 additions & 6 deletions projects/igniteui-angular/src/lib/combo/combo.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import { AfterViewInit, ChangeDetectorRef, Component, Injectable, OnInit, ViewCh
import { TestBed, tick, fakeAsync, ComponentFixture, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { FormGroup, FormControl, Validators, FormBuilder, ReactiveFormsModule,
FormsModule, NgControl, NgModel, NgForm } from '@angular/forms';
import {
FormGroup, FormControl, Validators, FormBuilder, ReactiveFormsModule,
FormsModule, NgControl, NgModel, NgForm
} from '@angular/forms';
import {
IgxComboComponent,
IgxComboModule,
IComboSelectionChangeEventArgs,
IgxComboState,
IComboSearchInputEventArgs,
IComboItemAdditionEvent
IComboItemAdditionEvent,
ComboSelectionMode
} from './combo.component';
import { IgxComboItemComponent } from './combo-item.component';
import { IgxComboDropDownComponent } from './combo-dropdown.component';
Expand Down Expand Up @@ -305,7 +308,7 @@ describe('igxCombo', () => {
};
combo.comboInput = {
nativeElement: {
focus: () => {}
focus: () => { }
}
} as any;
combo.handleOpening(inputEvent);
Expand Down Expand Up @@ -2090,6 +2093,50 @@ describe('igxCombo', () => {
expect(combo.selectedItems().length).toEqual(0);
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(0);
});
it('should allow changing of selection mode runtime, leaving only the last element selected', () => {
spyOn(combo.onSelectionChange, 'emit').and.callThrough();

combo.selectItems(['Michigan', 'Ohio', 'Wisconsin']);
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(1);
expect(combo.selectedItems().length).toBe(3);
combo.selectionMode = ComboSelectionMode.single;
fixture.detectChanges();
expect(combo.selectedItems().length).toBe(1);
expect(combo.selectedItems()).toEqual(['Wisconsin']);
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(2);

// does not mutate the input array
const selectCall = ['Ohio', 'Wisconsin', 'Michigan'];
combo.selectItems(selectCall);
expect(combo.selectedItems().length).toBe(1);
expect(combo.selectedItems()).toEqual(['Michigan']);
expect(selectCall).toEqual(['Ohio', 'Wisconsin', 'Michigan']);
});
it('should support single selection mode, allowing only one item to be selected', () => {
spyOn(combo.onSelectionChange, 'emit').and.callThrough();
combo.selectionMode = ComboSelectionMode.single;

combo.selectItems(['Michigan']);
expect(combo.selectedItems().length).toBe(1);
expect(combo.selectedItems()).toEqual(['Michigan']);
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(1);

combo.selectItems(['Wisconsin']);
expect(combo.selectedItems().length).toBe(1);
expect(combo.selectedItems()).toEqual(['Wisconsin']);
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(2);
});
it('should select only the last item when calling selectAllItems and selection mode === single', () => {
combo.selectAllItems();

expect(combo.selectedItems().length).toBe(51);
const lastItem = combo.selectedItems()[combo.selectedItems().length - 1];
expect(lastItem).toBe('Washington');

combo.selectionMode = ComboSelectionMode.single;
expect(combo.selectedItems().length).toBe(1);
expect(combo.selectedItems()[0]).toBe('Washington');
});
});
describe('Grouping tests: ', () => {
configureTestSuite();
Expand Down Expand Up @@ -2406,8 +2453,8 @@ describe('igxCombo', () => {
const searchInput = fixture.debugElement.query(By.css(CSS_CLASS_SEARCHINPUT));

const verifyFilteredItems = (inputValue: string,
expectedDropdownItemsNumber: number,
expectedFilteredItemsNumber: number) => {
expectedDropdownItemsNumber: number,
expectedFilteredItemsNumber: number) => {
UIInteractions.triggerInputEvent(searchInput, inputValue);
fixture.detectChanges();
dropdownList = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTAINER}`)).nativeElement;
Expand Down Expand Up @@ -2912,6 +2959,29 @@ describe('igxCombo', () => {
expect(combo.valid).toEqual(IgxComboState.INITIAL);
expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL);
}));

it('should allow binding to an array of multiple items, leaving only 1 item selected, when SINGLE mode', fakeAsync(() => {
expect(combo.selectionMode).toBe(ComboSelectionMode.multiple);
combo.selectItems(['Connecticut', 'Washington']);
tick();
fixture.detectChanges();
expect(fixture.componentInstance.values).toEqual(['Connecticut', 'Washington']);

combo.selectionMode = ComboSelectionMode.single;
tick();
fixture.detectChanges();
expect(fixture.componentInstance.values).toEqual(['Washington']);
expect(combo.selectedItems()).toEqual(['Washington']);

fixture.componentInstance.values = ['Connecticut', 'New Jersey'];
tick();
fixture.detectChanges();
tick();
fixture.detectChanges();

expect(combo.selectedItems()).toEqual(['New Jersey']);
expect(fixture.componentInstance.values).toEqual(['New Jersey']);
}));
});
});
describe('Display density', () => {
Expand Down
64 changes: 59 additions & 5 deletions projects/igniteui-angular/src/lib/combo/combo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
import { FormsModule, ReactiveFormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, AbstractControl } from '@angular/forms';
import { IgxCheckboxModule } from '../checkbox/checkbox.component';
import { IgxSelectionAPIService } from '../core/selection';
import { cloneArray, IBaseEventArgs, IBaseCancelableBrowserEventArgs, IBaseCancelableEventArgs, CancelableEventArgs } from '../core/utils';
import { cloneArray, IBaseEventArgs, IBaseCancelableBrowserEventArgs,
IBaseCancelableEventArgs, CancelableEventArgs,mkenum } from '../core/utils';
import { IgxStringFilteringOperand, IgxBooleanFilteringOperand } from '../data-operations/filtering-condition';
import { FilteringLogic } from '../data-operations/filtering-expression.interface';
import { IgxForOfModule, IForOfState, IgxForOfDirective } from '../directives/for-of/for_of.directive';
Expand Down Expand Up @@ -82,6 +83,14 @@ export enum IgxComboState {
INVALID = IgxInputState.INVALID
}

/** The selection modes that the IgxComboComponent can use */
export const ComboSelectionMode = mkenum({
single: 'single',
multiple: 'multiple',
});
export type ComboSelectionMode = (typeof ComboSelectionMode)[keyof typeof ComboSelectionMode];


/** The filtering criteria to be applied on data search */
export interface IComboFilteringOptions {
/** Defines filtering case-sensitivity */
Expand Down Expand Up @@ -183,6 +192,37 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
@Input()
public overlaySettings: OverlaySettings = null;

/**
* Specifies how the combo component handles selection.
*
* When set to single selection, the combo will allow only one of its items to be selected.
* When a new one is chosen, previous selection is erased.
*
* Multiple selection allows multiple items to be chosen.
*
* @default 'multiple'
*
* @example
* ```html
* <igx-combo [data]="comboData" selection="single" [(ngModel)]="selected">
* </igx-combo>
* ```
*/
@Input()
public get selectionMode(): ComboSelectionMode {
return this._selectionMode;
}

public set selectionMode(val: ComboSelectionMode) {
if (this._selectionMode !== val) {
this._selectionMode = val;
const selection = this.selectedItems();
if (this._selectionMode === ComboSelectionMode.single && selection.length > 1) {
this.setSelection(new Set(this.validateSelectionSize(selection)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to validate selection size. setSelection will do this for you.

Suggested change
this.setSelection(new Set(this.validateSelectionSize(selection)));
this.setSelection(new Set(selection));

}
}
}

/** @hidden @internal */
@ViewChild('inputGroup', { read: IgxInputGroupComponent, static: true }) public inputGroup: IgxInputGroupComponent;

Expand Down Expand Up @@ -891,6 +931,7 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
protected _displayKey: string;
protected _prevInputValue = '';

private _selectionMode: ComboSelectionMode = ComboSelectionMode.multiple;
private _dataType = '';
private _searchValue = '';
private _type = null;
Expand Down Expand Up @@ -1226,7 +1267,8 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
* @hidden @internal
*/
public writeValue(value: any[]): void {
const selection = Array.isArray(value) ? value : [];
let selection = Array.isArray(value) ? value : [];
selection = this.validateSelectionSize(selection, true);
const oldSelection = this.selectedItems();
this.selection.select_items(this.id, selection, true);
this.cdr.markForCheck();
Expand Down Expand Up @@ -1510,10 +1552,11 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
}

protected setSelection(newSelection: Set<any>, event?: Event): void {
const removed = diffInSets(this.selection.get(this.id), newSelection);
const added = diffInSets(newSelection, this.selection.get(this.id));
const newSelectionAsArray = Array.from(newSelection);
const newSelectionAsArray = this.validateSelectionSize(Array.from(newSelection));
const oldSelectionAsArray = Array.from(this.selection.get(this.id) || []);
const modifiedSelection = new Set(newSelectionAsArray);
const removed = diffInSets(this.selection.get(this.id), modifiedSelection);
const added = diffInSets(modifiedSelection, this.selection.get(this.id));
const displayText = this.createDisplayText(newSelectionAsArray, oldSelectionAsArray);
const args: IComboSelectionChangeEventArgs = {
newSelection: newSelectionAsArray,
Expand Down Expand Up @@ -1584,6 +1627,17 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
}));
}

private validateSelectionSize<T>(selection: T[], mutate = false): T[] {
if (this.selectionMode === ComboSelectionMode.single) {
if (mutate) {
selection.splice(0, selection.length - 1);
} else {
selection = selection.slice(selection.length - 1);
}
}
return selection;
}

private checkMatch(): void {
const displayKey = this.displayKey;
const matchFn = (e) => {
Expand Down