Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
64e5b77
feat(simple-combo): add selectionChanged event
georgianastasov Feb 25, 2026
5e5da00
Merge branch 'master' into ganastasov/feat-16655-master
georgianastasov Feb 25, 2026
ac1e53b
Merge branch 'master' into ganastasov/feat-16655-master
georgianastasov Feb 25, 2026
5e42597
feat(simple-combo): add cancelable selectionChanged
georgianastasov Mar 4, 2026
9eb3313
feat(combo): add cancelable selectionChanged
georgianastasov Mar 4, 2026
cb7bd7e
Merge branch 'master' into ganastasov/feat-16655-master
georgianastasov Mar 4, 2026
c2f8973
fix(combo): improve selection change handling with array equality check
georgianastasov Mar 4, 2026
bc81ba8
Merge branch 'ganastasov/feat-16655-master' of https://github.com/Ign…
georgianastasov Mar 4, 2026
9b7fbcf
chore(changelog): include selectionChanged event for combo and simple…
georgianastasov Mar 4, 2026
78fbd7c
fix(combo): fix selection handling with previous state
georgianastasov Mar 4, 2026
09199c1
feat(simple-combo/combo): revert previous changes
georgianastasov Mar 6, 2026
90b7fdf
test(simple-combo/combo): test scenarios for selectionChanged event l…
georgianastasov Mar 6, 2026
be7adfe
feat(simple-combo/combo): add selectionChanged event
georgianastasov Mar 6, 2026
294204e
fix(changelog): clarify details of selectionChanged event
georgianastasov Mar 9, 2026
c89ebae
Merge branch 'master' into ganastasov/feat-16655-master
georgianastasov Mar 9, 2026
fbeb987
Merge branch 'master' into ganastasov/feat-16655-master
wnvko Mar 10, 2026
6891cd4
feat(simple-combo/combo): document selection change events in README
georgianastasov Mar 10, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

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

## 21.2.0

### New Features

- `IgxCombo`, `IgxSimpleCombo`
- 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.

## 21.1.0

