Skip to content

Commit 7f715de

Browse files
feat(combo,simple-combo): add selectionChanged event (#16968)
1 parent 7762e98 commit 7f715de

File tree

8 files changed

+275
-28
lines changed

8 files changed

+275
-28
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

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

5+
## 21.2.0
6+
7+
### New Features
8+
9+
- `IgxCombo`, `IgxSimpleCombo`
10+
- Introduced the `selectionChanged` event for both components. The event is not cancelable and is emitted after the selection is committed and the component state is updated.
11+
512
## 21.1.0
613

714
### New Features

projects/igniteui-angular/combo/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ export class MyCombo {
8686
}
8787
```
8888

89+
### Selection Events
90+
91+
The `igx-combo` exposes both `selectionChanging` and `selectionChanged`.
92+
93+
- `selectionChanging` is emitted **before** a new selection state is committed and can be canceled.
94+
- If `selectionChanging` is not canceled, the component commits the final selection state and then emits `selectionChanged`.
95+
- `selectionChanged` is emitted **after** the selection completes and the component state is updated.
96+
- `selectionChanged` is not cancelable and reports the final committed selection state.
97+
8998
### Value Binding
9099

91100
If we want to use a two-way data-binding, we could just use `ngModel` like this:
@@ -330,7 +339,8 @@ When igxCombo is opened, allow custom values are enabled and add item button is
330339

331340
| Name | Description | Cancelable | Emitted with |
332341
|---------------------|-------------------------------------------------------------------------|--------------|-----------------------------------|
333-
| `selectionChanging` | Emitted when item selection is changing, before the selection completes | true | `IComboSelectionChangingEventArgs` |
342+
| `selectionChanging` | Emitted when item selection is changing, before the selection completes | true | `IComboSelectionChangingEventArgs` |
343+
| `selectionChanged` | Emitted after the selection completes and the component state has been updated | false | `IComboSelectionChangedEventArgs` |
334344
| `searchInputUpdate` | Emitted when an the search input's input event is triggered | true | `IComboSearchInputEventArgs` |
335345
| `addition` | Emitted when an item is being added to the data collection | true | `IComboItemAdditionEvent` |
336346
| `dataPreLoad` | Emitted when new chunk of data is loaded from the virtualization | false | `IForOfState` |

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,7 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh
982982

983983
public abstract dropdown: IgxComboDropDownComponent;
984984
public abstract selectionChanging: EventEmitter<any>;
985+
public abstract selectionChanged: EventEmitter<any>;
985986

986987
constructor() {
987988
onResourceChangeHandle(this.destroy$, () => {

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

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
import { By } from '@angular/platform-browser';
88
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
99
import { BehaviorSubject, Observable, firstValueFrom } from 'rxjs';
10-
import { take } from 'rxjs/operators';
1110
import { IgxSelectionAPIService } from 'igniteui-angular/core';
1211
import { IBaseCancelableBrowserEventArgs } from 'igniteui-angular/core';
1312
import { SortingDirection } from '../../../core/src/data-operations/sorting-strategy';
@@ -20,7 +19,7 @@ import { IgxComboDropDownComponent } from './combo-dropdown.component';
2019
import { IgxComboItemComponent } from './combo-item.component';
2120
import { IComboFilteringOptions, IGX_COMBO_COMPONENT } from './combo.common';
2221
import {
23-
IComboItemAdditionEvent, IComboSearchInputEventArgs, IComboSelectionChangingEventArgs, IgxComboComponent
22+
IComboItemAdditionEvent, IComboSearchInputEventArgs, IComboSelectionChangedEventArgs, IComboSelectionChangingEventArgs, IgxComboComponent
2423
} from './combo.component';
2524
import { IgxComboFooterDirective, IgxComboHeaderDirective, IgxComboItemDirective } from './combo.directives';
2625
import { IgxComboFilteringPipe, comboIgnoreDiacriticsFilter } from './combo.pipes';
@@ -658,6 +657,83 @@ describe('igxCombo', () => {
658657
});
659658
expect(selectionSpy).toHaveBeenCalledWith(expectedResults);
660659
});
660+
it('should emit selectionChanged after selectionChanging with the committed state', () => {
661+
const dropdown = jasmine.createSpyObj('IgxComboDropDownComponent', ['selectItem']);
662+
combo.ngOnInit();
663+
combo.data = data;
664+
combo.dropdown = dropdown;
665+
spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length);
666+
667+
const callOrder: string[] = [];
668+
spyOn(combo.selectionChanging, 'emit').and.callFake(() => callOrder.push('changing'));
669+
spyOn(combo.selectionChanged, 'emit').and.callFake(() => callOrder.push('changed'));
670+
671+
const selectedItems = [combo.data[1], combo.data[5]];
672+
combo.select(selectedItems);
673+
674+
expect(callOrder).toEqual(['changing', 'changed']);
675+
expect(combo.selectionChanged.emit).toHaveBeenCalledTimes(1);
676+
expect(combo.selectionChanged.emit).toHaveBeenCalledWith({
677+
oldValue: [],
678+
newValue: selectedItems,
679+
oldSelection: [],
680+
newSelection: selectedItems,
681+
added: selectedItems,
682+
removed: [],
683+
event: undefined,
684+
owner: combo,
685+
displayText: selectedItems.join(', ')
686+
} satisfies IComboSelectionChangedEventArgs);
687+
});
688+
it('should not emit selectionChanged when selectionChanging is canceled', () => {
689+
const dropdown = jasmine.createSpyObj('IgxComboDropDownComponent', ['selectItem']);
690+
combo.ngOnInit();
691+
combo.data = data;
692+
combo.dropdown = dropdown;
693+
spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length);
694+
695+
spyOn(combo.selectionChanging, 'emit').and.callFake((args: IComboSelectionChangingEventArgs) => {
696+
args.cancel = true;
697+
});
698+
spyOn(combo.selectionChanged, 'emit');
699+
700+
combo.select([combo.data[1], combo.data[5]]);
701+
702+
expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(1);
703+
expect(combo.selectionChanged.emit).not.toHaveBeenCalled();
704+
expect(combo.selection).toEqual([]);
705+
expect(combo.value).toEqual([]);
706+
});
707+
it('should emit selectionChanged with the actual committed state when selectionChanging modifies newValue', () => {
708+
const dropdown = jasmine.createSpyObj('IgxComboDropDownComponent', ['selectItem']);
709+
combo.ngOnInit();
710+
combo.data = data;
711+
combo.dropdown = dropdown;
712+
spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length);
713+
714+
spyOn(combo.selectionChanging, 'emit').and.callFake((args: IComboSelectionChangingEventArgs) => {
715+
args.newValue = [combo.data[2], combo.data[4]];
716+
args.displayText = `${combo.data[2]}, ${combo.data[4]}`;
717+
});
718+
719+
spyOn(combo.selectionChanged, 'emit');
720+
721+
combo.select([combo.data[0], combo.data[1]]);
722+
723+
expect(combo.selection).toEqual([combo.data[2], combo.data[4]]);
724+
expect(combo.value).toEqual([combo.data[2], combo.data[4]]);
725+
expect(combo.selectionChanged.emit).toHaveBeenCalledWith({
726+
oldValue: [],
727+
newValue: [combo.data[2], combo.data[4]],
728+
oldSelection: [],
729+
newSelection: [combo.data[2], combo.data[4]],
730+
added: [combo.data[2], combo.data[4]],
731+
removed: [],
732+
event: undefined,
733+
owner: combo,
734+
displayText: `${combo.data[2]}, ${combo.data[4]}`
735+
} satisfies IComboSelectionChangedEventArgs);
736+
});
661737
it('should handle select/deselect ALL items', () => {
662738
const dropdown = jasmine.createSpyObj('IgxComboDropDownComponent', ['selectItem']);
663739
combo.ngOnInit();
@@ -854,7 +930,7 @@ describe('igxCombo', () => {
854930
ComboModelBindingComponent,
855931
IgxComboBindingDataAfterInitComponent,
856932
IgxComboFormComponent,
857-
IgxComboInTemplatedFormComponent
933+
IgxComboInTemplatedFormComponent,
858934
]
859935
}).compileComponents();
860936
}));

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

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,29 @@ import { IgxInputGroupComponent, IgxInputDirective, IgxReadOnlyInputDirective, I
2222
import { IgxIconComponent } from 'igniteui-angular/icon';
2323
import { IgxDropDownItemNavigationDirective } from 'igniteui-angular/drop-down';
2424

25-
/** Event emitted when an igx-combo's selection is changing */
26-
export interface IComboSelectionChangingEventArgs extends IBaseCancelableEventArgs {
27-
/** An array containing the values that are currently selected */
25+
/** Event emitted when the Combo's selection has changed */
26+
export interface IComboSelectionChangedEventArgs extends IBaseEventArgs {
27+
/** An array containing the old selection values */
2828
oldValue: any[];
29-
/** An array containing the values that will be selected after this event */
29+
/** An array containing the new selection items */
3030
newValue: any[];
31-
/** An array containing the items that are currently selected */
31+
/** An array containing the old selection items */
3232
oldSelection: any[];
33-
/** An array containing the items that will be selected after this event */
33+
/** An array containing the new selection items */
3434
newSelection: any[];
35-
/** An array containing the items that will be added to the selection (if any) */
35+
/** An array containing the items added to the selection (if any) */
3636
added: any[];
37-
/** An array containing the items that will be removed from the selection (if any) */
37+
/** An array containing the items removed from the selection (if any) */
3838
removed: any[];
39-
/** The text that will be displayed in the combo text box */
39+
/** The display text of the combo text box */
4040
displayText: string;
4141
/** The user interaction that triggered the selection change */
4242
event?: Event;
4343
}
4444

45+
/** Event emitted when the Combo's selection is changing */
46+
export interface IComboSelectionChangingEventArgs extends IComboSelectionChangedEventArgs, IBaseCancelableEventArgs {}
47+
4548
/** Event emitted when the igx-combo's search input changes */
4649
export interface IComboSearchInputEventArgs extends IBaseCancelableEventArgs {
4750
/** The text that has been typed into the search input */
@@ -125,7 +128,6 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
125128
@Input({ transform: booleanAttribute })
126129
public autoFocusSearch = true;
127130

128-
129131
/**
130132
* Defines the placeholder value for the combo dropdown search field
131133
*
@@ -154,6 +156,16 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
154156
@Output()
155157
public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
156158

159+
/**
160+
* Emitted when item selection is changed, after the selection completes
161+
*
162+
* ```html
163+
* <igx-combo (selectionChanged)='handleSelection()'></igx-combo>
164+
* ```
165+
*/
166+
@Output()
167+
public selectionChanged = new EventEmitter<IComboSelectionChangedEventArgs>();
168+
157169
/** @hidden @internal */
158170
@ViewChild(IgxComboDropDownComponent, { static: true })
159171
public dropdown: IgxComboDropDownComponent;
@@ -423,7 +435,19 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
423435
} else {
424436
this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
425437
}
426-
this._onChangeCallback(args.newValue);
438+
this._onChangeCallback(this.value);
439+
const changedArgs: IComboSelectionChangedEventArgs = {
440+
newValue: this.value,
441+
oldValue,
442+
newSelection: this.selection,
443+
oldSelection,
444+
added: this.convertKeysToItems(diffInSets(new Set(this.value), new Set(oldValue))),
445+
removed: this.convertKeysToItems(diffInSets(new Set(oldValue), new Set(this.value))),
446+
event,
447+
owner: this,
448+
displayText: this._displayValue
449+
};
450+
this.selectionChanged.emit(changedArgs);
427451
} else if (this.isRemote) {
428452
this.registerRemoteEntries(diffInSets(selection, currentSelection), false);
429453
}

projects/igniteui-angular/simple-combo/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ export class MyCombo {
8585
}
8686
```
8787

88+
### Selection Events
89+
90+
The `igx-simple-combo` exposes both `selectionChanging` and `selectionChanged`.
91+
92+
- `selectionChanging` is emitted **before** the selection is committed and can be canceled.
93+
- `selectionChanged` is emitted **after** the selection is committed and the component state is updated.
94+
- `selectionChanged` is not cancelable and always reports the final committed single selection state.
95+
8896
### Value Binding
8997

9098
If we want to use a two-way data-binding, we could just use `ngModel` like this:
@@ -305,7 +313,8 @@ When the combo is opened, allow custom values are enabled and add item button is
305313
### Events
306314
| Name | Description | Cancelable | Parameters |
307315
|------------------ |-------------------------------------------------------------------------|------------- |-----------------------------------------|
308-
| `selectionChanging` | Emitted when item selection is changing, before the selection completes | true | { oldSelection: `any`, newSelection: `any`, displayText: `string`, owner: `IgxSimpleComboComponent` } |
316+
| `selectionChanging` | Emitted when item selection is changing, before the selection completes | true | { oldValue: `any`, newValue: `any`, oldSelection: `any`, newSelection: `any`, displayText: `string`, owner: `IgxSimpleComboComponent` } |
317+
| `selectionChanged` | Emitted after the selection completes and the component state has been updated | false | { oldValue: `any`, newValue: `any`, oldSelection: `any`, newSelection: `any`, displayText: `string`, owner: `IgxSimpleComboComponent` } |
309318
| `searchInputUpdate` | Emitted when an the search input's input event is triggered | true | `IComboSearchInputEventArgs` |
310319
| `addition` | Emitted when an item is being added to the data collection | false | { oldCollection: `Array<any>`, addedItem: `<any>`, newCollection: `Array<any>`, owner: `IgxSimpleComboComponent` }|
311320
| `onDataPreLoad` | Emitted when new chunk of data is loaded from the virtualization | false | `IForOfState` |

0 commit comments

Comments
 (0)