diff --git a/CHANGELOG.md b/CHANGELOG.md index ae45bdad60b..cfa50b15860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,17 @@ All notable changes for each version of this project will be documented in this - We're working on reducing the library size - IgxRadioComponent has been reduced in half - IgxSwitchComponent has been reduced in half +- `IgxCombo` + - **Breaking Change** `IComboSelectionChangingEventArgs` properties `newSelection` and `oldSelection` have been renamed to `newValue` and `oldValue` respectively to better reflect their function. Just like Combo's `value`, those will emit either the specified property values or full data items depending on whether `valueKey` is set or not. Automatic migrations are available and will be applied on `ng update`. + - `IComboSelectionChangingEventArgs` exposes two new properties `newSelection` and `oldSelection` in place of the old ones that are no longer affected by `valueKey` and consistently emit items from Combo's `data`. + + Note: In remote data scenarios with `valueKey` set, selected items that are not currently part of the loaded data chunk will be emitted a partial item data object with the `valueKey` property. + - **Breaking Change** - `IComboSelectionChangingEventArgs` properties `added` and `removed` now always contain data items, regardless of `valueKey` being set. This aligns them with the updated `newSelection` and `oldSelection` properties, including the same limitation for remote data as described above. +- `IgxSimpleCombo` + - **Breaking Change** - `ISimpleComboSelectionChangingEventArgs` properties `newSelection` and `oldSelection` have been renamed to `newValue` and `oldValue` respectively to better reflect their function. Just like Combo's `value`, those will emit either the specified property value or full data item depending on whether `valueKey` is set or not. Automatic migrations are available and will be applied on `ng update`. + - `ISimpleComboSelectionChangingEventArgs` exposes two new properties `newSelection` and `oldSelection` in place of the old ones that are no longer affected by `valueKey` and consistently emit items from Combo's `data`. + Note: In remote data scenarios with `valueKey` set, selected items that are not currently part of the loaded data chunk will be emitted a partial item data object with the `valueKey` property. ## 16.1.4 ### New Features - `Themes`: diff --git a/projects/igniteui-angular/migrations/update-17_0_0/changes/members.json b/projects/igniteui-angular/migrations/update-17_0_0/changes/members.json new file mode 100644 index 00000000000..8ed69c50f80 --- /dev/null +++ b/projects/igniteui-angular/migrations/update-17_0_0/changes/members.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../common/schema/members-changes.schema.json", + "changes": [ + { + "member": "newSelection", + "replaceWith": "newValue", + "definedIn": [ + "IComboSelectionChangingEventArgs", + "ISimpleComboSelectionChangingEventArgs" + ] + }, + { + "member": "oldSelection", + "replaceWith": "oldValue", + "definedIn": [ + "IComboSelectionChangingEventArgs", + "ISimpleComboSelectionChangingEventArgs" + ] + } + ] +} diff --git a/projects/igniteui-angular/migrations/update-17_0_0/index.spec.ts b/projects/igniteui-angular/migrations/update-17_0_0/index.spec.ts index 972ae160f3c..2bf57f2407d 100644 --- a/projects/igniteui-angular/migrations/update-17_0_0/index.spec.ts +++ b/projects/igniteui-angular/migrations/update-17_0_0/index.spec.ts @@ -206,6 +206,66 @@ describe(`Update to ${version}`, () => { ); }); + it('Should properly rename newSelection and oldSelection property to newValue and oldValue in Combo', async () => { + pending('set up tests for migrations through lang service'); + appTree.create('/testSrc/appPrefix/component/test.component.ts', + ` + import { IgxComboComponent, IComboSelectionChangingEventArgs } from 'igniteui-angular'; + export class MyClass { + public handleSelectionChanging(e: IComboSelectionChangingEventArgs) { + const newSelection = e.newSelection; + const oldSelection = e.oldSelection; + } + } + `); + + const tree = await schematicRunner.runSchematic(migrationName, {}, appTree); + + expect( + tree.readContent('/testSrc/appPrefix/component/test.component.ts') + ).toEqual( + ` + import { IgxComboComponent, IComboSelectionChangingEventArgs } from 'igniteui-angular'; + export class MyClass { + public handleSelectionChanging(e: IComboSelectionChangingEventArgs) { + const newSelection = e.newValue; + const oldSelection = e.oldValue; + } + } + ` + ); + }); + + it('Should properly rename newSelection and oldSelection property to newValue and oldValue SimpleCombo', async () => { + pending('set up tests for migrations through lang service'); + appTree.create('/testSrc/appPrefix/component/test.component.ts', + ` + import { IgxSimpleComboComponent, ISimpleComboSelectionChangingEventArgs } from 'igniteui-angular'; + export class MyClass { + public handleSelectionChanging(e: ISimpleComboSelectionChangingEventArgs) { + const newSelection = e.newSelection; + const oldSelection = e.oldSelection; + } + } + `); + + const tree = await schematicRunner.runSchematic(migrationName, {}, appTree); + + expect( + tree.readContent('/testSrc/appPrefix/component/test.component.ts') + ).toEqual( + ` + import { IgxComboComponent, IComboSelectionChangingEventArgs } from 'igniteui-angular'; + export class MyClass { + public handleSelectionChanging(e: IComboSelectionChangingEventArgs) { + const newSelection = e.newValue; + const oldSelection = e.oldValue; + } + } + ` + ); + }); + for (const igPackage of ['igniteui-angular', '@infragistics/igniteui-angular']) { it('should move animation imports from igniteui-angular to igniteui-angular/animations', async () => { appTree.create(`/testSrc/appPrefix/component/test.component.ts`, diff --git a/projects/igniteui-angular/src/lib/combo/combo.common.ts b/projects/igniteui-angular/src/lib/combo/combo.common.ts index 828ed9f4546..37077d7d89f 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.common.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.common.ts @@ -1295,11 +1295,13 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement return keys; } - // map keys vs. filter data to retain the order of the selected items - return keys.map(key => isNaNvalue(key) - ? this.data.find(entry => isNaNvalue(entry[this.valueKey])) - : this.data.find(entry => entry[this.valueKey] === key)) - .filter(e => e !== undefined); + return keys.map(key => { + const item = isNaNvalue(key) + ? this.data.find(entry => isNaNvalue(entry[this.valueKey])) + : this.data.find(entry => entry[this.valueKey] === key); + + return item !== undefined ? item : { [this.valueKey]: key }; + }); } protected checkMatch(): void { diff --git a/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts b/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts index 209ddf6c000..50979855154 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts @@ -351,12 +351,17 @@ describe('igxCombo', () => { spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length); spyOn(combo.selectionChanging, 'emit'); + let oldValue = []; + let newValue = [combo.data[1], combo.data[5], combo.data[6]]; + let oldSelection = []; let newSelection = [combo.data[1], combo.data[5], combo.data[6]]; combo.select(newSelection); expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(1); expect(combo.selectionChanging.emit).toHaveBeenCalledWith({ + oldValue, + newValue, oldSelection, newSelection, added: newSelection, @@ -369,10 +374,14 @@ describe('igxCombo', () => { let newItem = combo.data[3]; combo.select([newItem]); + oldValue = [...newValue]; + newValue.push(newItem); oldSelection = [...newSelection]; newSelection.push(newItem); expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(2); expect(combo.selectionChanging.emit).toHaveBeenCalledWith({ + oldValue, + newValue, oldSelection, newSelection, removed: [], @@ -383,11 +392,15 @@ describe('igxCombo', () => { cancel: false }); + oldValue = [...newValue]; + newValue = [combo.data[0]]; oldSelection = [...newSelection]; newSelection = [combo.data[0]]; combo.select(newSelection, true); expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(3); expect(combo.selectionChanging.emit).toHaveBeenCalledWith({ + oldValue, + newValue, oldSelection, newSelection, removed: oldSelection, @@ -398,6 +411,8 @@ describe('igxCombo', () => { cancel: false }); + oldValue = [...newValue]; + newValue = []; oldSelection = [...newSelection]; newSelection = []; newItem = combo.data[0]; @@ -405,6 +420,8 @@ describe('igxCombo', () => { expect(combo.selection.length).toEqual(0); expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(4); expect(combo.selectionChanging.emit).toHaveBeenCalledWith({ + oldValue, + newValue, oldSelection, newSelection, removed: [combo.data[0]], @@ -428,9 +445,11 @@ describe('igxCombo', () => { spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length); const selectionSpy = spyOn(combo.selectionChanging, 'emit'); const expectedResults: IComboSelectionChangingEventArgs = { - newSelection: [combo.data[0][combo.valueKey]], + newValue: [combo.data[0][combo.valueKey]], + oldValue: [], + newSelection: [combo.data[0]], oldSelection: [], - added: [combo.data[0][combo.valueKey]], + added: [combo.data[0]], removed: [], event: undefined, owner: combo, @@ -440,11 +459,13 @@ describe('igxCombo', () => { combo.select([combo.data[0][combo.valueKey]]); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); Object.assign(expectedResults, { + newValue: [], + oldValue: [combo.data[0][combo.valueKey]], newSelection: [], - oldSelection: [combo.data[0][combo.valueKey]], + oldSelection: [combo.data[0]], added: [], displayText: '', - removed: [combo.data[0][combo.valueKey]] + removed: [combo.data[0]] }); combo.deselect([combo.data[0][combo.valueKey]]); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); @@ -461,42 +482,51 @@ describe('igxCombo', () => { combo.displayKey = 'city'; combo.dropdown = dropdown; spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length); + const selectionSpy = spyOn(combo.selectionChanging, 'emit'); + let oldSelection = []; let newSelection = [combo.data[0], combo.data[1], combo.data[2]]; - const selectionSpy = spyOn(combo.selectionChanging, 'emit'); + combo.select(newSelection.map(e => e[combo.valueKey])); const expectedResults: IComboSelectionChangingEventArgs = { - newSelection: newSelection.map(e => e[combo.valueKey]), + newValue: newSelection.map(e => e[combo.valueKey]), + oldValue: [], + newSelection: newSelection, oldSelection, - added: newSelection.map(e => e[combo.valueKey]), + added: newSelection, removed: [], event: undefined, owner: combo, displayText: `${newSelection.map(entry => entry[combo.displayKey]).join(', ')}`, cancel: false }; - combo.select(newSelection.map(e => e[combo.valueKey])); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); - oldSelection = [...newSelection].map(e => e[combo.valueKey]); + + oldSelection = [...newSelection]; newSelection = [combo.data[1], combo.data[2]]; combo.deselect([combo.data[0][combo.valueKey]]); Object.assign(expectedResults, { - newSelection: newSelection.map(e => e[combo.valueKey]), + newValue: newSelection.map(e => e[combo.valueKey]), + oldValue: oldSelection.map(e => e[combo.valueKey]), + newSelection, oldSelection, added: [], displayText: newSelection.map(e => e[combo.displayKey]).join(', '), - removed: [combo.data[0][combo.valueKey]] + removed: [combo.data[0]] }); - oldSelection = [...newSelection].map(e => e[combo.valueKey]); - newSelection = [combo.data[4], combo.data[5], combo.data[6]]; expect(selectionSpy).toHaveBeenCalledWith(expectedResults); + + oldSelection = [...newSelection]; + newSelection = [combo.data[4], combo.data[5], combo.data[6]]; + combo.select(newSelection.map(e => e[combo.valueKey]), true); Object.assign(expectedResults, { - newSelection: newSelection.map(e => e[combo.valueKey]), + newValue: newSelection.map(e => e[combo.valueKey]), + oldValue: oldSelection.map(e => e[combo.valueKey]), + newSelection, oldSelection, - added: newSelection.map(e => e[combo.valueKey]), + added: newSelection, displayText: newSelection.map(e => e[combo.displayKey]).join(', '), removed: oldSelection }); - combo.select(newSelection.map(e => e[combo.valueKey]), true); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); }); it('should handle select/deselect ALL items', () => { @@ -537,6 +567,8 @@ describe('igxCombo', () => { expect(combo.value).toEqual(data); expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(1); expect(combo.selectionChanging.emit).toHaveBeenCalledWith({ + oldValue: [], + newValue: data, oldSelection: [], newSelection: data, added: data, @@ -552,6 +584,8 @@ describe('igxCombo', () => { expect(combo.value).toEqual([]); expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(2); expect(combo.selectionChanging.emit).toHaveBeenCalledWith({ + oldValue: data, + newValue: [], oldSelection: data, newSelection: [], added: [], @@ -572,7 +606,7 @@ describe('igxCombo', () => { combo.data = data; combo.dropdown = dropdown; spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length); - spyOn(combo.selectionChanging, 'emit').and.callFake((event: IComboSelectionChangingEventArgs) => event.newSelection = []); + spyOn(combo.selectionChanging, 'emit').and.callFake((event: IComboSelectionChangingEventArgs) => event.newValue = []); // No items are initially selected expect(combo.selection).toEqual([]); // Select the first 5 items @@ -1330,14 +1364,14 @@ describe('igxCombo', () => { combo.select([combo.data[7][combo.valueKey]]); expect(combo.displayValue).toEqual([combo.data[7][combo.displayKey]]); }); - it('should add selected items to the input when data is loaded', async() => { + it('should add selected items to the input when data is loaded', async () => { expect(combo.selection.length).toEqual(0); expect(combo.value).toEqual([]); // current combo data - id: 0 - 9 - // select item that is not present in the data source yet + // select item that is not present in the data source yet should be added as partial item combo.select([9, 19]); - expect(combo.selection.length).toEqual(1); + expect(combo.selection.length).toEqual(2); expect(combo.value.length).toEqual(2); const firstItem = combo.data[combo.data.length - 1]; @@ -1353,6 +1387,47 @@ describe('igxCombo', () => { const secondItem = combo.data[combo.data.length - 1]; expect(combo.displayValue).toEqual([`${firstItem[combo.displayKey]}`, `${secondItem[combo.displayKey]}`]); }); + it('should fire selectionChanging event with partial data for items out of view', async () => { + const selectionSpy = spyOn(combo.selectionChanging, 'emit').and.callThrough(); + const valueKey = combo.valueKey; + + combo.toggle(); + combo.select([combo.data[0][valueKey], combo.data[1][valueKey]]); + + const expectedResults: IComboSelectionChangingEventArgs = { + newValue: [combo.data[0][valueKey], combo.data[1][valueKey]], + oldValue: [], + newSelection: [combo.data[0], combo.data[1]], + oldSelection: [], + added: [combo.data[0], combo.data[1]], + removed: [], + event: undefined, + owner: combo, + displayText: `${combo.data[0][combo.displayKey]}, ${combo.data[1][combo.displayKey]}`, + cancel: false + }; + expect(selectionSpy).toHaveBeenCalledWith(expectedResults); + + // Scroll selected items out of view + combo.virtualScrollContainer.scrollTo(40); + await wait(); + fixture.detectChanges(); + combo.select([combo.data[0][valueKey], combo.data[1][valueKey]]); + Object.assign(expectedResults, { + newValue: [0, 1, 31, 32], + oldValue: [0, 1], + newSelection: [{[valueKey]: 0}, {[valueKey]: 1}, combo.data[0], combo.data[1]], + oldSelection: [{[valueKey]: 0}, {[valueKey]: 1}], + added: [combo.data[0], combo.data[1]], + removed: [], + event: undefined, + owner: combo, + displayText: `Product 0, Product 1, Product 31, Product 32`, + cancel: false + }); + + expect(selectionSpy).toHaveBeenCalledWith(expectedResults); + }); }); describe('Binding to ngModel tests: ', () => { let component: ComboModelBindingComponent; @@ -1975,9 +2050,11 @@ describe('igxCombo', () => { expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(1); expect(combo.selectionChanging.emit).toHaveBeenCalledWith( { - newSelection: [selectedItem_1.value[combo.valueKey]], + newValue: [selectedItem_1.value[combo.valueKey]], + oldValue: [], + newSelection: [selectedItem_1.value], oldSelection: [], - added: [selectedItem_1.value[combo.valueKey]], + added: [selectedItem_1.value], removed: [], event: UIInteractions.getMouseEvent('click'), owner: combo, @@ -1994,9 +2071,11 @@ describe('igxCombo', () => { expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(2); expect(combo.selectionChanging.emit).toHaveBeenCalledWith( { - newSelection: [selectedItem_1.value[combo.valueKey], selectedItem_2.value[combo.valueKey]], - oldSelection: [selectedItem_1.value[combo.valueKey]], - added: [selectedItem_2.value[combo.valueKey]], + newValue: [selectedItem_1.value[combo.valueKey], selectedItem_2.value[combo.valueKey]], + oldValue: [selectedItem_1.value[combo.valueKey]], + newSelection: [selectedItem_1.value, selectedItem_2.value], + oldSelection: [selectedItem_1.value], + added: [selectedItem_2.value], removed: [], event: UIInteractions.getMouseEvent('click'), owner: combo, @@ -2013,10 +2092,12 @@ describe('igxCombo', () => { expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(3); expect(combo.selectionChanging.emit).toHaveBeenCalledWith( { - newSelection: [selectedItem_2.value[combo.valueKey]], - oldSelection: [selectedItem_1.value[combo.valueKey], selectedItem_2.value[combo.valueKey]], + newValue: [selectedItem_2.value[combo.valueKey]], + oldValue: [selectedItem_1.value[combo.valueKey], selectedItem_2.value[combo.valueKey]], + newSelection: [selectedItem_2.value], + oldSelection: [selectedItem_1.value, selectedItem_2.value], added: [], - removed: [unselectedItem.value[combo.valueKey]], + removed: [unselectedItem.value], event: UIInteractions.getMouseEvent('click'), owner: combo, displayText: selectedItem_2.value[combo.valueKey], diff --git a/projects/igniteui-angular/src/lib/combo/combo.component.ts b/projects/igniteui-angular/src/lib/combo/combo.component.ts index 857b9f0aa7f..ef632f5f99a 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.ts @@ -32,12 +32,16 @@ import { IgxInputDirective } from '../directives/input/input.directive'; /** Event emitted when an igx-combo's selection is changing */ export interface IComboSelectionChangingEventArgs extends IBaseCancelableEventArgs { /** An array containing the values that are currently selected */ - oldSelection: any[]; + oldValue: any[]; /** An array containing the values that will be selected after this event */ + newValue: any[]; + /** An array containing the items that are currently selected */ + oldSelection: any[]; + /** An array containing the items that will be selected after this event */ newSelection: any[]; - /** An array containing the values that will be added to the selection (if any) */ + /** An array containing the items that will be added to the selection (if any) */ added: any[]; - /** An array containing the values that will be removed from the selection (if any) */ + /** An array containing the items that will be removed from the selection (if any) */ removed: any[]; /** The text that will be displayed in the combo text box */ displayText: string; @@ -427,15 +431,20 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie } } - protected setSelection(newSelection: Set, event?: Event): void { - const removed = diffInSets(this.selectionService.get(this.id), newSelection); - const added = diffInSets(newSelection, this.selectionService.get(this.id)); - const newSelectionAsArray = Array.from(newSelection); - const oldSelectionAsArray = Array.from(this.selectionService.get(this.id) || []); - const displayText = this.createDisplayText(this.convertKeysToItems(newSelectionAsArray), oldSelectionAsArray); + protected setSelection(selection: Set, event?: Event): void { + const currentSelection = this.selectionService.get(this.id); + const removed = this.convertKeysToItems(diffInSets(currentSelection, selection)); + const added = this.convertKeysToItems(diffInSets(selection, currentSelection)); + const newValue = Array.from(selection); + const oldValue = Array.from(currentSelection || []); + const newSelection = this.convertKeysToItems(newValue); + const oldSelection = this.convertKeysToItems(oldValue); + const displayText = this.createDisplayText(this.convertKeysToItems(newValue), oldValue); const args: IComboSelectionChangingEventArgs = { - newSelection: newSelectionAsArray, - oldSelection: oldSelectionAsArray, + newValue, + oldValue, + newSelection, + oldSelection, added, removed, event, @@ -445,16 +454,16 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie }; this.selectionChanging.emit(args); if (!args.cancel) { - this.selectionService.select_items(this.id, args.newSelection, true); - this._value = args.newSelection; + this.selectionService.select_items(this.id, args.newValue, true); + this._value = args.newValue; if (displayText !== args.displayText) { this._displayValue = this._displayText = args.displayText; } else { this._displayValue = this.createDisplayText(this.selection, args.oldSelection); } - this._onChangeCallback(args.newSelection); + this._onChangeCallback(args.newValue); } else if (this.isRemote) { - this.registerRemoteEntries(args.added, false); + this.registerRemoteEntries(diffInSets(selection, currentSelection), false); } } diff --git a/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.spec.ts b/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.spec.ts index 4e7a1f2ff11..4a53c014450 100644 --- a/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.spec.ts +++ b/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.spec.ts @@ -49,6 +49,8 @@ describe('IgxSimpleCombo', () => { let fixture: ComponentFixture; let combo: IgxSimpleComboComponent; let input: DebugElement; + let reactiveForm: NgForm; + let reactiveControl: any; configureTestSuite(); @@ -230,6 +232,8 @@ describe('IgxSimpleCombo', () => { combo.select(combo.data[1]); expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(1); expect(combo.selectionChanging.emit).toHaveBeenCalledWith({ + oldValue: undefined, + newValue: newSelection[0], oldSelection, newSelection: newSelection[0], owner: combo, @@ -242,6 +246,8 @@ describe('IgxSimpleCombo', () => { combo.select(combo.data[0]); expect(combo.selectionChanging.emit).toHaveBeenCalledTimes(2); expect(combo.selectionChanging.emit).toHaveBeenCalledWith({ + oldValue: oldSelection[0], + newValue: newSelection[0], oldSelection: oldSelection[0], newSelection: newSelection[0], owner: combo, @@ -262,7 +268,9 @@ describe('IgxSimpleCombo', () => { spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length); const selectionSpy = spyOn(combo.selectionChanging, 'emit'); const expectedResults: ISimpleComboSelectionChangingEventArgs = { - newSelection: combo.data[0][combo.valueKey], + newValue: combo.data[0][combo.valueKey], + oldValue: undefined, + newSelection: combo.data[0], oldSelection: undefined, owner: combo, displayText: `${combo.data[0][combo.displayKey]}`, @@ -274,8 +282,10 @@ describe('IgxSimpleCombo', () => { combo.select(combo.data[0][combo.valueKey]); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); Object.assign(expectedResults, { + newValue: undefined, + oldValue: combo.data[0][combo.valueKey], newSelection: undefined, - oldSelection: combo.data[0][combo.valueKey], + oldSelection: combo.data[0], displayText: '' }); combo.deselect(); @@ -291,7 +301,7 @@ describe('IgxSimpleCombo', () => { combo.data = data; combo.dropdown = dropdown; spyOnProperty(combo, 'totalItemCount').and.returnValue(combo.data.length); - spyOn(combo.selectionChanging, 'emit').and.callFake((event: IComboSelectionChangingEventArgs) => event.newSelection = []); + spyOn(combo.selectionChanging, 'emit').and.callFake((event: IComboSelectionChangingEventArgs) => event.newValue = []); const comboInput = jasmine.createSpyObj('IgxInputDirective', ['value']); combo.comboInput = comboInput; // No items are initially selected @@ -1785,23 +1795,30 @@ describe('IgxSimpleCombo', () => { fixture = TestBed.createComponent(IgxSimpleComboInReactiveFormComponent); fixture.detectChanges(); combo = fixture.componentInstance.reactiveCombo; + reactiveForm = fixture.componentInstance.reactiveForm; + reactiveControl = reactiveForm.form.controls['comboValue']; }); - it('should not select null, undefined and empty string in a reactive form with required', () => { + it('should not select null, undefined and empty string in a reactive form with required', fakeAsync(() => { // array of objects combo.data = [ { field: '0', value: 0 }, { field: 'false', value: false }, - { field: '', value: '' }, + { field: 'empty string', value: ''}, { field: 'null', value: null }, { field: 'NaN', value: NaN }, { field: 'undefined', value: undefined }, ]; + reactiveForm.resetForm(); + fixture.detectChanges(); + expect(combo.displayValue).toEqual([]); expect(combo.selection).toEqual([]); expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INITIAL); expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); // empty string combo.open(); @@ -1815,6 +1832,8 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); // null combo.open(); @@ -1828,6 +1847,8 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); // undefined combo.open(); @@ -1841,19 +1862,23 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); // primitive data - undefined is not displayed in the dropdown combo.valueKey = undefined; combo.displayKey = undefined; - combo.data = [ 0, false, '', null, NaN, undefined]; + combo.data = [0, false, '', null, NaN, undefined]; - fixture.componentInstance.reactiveForm.resetForm(); + reactiveForm.resetForm(); fixture.detectChanges(); expect(combo.displayValue).toEqual([]); expect(combo.selection).toEqual([]); expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INITIAL); expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); // empty string combo.open(); @@ -1867,6 +1892,8 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); // null combo.open(); @@ -1880,23 +1907,30 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); - }); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); + })); it('should not select null, undefined and empty string with "writeValue" method in a reactive form with required', () => { // array of objects combo.data = [ { field: '0', value: 0 }, { field: 'false', value: false }, - { field: '', value: '' }, + { field: 'empty string', value: '' }, { field: 'null', value: null }, { field: 'NaN', value: NaN }, { field: 'undefined', value: undefined }, ]; + reactiveForm.resetForm(); + fixture.detectChanges(); + expect(combo.displayValue).toEqual([]); expect(combo.selection).toEqual([]); expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INITIAL); expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); combo.onBlur(); fixture.detectChanges(); @@ -1907,6 +1941,8 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); combo.writeValue(''); expect(combo.displayValue).toEqual([]); @@ -1914,6 +1950,8 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); combo.writeValue(undefined); expect(combo.displayValue).toEqual([]); @@ -1921,19 +1959,24 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); // primitive data - undefined is not displayed in the dropdown combo.valueKey = undefined; combo.displayKey = undefined; combo.data = [ 0, false, '', null, NaN, undefined]; - fixture.componentInstance.reactiveForm.resetForm(); + reactiveForm.resetForm(); fixture.detectChanges(); + expect(combo.displayValue).toEqual([]); expect(combo.selection).toEqual([]); expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INITIAL); expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); combo.onBlur(); fixture.detectChanges(); @@ -1944,6 +1987,8 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); combo.writeValue(''); expect(combo.displayValue).toEqual([]); @@ -1951,6 +1996,8 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); combo.writeValue(undefined); expect(combo.displayValue).toEqual([]); @@ -1958,6 +2005,8 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(reactiveForm.status).toEqual('INVALID'); + expect(reactiveControl.status).toEqual('INVALID'); }); it('Should update validity state when programmatically setting errors on reactive form controls', fakeAsync(() => { @@ -2049,7 +2098,7 @@ describe('IgxSimpleCombo', () => { UIInteractions.triggerEventHandlerKeyDown('Tab', input); fixture.detectChanges(); - expect(combo.selection.length).toEqual(0); + expect(combo.selection.length).toEqual(1); expect(combo.value.length).toEqual(1); expect(combo.displayValue).toEqual([`${selectedItem[combo.displayKey]}`]); expect(combo.value).toEqual([selectedItem[combo.valueKey]]); @@ -2063,7 +2112,7 @@ describe('IgxSimpleCombo', () => { // select item that is not present in the data source yet combo.select(15); - expect(combo.selection.length).toEqual(0); + expect(combo.selection.length).toEqual(1); expect(combo.value.length).toEqual(1); expect(combo.displayValue).toEqual([]); diff --git a/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.ts b/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.ts index 513684cea98..359160f53c0 100644 --- a/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.ts +++ b/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.ts @@ -30,8 +30,12 @@ import { IgxInputGroupComponent } from '../input-group/input-group.component'; /** Emitted when an igx-simple-combo's selection is changing. */ export interface ISimpleComboSelectionChangingEventArgs extends CancelableEventArgs, IBaseEventArgs { /** An object which represents the value that is currently selected */ - oldSelection: any; + oldValue: any; /** An object which represents the value that will be selected after this event */ + newValue: any; + /** An object which represents the item that is currently selected */ + oldSelection: any; + /** An object which represents the item that will be selected after this event */ newSelection: any; /** The text that will be displayed in the combo text box */ displayText: string; @@ -443,12 +447,16 @@ export class IgxSimpleComboComponent extends IgxComboBaseDirective implements Co }; protected setSelection(newSelection: any): void { - const newSelectionAsArray = newSelection ? Array.from(newSelection) as IgxComboItemComponent[] : []; - const oldSelectionAsArray = Array.from(this.selectionService.get(this.id) || []); - const displayText = this.createDisplayText(this.convertKeysToItems(newSelectionAsArray), oldSelectionAsArray); + const newValueAsArray = newSelection ? Array.from(newSelection) as IgxComboItemComponent[] : []; + const oldValueAsArray = Array.from(this.selectionService.get(this.id) || []); + const newItems = this.convertKeysToItems(newValueAsArray); + const oldItems = this.convertKeysToItems(oldValueAsArray); + const displayText = this.createDisplayText(this.convertKeysToItems(newValueAsArray), oldValueAsArray); const args: ISimpleComboSelectionChangingEventArgs = { - newSelection: newSelectionAsArray[0], - oldSelection: oldSelectionAsArray[0], + newValue: newValueAsArray[0], + oldValue: oldValueAsArray[0], + newSelection: newItems[0], + oldSelection: oldItems[0], displayText, owner: this, cancel: false @@ -458,8 +466,8 @@ export class IgxSimpleComboComponent extends IgxComboBaseDirective implements Co } // TODO: refactor below code as it sets the selection and the display text if (!args.cancel) { - let argsSelection = this.isValid(args.newSelection) - ? args.newSelection + let argsSelection = this.isValid(args.newValue) + ? args.newValue : []; argsSelection = Array.isArray(argsSelection) ? argsSelection : [argsSelection]; this.selectionService.select_items(this.id, argsSelection, true); @@ -467,12 +475,12 @@ export class IgxSimpleComboComponent extends IgxComboBaseDirective implements Co if (this._updateInput) { this.comboInput.value = this._internalFilter = this._displayValue = this.searchValue = displayText !== args.displayText ? args.displayText - : this.createDisplayText(this.selection, [args.oldSelection]); + : this.createDisplayText(this.selection, [args.oldValue]); } - this._onChangeCallback(args.newSelection); + this._onChangeCallback(args.newValue); this._updateInput = true; } else if (this.isRemote) { - this.registerRemoteEntries(newSelectionAsArray, false); + this.registerRemoteEntries(newValueAsArray, false); } }