### New Features
Expand Down
13 changes: 12 additions & 1 deletion projects/igniteui-angular/combo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ export class MyCombo {
}
```

### Selection Events

The `igx-combo` exposes both `selectionChanging` and `selectionChanged`.

- `selectionChanging` is emitted **before** a new selection state is committed and can be canceled.
- If `selectionChanging` is not canceled, the component commits the final selection state and then emits `selectionChanged`.
- `selectionChanged` is emitted **after** the selection completes and the component state is updated.
- When the combo is used with `ngModel` or Angular forms, `selectionChanged` is emitted after the value change callback is invoked.
- `selectionChanged` is not cancelable and reports the final committed selection state.

### Value Binding

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

| Name | Description | Cancelable | Emitted with |
|---------------------|-------------------------------------------------------------------------|--------------|-----------------------------------|
| `selectionChanging` | Emitted when item selection is changing, before the selection completes | true | `IComboSelectionChangingEventArgs` |
| `selectionChanging` | Emitted when item selection is changing, before the selection completes | true | `IComboSelectionChangingEventArgs` |
| `selectionChanged` | Emitted after the selection completes and the component state has been updated | false | `IComboSelectionChangedEventArgs` |
| `searchInputUpdate` | Emitted when an the search input's input event is triggered | true | `IComboSearchInputEventArgs` |
| `addition` | Emitted when an item is being added to the data collection | true | `IComboItemAdditionEvent` |
| `dataPreLoad` | Emitted when new chunk of data is loaded from the virtualization | false | `IForOfState` |
Expand Down
1 change: 1 addition & 0 deletions projects/igniteui-angular/combo/src/combo/combo.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,7 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh

public abstract dropdown: IgxComboDropDownComponent;
public abstract selectionChanging: EventEmitter<any>;
public abstract selectionChanged: EventEmitter<any>;

constructor() {
onResourceChangeHandle(this.destroy$, () => {
Expand Down
139 changes: 136 additions & 3 deletions projects/igniteui-angular/combo/src/combo/combo.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { BehaviorSubject, Observable, firstValueFrom } from 'rxjs';
import { take } from 'rxjs/operators';
import { IgxSelectionAPIService } from 'igniteui-angular/core';
import { IBaseCancelableBrowserEventArgs } from 'igniteui-angular/core';
import { SortingDirection } from '../../../core/src/data-operations/sorting-strategy';
Expand All @@ -20,7 +19,7 @@ import { IgxComboDropDownComponent } from './combo-dropdown.component';
import { IgxComboItemComponent } from './combo-item.component';
import { IComboFilteringOptions, IGX_COMBO_COMPONENT } from './combo.common';
import {
IComboItemAdditionEvent, IComboSearchInputEventArgs, IComboSelectionChangingEventArgs, IgxComboComponent
IComboItemAdditionEvent, IComboSearchInputEventArgs, IComboSelectionChangedEventArgs, IComboSelectionChangingEventArgs, IgxComboComponent
} from './combo.component';
import { IgxComboFooterDirective, IgxComboHeaderDirective, IgxComboItemDirective } from './combo.directives';
import { IgxComboFilteringPipe, comboIgnoreDiacriticsFilter } from './combo.pipes';
Expand Down Expand Up @@ -658,6 +657,83 @@ describe('igxCombo', () => {
});
expect(selectionSpy).toHaveBeenCalledWith(expectedResults);
});
it('should emit selectionChanged after selectionChanging with the committed state', () => {
const dropdown = jasmine.createSpyObj('IgxComboDropDownComponent', ['selectItem']);
combo.ngOnInit();
combo.data = data;
combo.dropdown = dropdown;
spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length);

const callOrder: string[] = [];
spyOn(combo.selectionChanging, 'emit').and.callFake(() => callOrder.push('changing'));
spyOn(combo.selectionChanged, 'emit').and.callFake(() => callOrder.push('changed'));

const selectedItems = [combo.data[1], combo.data[5]];
combo.select(selectedItems);

expect(callOrder).toEqual(['changing', 'changed']);
expect(combo.selectionChanged.emit).toHaveBeenCalledTimes(1);
expect(combo.selectionChanged.emit).toHaveBeenCalledWith({
oldValue: [],
newValue: selectedItems,
oldSelection: [],
newSelection: selectedItems,
added: selectedItems,
removed: [],
event: undefined,
owner: combo,
displayText: selectedItems.join(', ')
});
});
it('should not emit selectionChanged when selectionChanging is canceled', () => {
const dropdown = jasmine.createSpyObj('IgxComboDropDownComponent', ['selectItem']);
combo.ngOnInit();
combo.data = data;
combo.dropdown = dropdown;
spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length);

spyOn(combo.selectionChanging, 'emit').and.callFake((args: IComboSelectionChangingEventArgs) => {
args.cancel = true;
});
spyOn(combo.selectionChanged, 'emit');

combo.select([combo.data[1], combo.data[5]]);

expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(1);
expect(combo.selectionChanged.emit).not.toHaveBeenCalled();
expect(combo.selection).toEqual([]);
expect(combo.value).toEqual([]);
});
it('should emit selectionChanged with the actual committed state when selectionChanging modifies newValue', () => {
const dropdown = jasmine.createSpyObj('IgxComboDropDownComponent', ['selectItem']);
combo.ngOnInit();
combo.data = data;
combo.dropdown = dropdown;
spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length);

spyOn(combo.selectionChanging, 'emit').and.callFake((args: IComboSelectionChangingEventArgs) => {
args.newValue = [combo.data[2], combo.data[4]];
args.displayText = `${combo.data[2]}, ${combo.data[4]}`;
});

spyOn(combo.selectionChanged, 'emit');

combo.select([combo.data[0], combo.data[1]]);

expect(combo.selection).toEqual([combo.data[2], combo.data[4]]);
expect(combo.value).toEqual([combo.data[2], combo.data[4]]);
expect(combo.selectionChanged.emit).toHaveBeenCalledWith({
oldValue: [],
newValue: [combo.data[2], combo.data[4]],
oldSelection: [],
newSelection: [combo.data[2], combo.data[4]],
added: [combo.data[2], combo.data[4]],
removed: [],
event: undefined,
owner: combo,
displayText: `${combo.data[2]}, ${combo.data[4]}`
});
});
it('should handle select/deselect ALL items', () => {
const dropdown = jasmine.createSpyObj('IgxComboDropDownComponent', ['selectItem']);
combo.ngOnInit();
Expand Down Expand Up @@ -854,7 +930,8 @@ describe('igxCombo', () => {
ComboModelBindingComponent,
IgxComboBindingDataAfterInitComponent,
IgxComboFormComponent,
IgxComboInTemplatedFormComponent
IgxComboInTemplatedFormComponent,
ComboSelectionChangedNgModelOrderComponent
]
}).compileComponents();
}));
Expand Down Expand Up @@ -1507,6 +1584,23 @@ describe('igxCombo', () => {
fixture.detectChanges();
expect(fixture.componentInstance.selectedItems).toEqual([...data].splice(1, 3));
}));
it('should emit selectionChanged after ngModelChange', fakeAsync(() => {
fixture = TestBed.createComponent(ComboSelectionChangedNgModelOrderComponent);
fixture.detectChanges();
tick();

const host = fixture.componentInstance;
combo = host.combo;

combo.select([1]);
fixture.detectChanges();
tick();

expect(host.eventLog).toEqual(['ngModelChange', 'selectionChanged']);
expect(host.comboValue).toEqual([1]);
expect(host.changedArgs.newValue).toEqual([1]);
expect(host.changedArgs.newSelection).toEqual([host.items[1]]);
}));
});
describe('Dropdown tests: ', () => {
describe('complex data dropdown: ', () => {
Expand Down Expand Up @@ -3991,3 +4085,42 @@ export class ComboWithIdComponent {
];
}
}

@Component({
template: `
<igx-combo
#combo
[data]="items"
[displayKey]="'text'"
[valueKey]="'id'"
[ngModel]="comboValue"
(ngModelChange)="onNgModelChange($event)"
(selectionChanged)="onSelectionChanged($event)">
</igx-combo>
`,
imports: [IgxComboComponent, FormsModule]
})
class ComboSelectionChangedNgModelOrderComponent {
@ViewChild('combo', { read: IgxComboComponent, static: true })
public combo: IgxComboComponent;

public items = [
{ id: 0, text: 'One' },
{ id: 1, text: 'Two' },
{ id: 2, text: 'Three' }
];

public comboValue: number[] = [];
public eventLog: string[] = [];
public changedArgs: IComboSelectionChangedEventArgs;

public onNgModelChange(value: number[]): void {
this.eventLog.push('ngModelChange');
this.comboValue = value;
}

public onSelectionChanged(args: IComboSelectionChangedEventArgs): void {
this.eventLog.push('selectionChanged');
this.changedArgs = args;
}
}
46 changes: 35 additions & 11 deletions projects/igniteui-angular/combo/src/combo/combo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,29 @@ import { IgxInputGroupComponent, IgxInputDirective, IgxReadOnlyInputDirective, I
import { IgxIconComponent } from 'igniteui-angular/icon';
import { IgxDropDownItemNavigationDirective } from 'igniteui-angular/drop-down';

/** Event emitted when an igx-combo's selection is changing */
export interface IComboSelectionChangingEventArgs extends IBaseCancelableEventArgs {
/** An array containing the values that are currently selected */
/** Event emitted when an igx-combo's selection has been changed */
export interface IComboSelectionChangedEventArgs extends IBaseEventArgs {
/** An array containing the values that were previously selected */
oldValue: any[];
/** An array containing the values that will be selected after this event */
/** An array containing the values that are currently selected */
newValue: any[];
/** An array containing the items that are currently selected */
/** An array containing the items that were previously selected */
oldSelection: any[];
/** An array containing the items that will be selected after this event */
/** An array containing the items that are currently selected */
newSelection: any[];
/** An array containing the items that will be added to the selection (if any) */
/** An array containing the items that were added to the selection (if any) */
added: any[];
/** An array containing the items that will be removed from the selection (if any) */
/** An array containing the items that were removed from the selection (if any) */
removed: any[];
/** The text that will be displayed in the combo text box */
/** The text that is displayed in the combo text box */
displayText: string;
/** The user interaction that triggered the selection change */
event?: Event;
}

/** Event emitted when an igx-combo's selection is changing */
export interface IComboSelectionChangingEventArgs extends IComboSelectionChangedEventArgs, IBaseCancelableEventArgs {}

/** Event emitted when the igx-combo's search input changes */
export interface IComboSearchInputEventArgs extends IBaseCancelableEventArgs {
/** The text that has been typed into the search input */
Expand Down Expand Up @@ -125,7 +128,6 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
@Input({ transform: booleanAttribute })
public autoFocusSearch = true;


/**
* Defines the placeholder value for the combo dropdown search field
*
Expand Down Expand Up @@ -154,6 +156,16 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
@Output()
public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();

/**
* Emitted when item selection is changed, after the selection completes
*
* ```html
* <igx-combo (selectionChanged)='handleSelection()'></igx-combo>
* ```
*/
@Output()
public selectionChanged = new EventEmitter<IComboSelectionChangedEventArgs>();

/** @hidden @internal */
@ViewChild(IgxComboDropDownComponent, { static: true })
public dropdown: IgxComboDropDownComponent;
Expand Down Expand Up @@ -423,7 +435,19 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie
} else {
this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
}
this._onChangeCallback(args.newValue);
this._onChangeCallback(this.value);
const changedArgs: IComboSelectionChangedEventArgs = {
newValue: this.value,
oldValue,
newSelection: this.selection,
oldSelection,
added: this.convertKeysToItems(diffInSets(new Set(this.value), new Set(oldValue))),
removed: this.convertKeysToItems(diffInSets(new Set(oldValue), new Set(this.value))),
event,
owner: this,
displayText: this._displayValue
};
this.selectionChanged.emit(changedArgs);
} else if (this.isRemote) {
this.registerRemoteEntries(diffInSets(selection, currentSelection), false);
}
Expand Down
12 changes: 11 additions & 1 deletion projects/igniteui-angular/simple-combo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ export class MyCombo {
}
```

