Skip to content

Commit 3fa30b6

Browse files
committed
feat(combo): add singleSelection input, #9832
1 parent 6b1ddcb commit 3fa30b6

File tree

4 files changed

+144
-11
lines changed

4 files changed

+144
-11
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ All notable changes for each version of this project will be documented in this
8282
- `IgxDateTimeEditor`, `IgxMask`, `IgxDatePicker`, `IgxTimePicker`, `IgxDateRangePicker`
8383
- 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.
8484

85+
- `IgxCombo`
86+
- 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).
87+
```html
88+
<igx-combo #single [data]="myData" selectionMode="single">
89+
...
90+
</igx-combo>
91+
```
92+
8593
### General
8694
- `IgxGridComponent`
8795
- The following properties are deprecated:

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

+1
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ Setting `[displayDensity]` affects the control's items' and inputs' css properti
301301
| `id` | combo id | string |
302302
| `data` | combo data source | any |
303303
| `allowCustomValue` | enables/disables combo custom value | boolean |
304+
| `selectionMode` | specified whether the combo allows single or multiple selection | string ('multiple' | 'single' ) |
304305
| `filterable` | enables/disables combo drop down filtering - enabled by default | boolean |
305306
| `showSearchCaseIcon` | defines whether the search case-sensitive icon should be displayed - disabled by default| boolean |
306307
| `valueKey` | combo value data source property | string |

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