### Selection Events

The `igx-simple-combo` exposes both `selectionChanging` and `selectionChanged`.

- `selectionChanging` is emitted **before** the selection is committed and can be canceled.
- `selectionChanged` is emitted **after** the selection is committed and the component state is updated.
- When the combo is used with `ngModel` or Angular forms, `selectionChanged` is emitted after the value change callback is invoked.
- `selectionChanged` is not cancelable and always reports the final committed single selection state.

### Value Binding

If we want to use a two-way data-binding, we could just use `ngModel` like this:
Expand Down Expand Up @@ -305,7 +314,8 @@ When the combo is opened, allow custom values are enabled and add item button is
### Events
| Name | Description | Cancelable | Parameters |
|------------------ |-------------------------------------------------------------------------|------------- |-----------------------------------------|
| `selectionChanging` | Emitted when item selection is changing, before the selection completes | true | { oldSelection: `any`, newSelection: `any`, displayText: `string`, owner: `IgxSimpleComboComponent` } |
| `selectionChanging` | Emitted when item selection is changing, before the selection completes | true | { oldValue: `any`, newValue: `any`, oldSelection: `any`, newSelection: `any`, displayText: `string`, owner: `IgxSimpleComboComponent` } |
| `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` } |
| `searchInputUpdate` | Emitted when an the search input's input event is triggered | true | `IComboSearchInputEventArgs` |
| `addition` | Emitted when an item is being added to the data collection | false | { oldCollection: `Array<any>`, addedItem: `<any>`, newCollection: `Array<any>`, owner: `IgxSimpleComboComponent` }|
| `onDataPreLoad` | Emitted when new chunk of data is loaded from the virtualization | false | `IForOfState` |
Expand Down
Loading
Loading