+76-6
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import { AfterViewInit, ChangeDetectorRef, Component, Injectable, OnInit, ViewCh
22
import { TestBed, tick, fakeAsync, ComponentFixture, waitForAsync } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
5-
import { FormGroup, FormControl, Validators, FormBuilder, ReactiveFormsModule,
6-
FormsModule, NgControl, NgModel, NgForm } from '@angular/forms';
5+
import {
6+
FormGroup, FormControl, Validators, FormBuilder, ReactiveFormsModule,
7+
FormsModule, NgControl, NgModel, NgForm
8+
} from '@angular/forms';
79
import {
810
IgxComboComponent,
911
IgxComboModule,
1012
IComboSelectionChangeEventArgs,
1113
IgxComboState,
1214
IComboSearchInputEventArgs,
13-
IComboItemAdditionEvent
15+
IComboItemAdditionEvent,
16+
ComboSelectionMode
1417
} from './combo.component';
1518
import { IgxComboItemComponent } from './combo-item.component';
1619
import { IgxComboDropDownComponent } from './combo-dropdown.component';
@@ -305,7 +308,7 @@ describe('igxCombo', () => {
305308
};
306309
combo.comboInput = {
307310
nativeElement: {
308-
focus: () => {}
311+
focus: () => { }
309312
}
310313
} as any;
311314
combo.handleOpening(inputEvent);
@@ -2090,6 +2093,50 @@ describe('igxCombo', () => {
20902093
expect(combo.selectedItems().length).toEqual(0);
20912094
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(0);
20922095
});
2096+
it('should allow changing of selection mode runtime, leaving only the last element selected', () => {
2097+
spyOn(combo.onSelectionChange, 'emit').and.callThrough();
2098+
2099+
combo.selectItems(['Michigan', 'Ohio', 'Wisconsin']);
2100+
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(1);
2101+
expect(combo.selectedItems().length).toBe(3);
2102+
combo.selectionMode = ComboSelectionMode.single;
2103+
fixture.detectChanges();
2104+
expect(combo.selectedItems().length).toBe(1);
2105+
expect(combo.selectedItems()).toEqual(['Wisconsin']);
2106+
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(2);
2107+
2108+
// does not mutate the input array
2109+
const selectCall = ['Ohio', 'Wisconsin', 'Michigan'];
2110+
combo.selectItems(selectCall);
2111+
expect(combo.selectedItems().length).toBe(1);
2112+
expect(combo.selectedItems()).toEqual(['Michigan']);
2113+
expect(selectCall).toEqual(['Ohio', 'Wisconsin', 'Michigan']);
2114+
});
2115+
it('should support single selection mode, allowing only one item to be selected', () => {
2116+
spyOn(combo.onSelectionChange, 'emit').and.callThrough();
2117+
combo.selectionMode = ComboSelectionMode.single;
2118+
2119+
combo.selectItems(['Michigan']);
2120+
expect(combo.selectedItems().length).toBe(1);
2121+
expect(combo.selectedItems()).toEqual(['Michigan']);
2122+
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(1);
2123+
2124+
combo.selectItems(['Wisconsin']);
2125+
expect(combo.selectedItems().length).toBe(1);
2126+
expect(combo.selectedItems()).toEqual(['Wisconsin']);
2127+
expect(combo.onSelectionChange.emit).toHaveBeenCalledTimes(2);
2128+
});
2129+
it('should select only the last item when calling selectAllItems and selection mode === single', () => {
2130+
combo.selectAllItems();
2131+
2132+
expect(combo.selectedItems().length).toBe(51);
2133+
const lastItem = combo.selectedItems()[combo.selectedItems().length - 1];
2134+
expect(lastItem).toBe('Washington');
2135+
2136+
combo.selectionMode = ComboSelectionMode.single;
2137+
expect(combo.selectedItems().length).toBe(1);
2138+
expect(combo.selectedItems()[0]).toBe('Washington');
2139+
});
20932140
});
20942141
describe('Grouping tests: ', () => {
20952142
configureTestSuite();
@@ -2406,8 +2453,8 @@ describe('igxCombo', () => {
24062453
const searchInput = fixture.debugElement.query(By.css(CSS_CLASS_SEARCHINPUT));
24072454

24082455
const verifyFilteredItems = (inputValue: string,
2409-
expectedDropdownItemsNumber: number,
2410-
expectedFilteredItemsNumber: number) => {
2456+
expectedDropdownItemsNumber: number,
2457+
expectedFilteredItemsNumber: number) => {
24112458
UIInteractions.triggerInputEvent(searchInput, inputValue);
24122459
fixture.detectChanges();
24132460
dropdownList = fixture.debugElement.query(By.css(`.${CSS_CLASS_CONTAINER}`)).nativeElement;
@@ -2912,6 +2959,29 @@ describe('igxCombo', () => {
29122959
expect(combo.valid).toEqual(IgxComboState.INITIAL);
29132960
expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL);
29142961
}));
2962+
2963+
it('should allow binding to an array of multiple items, leaving only 1 item selected, when SINGLE mode', fakeAsync(() => {
2964+
expect(combo.selectionMode).toBe(ComboSelectionMode.multiple);
2965+
combo.selectItems(['Connecticut', 'Washington']);
2966+
tick();
2967+
fixture.detectChanges();
2968+
expect(fixture.componentInstance.values).toEqual(['Connecticut', 'Washington']);
2969+
2970+
combo.selectionMode = ComboSelectionMode.single;
2971+
tick();
2972+
fixture.detectChanges();
2973+
expect(fixture.componentInstance.values).toEqual(['Washington']);
2974+
expect(combo.selectedItems()).toEqual(['Washington']);
2975+
2976+
fixture.componentInstance.values = ['Connecticut', 'New Jersey'];
2977+
tick();
2978+
fixture.detectChanges();
2979+
tick();
2980+
fixture.detectChanges();
2981+
2982+
expect(combo.selectedItems()).toEqual(['New Jersey']);
2983+
expect(fixture.componentInstance.values).toEqual(['New Jersey']);
2984+
}));
29152985
});
29162986
});
29172987
describe('Display density', () => {

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

+59-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
import { FormsModule, ReactiveFormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, AbstractControl } from '@angular/forms';
1717
import { IgxCheckboxModule } from '../checkbox/checkbox.component';
1818
import { IgxSelectionAPIService } from '../core/selection';
19-
import { cloneArray, IBaseEventArgs, IBaseCancelableBrowserEventArgs, IBaseCancelableEventArgs, CancelableEventArgs } from '../core/utils';
19+
import { cloneArray, IBaseEventArgs, IBaseCancelableBrowserEventArgs,
20+
IBaseCancelableEventArgs, CancelableEventArgs,mkenum } from '../core/utils';
2021
import { IgxStringFilteringOperand, IgxBooleanFilteringOperand } from '../data-operations/filtering-condition';
2122
import { FilteringLogic } from '../data-operations/filtering-expression.interface';
2223
import { IgxForOfModule, IForOfState, IgxForOfDirective } from '../directives/for-of/for_of.directive';
@@ -82,6 +83,14 @@ export enum IgxComboState {
8283
INVALID = IgxInputState.INVALID
8384
}
8485

86+
/** The selection modes that the IgxComboComponent can use */
87+
export const ComboSelectionMode = mkenum({
88+
single: 'single',
89+
multiple: 'multiple',
90+
});
91+
export type ComboSelectionMode = (typeof ComboSelectionMode)[keyof typeof ComboSelectionMode];
92+
93+
8594
/** The filtering criteria to be applied on data search */
8695
export interface IComboFilteringOptions {
8796
/** Defines filtering case-sensitivity */
@@ -183,6 +192,37 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
183192
@Input()
184193
public overlaySettings: OverlaySettings = null;
185194

195+
/**
196+
* Specifies how the combo component handles selection.
197+
*
198+
* When set to single selection, the combo will allow only one of its items to be selected.
199+
* When a new one is chosen, previous selection is erased.
200+
*
201+
* Multiple selection allows multiple items to be chosen.
202+
*
203+
* @default 'multiple'
204+
*
205+
* @example
206+
* ```html
207+
* <igx-combo [data]="comboData" selection="single" [(ngModel)]="selected">
208+
* </igx-combo>
209+
* ```
210+
*/
211+
@Input()
212+
public get selectionMode(): ComboSelectionMode {
213+
return this._selectionMode;
214+
}
215+
216+
public set selectionMode(val: ComboSelectionMode) {
217+
if (this._selectionMode !== val) {
218+
this._selectionMode = val;
219+
const selection = this.selectedItems();
220+
if (this._selectionMode === ComboSelectionMode.single && selection.length > 1) {
221+
this.setSelection(new Set(this.validateSelectionSize(selection)));
222+
}
223+
}
224+
}
225+
186226
/** @hidden @internal */
187227
@ViewChild('inputGroup', { read: IgxInputGroupComponent, static: true }) public inputGroup: IgxInputGroupComponent;
188228

@@ -891,6 +931,7 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
891931
protected _displayKey: string;
892932
protected _prevInputValue = '';
893933

934+
private _selectionMode: ComboSelectionMode = ComboSelectionMode.multiple;
894935
private _dataType = '';
895936
private _searchValue = '';
896937
private _type = null;
@@ -1226,7 +1267,8 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
12261267
* @hidden @internal
12271268
*/
12281269
public writeValue(value: any[]): void {
1229-
const selection = Array.isArray(value) ? value : [];
1270+
let selection = Array.isArray(value) ? value : [];
1271+
selection = this.validateSelectionSize(selection, true);
12301272
const oldSelection = this.selectedItems();
12311273
this.selection.select_items(this.id, selection, true);
12321274
this.cdr.markForCheck();
@@ -1510,10 +1552,11 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
15101552
}
15111553

15121554
protected setSelection(newSelection: Set<any>, event?: Event): void {
1513-
const removed = diffInSets(this.selection.get(this.id), newSelection);
1514-
const added = diffInSets(newSelection, this.selection.get(this.id));
1515-
const newSelectionAsArray = Array.from(newSelection);
1555+
const newSelectionAsArray = this.validateSelectionSize(Array.from(newSelection));
15161556
const oldSelectionAsArray = Array.from(this.selection.get(this.id) || []);
1557+
const modifiedSelection = new Set(newSelectionAsArray);
1558+
const removed = diffInSets(this.selection.get(this.id), modifiedSelection);
1559+
const added = diffInSets(modifiedSelection, this.selection.get(this.id));
15171560
const displayText = this.createDisplayText(newSelectionAsArray, oldSelectionAsArray);
15181561
const args: IComboSelectionChangeEventArgs = {
15191562
newSelection: newSelectionAsArray,
@@ -1584,6 +1627,17 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas
15841627
}));
15851628
}
15861629

1630+
private validateSelectionSize<T>(selection: T[], mutate = false): T[] {
1631+
if (this.selectionMode === ComboSelectionMode.single) {
1632+
if (mutate) {
1633+
selection.splice(0, selection.length - 1);
1634+
} else {
1635+
selection = selection.slice(selection.length - 1);
1636+
}
1637+
}
1638+
return selection;
1639+
}
1640+
15871641
private checkMatch(): void {
15881642
const displayKey = this.displayKey;
15891643
const matchFn = (e) => {

0 commit comments

Comments
 (0)