From 325d26b390031a5cc9c172a921c4165c8256ad5a Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Tue, 31 Oct 2023 18:55:34 +0200 Subject: [PATCH 01/13] feat(combos): update selection changing events --- CHANGELOG.md | 8 ++- .../src/lib/combo/combo.common.ts | 16 +++-- .../src/lib/combo/combo.component.spec.ts | 65 +++++++++++++++---- .../src/lib/combo/combo.component.ts | 30 +++++---- .../simple-combo.component.spec.ts | 12 +++- .../simple-combo/simple-combo.component.ts | 30 +++++---- 6 files changed, 117 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae45bdad60b..ed19ab940c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,13 @@ 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` + - `IComboSelectionChangingEventArgs` exposes two new properties - `newValue` and `oldValue`. When combo's `valueKey` is set the keys of the items are committed in both. When combo's `valueKey` is not set the entire items are committed. _Note: when combo is working with remote data and a primary key has been set for the selected items that are not currently part of the combo view, will be emitted a partial item data object_ + - **Breaking Change** - `IComboSelectionChangingEventArgs` event arguments are changed. Now the `oldSelection` and `newSelection` collections no longer consist of the keys of the selected items when the combo has set a primaryKey, but now in any case the item data is emitted. + When the combo is working with remote data and a primary key has been set for the selected items that are not currently part of the combo view, will be emitted a partial item data object. +- `IgxSimpleCombo` + - **Breaking Change** - `ISimpleComboSelectionChangingEventArgs` event arguments are changed. Now the `oldSelection` and `newSelection` collections no longer consist of the key of the selected item when the combo has set a primaryKey, but now in any case the item data is emitted. + - `ISimpleComboSelectionChangingEventArgs` exposes two new properties - `newValue` and `oldValue`. When combo's `valueKey` is set the key of the item is committed in both. When combo's `valueKey` is not set the entire item is committed. ## 16.1.4 ### New Features - `Themes`: diff --git a/projects/igniteui-angular/src/lib/combo/combo.common.ts b/projects/igniteui-angular/src/lib/combo/combo.common.ts index 2a7dc8ac074..7a957788ffe 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.common.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.common.ts @@ -1294,11 +1294,17 @@ 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 => { + if (this.valueKey === null || this.valueKey === undefined) { + return key + } + + const item = isNaNvalue(key) + ? this.data.find(entry => isNaNvalue(entry[this.valueKey])) + : this.data.find(entry => entry[this.valueKey] === key); + + return 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..07920907969 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts @@ -61,7 +61,7 @@ const CSS_CLASS_ITME_CHECKBOX_CHECKED = 'igx-checkbox--checked'; const defaultDropdownItemHeight = 40; const defaultDropdownItemMaxHeight = 400; -describe('igxCombo', () => { +fdescribe('igxCombo', () => { let fixture: ComponentFixture; let combo: IgxComboComponent; let input: DebugElement; @@ -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,7 +445,9 @@ 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]], removed: [], @@ -440,8 +459,10 @@ 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]] @@ -465,7 +486,9 @@ describe('igxCombo', () => { let newSelection = [combo.data[0], combo.data[1], combo.data[2]]; const selectionSpy = spyOn(combo.selectionChanging, 'emit'); 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]), removed: [], @@ -476,21 +499,25 @@ describe('igxCombo', () => { }; 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: newSelection, oldSelection, added: [], displayText: newSelection.map(e => e[combo.displayKey]).join(', '), removed: [combo.data[0][combo.valueKey]] }); - oldSelection = [...newSelection].map(e => e[combo.valueKey]); + oldSelection = [...newSelection]; newSelection = [combo.data[4], combo.data[5], combo.data[6]]; expect(selectionSpy).toHaveBeenCalledWith(expectedResults); 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: newSelection, oldSelection, added: newSelection.map(e => e[combo.valueKey]), displayText: newSelection.map(e => e[combo.displayKey]).join(', '), @@ -537,6 +564,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 +581,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 +603,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 @@ -1975,7 +2006,9 @@ 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]], removed: [], @@ -1994,8 +2027,10 @@ 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]], + 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[combo.valueKey]], removed: [], event: UIInteractions.getMouseEvent('click'), @@ -2013,8 +2048,10 @@ 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]], event: UIInteractions.getMouseEvent('click'), diff --git a/projects/igniteui-angular/src/lib/combo/combo.component.ts b/projects/igniteui-angular/src/lib/combo/combo.component.ts index d9c63fabdf3..cb3b8836f73 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; @@ -430,12 +434,16 @@ 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); + const newValueAsArray = Array.from(newSelection); + 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: IComboSelectionChangingEventArgs = { - newSelection: newSelectionAsArray, - oldSelection: oldSelectionAsArray, + newValue: newValueAsArray, + oldValue: oldValueAsArray, + newSelection: newItems, + oldSelection: oldItems, added, removed, event, @@ -445,14 +453,14 @@ 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); } 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..63b1ef4d06f 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 @@ -230,6 +230,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 +244,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 +266,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 +280,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(); 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); } } From 18d01dc09686a453f7770b578bb7a4f36f69b8a7 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Tue, 31 Oct 2023 22:23:43 +0200 Subject: [PATCH 02/13] test(combos): fix failing tests --- .../src/lib/combo/combo.component.spec.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) 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 07920907969..f773e8f517e 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts @@ -61,7 +61,7 @@ const CSS_CLASS_ITME_CHECKBOX_CHECKED = 'igx-checkbox--checked'; const defaultDropdownItemHeight = 40; const defaultDropdownItemMaxHeight = 400; -fdescribe('igxCombo', () => { +describe('igxCombo', () => { let fixture: ComponentFixture; let combo: IgxComboComponent; let input: DebugElement; @@ -482,9 +482,11 @@ fdescribe('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 = { newValue: newSelection.map(e => e[combo.valueKey]), oldValue: [], @@ -497,33 +499,34 @@ fdescribe('igxCombo', () => { displayText: `${newSelection.map(entry => entry[combo.displayKey]).join(', ')}`, cancel: false }; - combo.select(newSelection.map(e => e[combo.valueKey])); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); + oldSelection = [...newSelection]; newSelection = [combo.data[1], combo.data[2]]; combo.deselect([combo.data[0][combo.valueKey]]); Object.assign(expectedResults, { newValue: newSelection.map(e => e[combo.valueKey]), oldValue: oldSelection.map(e => e[combo.valueKey]), - newSelection: newSelection, + newSelection, oldSelection, added: [], displayText: newSelection.map(e => e[combo.displayKey]).join(', '), removed: [combo.data[0][combo.valueKey]] }); + expect(selectionSpy).toHaveBeenCalledWith(expectedResults); + oldSelection = [...newSelection]; newSelection = [combo.data[4], combo.data[5], combo.data[6]]; - expect(selectionSpy).toHaveBeenCalledWith(expectedResults); + combo.select(newSelection.map(e => e[combo.valueKey]), true); Object.assign(expectedResults, { newValue: newSelection.map(e => e[combo.valueKey]), oldValue: oldSelection.map(e => e[combo.valueKey]), - newSelection: newSelection, + newSelection, oldSelection, added: newSelection.map(e => e[combo.valueKey]), displayText: newSelection.map(e => e[combo.displayKey]).join(', '), - removed: oldSelection + removed: oldSelection.map(e => e[combo.valueKey]) }); - combo.select(newSelection.map(e => e[combo.valueKey]), true); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); }); it('should handle select/deselect ALL items', () => { @@ -1366,9 +1369,9 @@ fdescribe('igxCombo', () => { 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]; From e42d1a1af351862ab5347b6488af9aa80a3460d2 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Tue, 31 Oct 2023 23:17:00 +0200 Subject: [PATCH 03/13] test(simple-combo): fix failing tests --- .../lib/simple-combo/simple-combo.component.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 63b1ef4d06f..91d92f13953 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 @@ -299,7 +299,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 @@ -1807,7 +1807,7 @@ describe('IgxSimpleCombo', () => { expect(combo.displayValue).toEqual([]); expect(combo.selection).toEqual([]); - expect(combo.value).toEqual([]); + expect(combo.value).toEqual(['']); expect(combo.valid).toEqual(IgxComboState.INITIAL); expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL); @@ -1902,7 +1902,7 @@ describe('IgxSimpleCombo', () => { expect(combo.displayValue).toEqual([]); expect(combo.selection).toEqual([]); - expect(combo.value).toEqual([]); + expect(combo.value).toEqual(['']); expect(combo.valid).toEqual(IgxComboState.INITIAL); expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL); @@ -2057,7 +2057,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]]); @@ -2071,7 +2071,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([]); From 830802796e3fda56a99507078a151940296b08ff Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Wed, 1 Nov 2023 00:30:48 +0200 Subject: [PATCH 04/13] chore(combo): clean up combo common --- projects/igniteui-angular/src/lib/combo/combo.common.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/projects/igniteui-angular/src/lib/combo/combo.common.ts b/projects/igniteui-angular/src/lib/combo/combo.common.ts index 7a957788ffe..d7effb1ee98 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.common.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.common.ts @@ -1295,15 +1295,11 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement } return keys.map(key => { - if (this.valueKey === null || this.valueKey === undefined) { - return key - } - const item = isNaNvalue(key) ? this.data.find(entry => isNaNvalue(entry[this.valueKey])) : this.data.find(entry => entry[this.valueKey] === key); - return item ?? { [this.valueKey]: key }; + return item !== undefined ? item : { [this.valueKey]: key }; }); } From edfd98086807995124bc90aea0dbf331263f9bb6 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Thu, 2 Nov 2023 12:07:14 +0200 Subject: [PATCH 05/13] feat(combo): emit added and removed as full object --- .../src/lib/combo/combo.component.spec.ts | 18 ++++++------- .../src/lib/combo/combo.component.ts | 27 ++++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) 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 f773e8f517e..076a6176246 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts @@ -449,7 +449,7 @@ describe('igxCombo', () => { oldValue: [], newSelection: [combo.data[0]], oldSelection: [], - added: [combo.data[0][combo.valueKey]], + added: [combo.data[0]], removed: [], event: undefined, owner: combo, @@ -465,7 +465,7 @@ describe('igxCombo', () => { 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); @@ -492,7 +492,7 @@ describe('igxCombo', () => { oldValue: [], newSelection: newSelection, oldSelection, - added: newSelection.map(e => e[combo.valueKey]), + added: newSelection, removed: [], event: undefined, owner: combo, @@ -511,7 +511,7 @@ describe('igxCombo', () => { oldSelection, added: [], displayText: newSelection.map(e => e[combo.displayKey]).join(', '), - removed: [combo.data[0][combo.valueKey]] + removed: [combo.data[0]] }); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); @@ -523,9 +523,9 @@ describe('igxCombo', () => { 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.map(e => e[combo.valueKey]) + removed: oldSelection }); expect(selectionSpy).toHaveBeenCalledWith(expectedResults); }); @@ -2013,7 +2013,7 @@ describe('igxCombo', () => { oldValue: [], newSelection: [selectedItem_1.value], oldSelection: [], - added: [selectedItem_1.value[combo.valueKey]], + added: [selectedItem_1.value], removed: [], event: UIInteractions.getMouseEvent('click'), owner: combo, @@ -2034,7 +2034,7 @@ describe('igxCombo', () => { oldValue: [selectedItem_1.value[combo.valueKey]], newSelection: [selectedItem_1.value, selectedItem_2.value], oldSelection: [selectedItem_1.value], - added: [selectedItem_2.value[combo.valueKey]], + added: [selectedItem_2.value], removed: [], event: UIInteractions.getMouseEvent('click'), owner: combo, @@ -2056,7 +2056,7 @@ describe('igxCombo', () => { 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 cb3b8836f73..fcec648b840 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.ts @@ -431,19 +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 newValueAsArray = Array.from(newSelection); - 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); + 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 = { - newValue: newValueAsArray, - oldValue: oldValueAsArray, - newSelection: newItems, - oldSelection: oldItems, + newValue, + oldValue, + newSelection, + oldSelection, added, removed, event, @@ -462,7 +463,7 @@ export class IgxComboComponent extends IgxComboBaseDirective implements AfterVie } this._onChangeCallback(args.newValue); } else if (this.isRemote) { - this.registerRemoteEntries(args.added, false); + this.registerRemoteEntries(diffInSets(selection, currentSelection), false); } } From be96bdf652f0d90577c89a25fd3817e36963f73f Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Thu, 2 Nov 2023 13:42:09 +0200 Subject: [PATCH 06/13] feat(combos): add migration for changed properties --- CHANGELOG.md | 2 +- .../update-17_0_0/changes/members.json | 21 +++++++ .../migrations/update-17_0_0/index.spec.ts | 60 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 projects/igniteui-angular/migrations/update-17_0_0/changes/members.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ed19ab940c2..b3a9f02f046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ All notable changes for each version of this project will be documented in this - IgxSwitchComponent has been reduced in half - `IgxCombo` - `IComboSelectionChangingEventArgs` exposes two new properties - `newValue` and `oldValue`. When combo's `valueKey` is set the keys of the items are committed in both. When combo's `valueKey` is not set the entire items are committed. _Note: when combo is working with remote data and a primary key has been set for the selected items that are not currently part of the combo view, will be emitted a partial item data object_ - - **Breaking Change** - `IComboSelectionChangingEventArgs` event arguments are changed. Now the `oldSelection` and `newSelection` collections no longer consist of the keys of the selected items when the combo has set a primaryKey, but now in any case the item data is emitted. + - **Breaking Change** - `IComboSelectionChangingEventArgs` event arguments are changed. Now the `oldSelection`, `newSelection`, `added` and `removed` collections no longer consist of the keys of the selected items when the combo has set a primaryKey, but now in any case the item data is emitted. When the combo is working with remote data and a primary key has been set for the selected items that are not currently part of the combo view, will be emitted a partial item data object. - `IgxSimpleCombo` - **Breaking Change** - `ISimpleComboSelectionChangingEventArgs` event arguments are changed. Now the `oldSelection` and `newSelection` collections no longer consist of the key of the selected item when the combo has set a primaryKey, but now in any case the item data is emitted. 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 ca931c566de..82c3c8de11e 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 @@ -205,4 +205,64 @@ 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; + } + } + ` + ); + }); }); \ No newline at end of file From 7ae525ef25f7200691c54a6515acdd636efb09b1 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Mon, 6 Nov 2023 16:24:53 +0200 Subject: [PATCH 07/13] Update CHANGELOG.md Co-authored-by: Damyan Petev --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a9f02f046..708cfea3e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,10 @@ All notable changes for each version of this project will be documented in this - IgxRadioComponent has been reduced in half - IgxSwitchComponent has been reduced in half - `IgxCombo` - - `IComboSelectionChangingEventArgs` exposes two new properties - `newValue` and `oldValue`. When combo's `valueKey` is set the keys of the items are committed in both. When combo's `valueKey` is not set the entire items are committed. _Note: when combo is working with remote data and a primary key has been set for the selected items that are not currently part of the combo view, will be emitted a partial item data object_ + - **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` event arguments are changed. Now the `oldSelection`, `newSelection`, `added` and `removed` collections no longer consist of the keys of the selected items when the combo has set a primaryKey, but now in any case the item data is emitted. When the combo is working with remote data and a primary key has been set for the selected items that are not currently part of the combo view, will be emitted a partial item data object. - `IgxSimpleCombo` From 3c2f976e17d5d2fc7fbdb6c7c23e4cda6435e89a Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Mon, 6 Nov 2023 16:25:11 +0200 Subject: [PATCH 08/13] Update CHANGELOG.md Co-authored-by: Damyan Petev --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 708cfea3e3b..68404bddfc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,10 @@ All notable changes for each version of this project will be documented in this - **Breaking Change** - `IComboSelectionChangingEventArgs` event arguments are changed. Now the `oldSelection`, `newSelection`, `added` and `removed` collections no longer consist of the keys of the selected items when the combo has set a primaryKey, but now in any case the item data is emitted. When the combo is working with remote data and a primary key has been set for the selected items that are not currently part of the combo view, will be emitted a partial item data object. - `IgxSimpleCombo` - - **Breaking Change** - `ISimpleComboSelectionChangingEventArgs` event arguments are changed. Now the `oldSelection` and `newSelection` collections no longer consist of the key of the selected item when the combo has set a primaryKey, but now in any case the item data is emitted. - - `ISimpleComboSelectionChangingEventArgs` exposes two new properties - `newValue` and `oldValue`. When combo's `valueKey` is set the key of the item is committed in both. When combo's `valueKey` is not set the entire item is committed. + - **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`: From 4a3c77a512e854e44c9f78e6a0b600a7b3af12da Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Mon, 6 Nov 2023 16:27:32 +0200 Subject: [PATCH 09/13] Update CHANGELOG.md Co-authored-by: Damyan Petev --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68404bddfc3..cfa50b15860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,7 @@ All notable changes for each version of this project will be documented in this - `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` event arguments are changed. Now the `oldSelection`, `newSelection`, `added` and `removed` collections no longer consist of the keys of the selected items when the combo has set a primaryKey, but now in any case the item data is emitted. - When the combo is working with remote data and a primary key has been set for the selected items that are not currently part of the combo view, will be emitted a partial item data object. + - **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`. From fe8f2a6451f4d5bc846aa870b586740781f5eec1 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Mon, 6 Nov 2023 17:28:42 +0200 Subject: [PATCH 10/13] test(combo): add test for emitting partial data --- .../src/lib/combo/combo.component.spec.ts | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) 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 076a6176246..50979855154 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.spec.ts @@ -1364,7 +1364,7 @@ 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([]); @@ -1387,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; From cf36da5a888a5c676593212ba8b057e8d10fad22 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Mon, 6 Nov 2023 19:49:57 +0200 Subject: [PATCH 11/13] test(combo): address PR comments --- .../simple-combo.component.spec.ts | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) 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 91d92f13953..82391a7616a 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 @@ -1794,20 +1794,24 @@ describe('IgxSimpleCombo', () => { fixture.detectChanges(); combo = fixture.componentInstance.reactiveCombo; }); - it('should not select null, undefined and empty string in a reactive form with required', () => { + // TODO: this test makes no sense - reactive combo with values equal to null, undefined and empty string + 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: 'null', value: null }, - { field: 'NaN', value: NaN }, - { field: 'undefined', value: undefined }, + { field: 'false', value: [false] }, + { field: 'empty string', value: [''] }, + { field: 'null', value: [null] }, + { field: 'NaN', value: [NaN] }, + { field: 'undefined', value: [undefined] }, ]; + fixture.componentInstance.reactiveForm.resetForm(); + fixture.detectChanges(); + expect(combo.displayValue).toEqual([]); expect(combo.selection).toEqual([]); - expect(combo.value).toEqual(['']); + expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INITIAL); expect(combo.comboInput.valid).toEqual(IgxInputState.INITIAL); @@ -1819,10 +1823,10 @@ describe('IgxSimpleCombo', () => { fixture.detectChanges(); expect(combo.displayValue).toEqual([]); - expect(combo.selection).toEqual([]); - expect(combo.value).toEqual([]); - expect(combo.valid).toEqual(IgxComboState.INVALID); - expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(combo.selection).toEqual([{ value: '' }]); + expect(combo.value).toEqual(['']); + expect(combo.valid).toEqual(IgxComboState.VALID); + expect(combo.comboInput.valid).toEqual(IgxInputState.VALID); // null combo.open(); @@ -1832,10 +1836,10 @@ describe('IgxSimpleCombo', () => { fixture.detectChanges(); expect(combo.displayValue).toEqual([]); - expect(combo.selection).toEqual([]); - expect(combo.value).toEqual([]); - expect(combo.valid).toEqual(IgxComboState.INVALID); - expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(combo.selection).toEqual([{ value: null }]); + expect(combo.value).toEqual([null]); + expect(combo.valid).toEqual(IgxComboState.VALID); + expect(combo.comboInput.valid).toEqual(IgxInputState.VALID); // undefined combo.open(); @@ -1845,15 +1849,15 @@ describe('IgxSimpleCombo', () => { fixture.detectChanges(); expect(combo.displayValue).toEqual([]); - expect(combo.selection).toEqual([]); - expect(combo.value).toEqual([]); - expect(combo.valid).toEqual(IgxComboState.INVALID); - expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); + expect(combo.selection).toEqual([{ value: undefined }]); + expect(combo.value).toEqual([undefined]); + expect(combo.valid).toEqual(IgxComboState.VALID); + expect(combo.comboInput.valid).toEqual(IgxInputState.VALID); // 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(); fixture.detectChanges(); @@ -1888,7 +1892,7 @@ describe('IgxSimpleCombo', () => { expect(combo.value).toEqual([]); expect(combo.valid).toEqual(IgxComboState.INVALID); expect(combo.comboInput.valid).toEqual(IgxInputState.INVALID); - }); + })); it('should not select null, undefined and empty string with "writeValue" method in a reactive form with required', () => { // array of objects combo.data = [ From 9e745f64af75061bcae771946ea97e17f7a57bf2 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Tue, 7 Nov 2023 15:17:59 +0200 Subject: [PATCH 12/13] test(simple-combo): fix failing tests --- .../simple-combo.component.spec.ts | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) 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 82391a7616a..f0be0f8bb92 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: any; + let reactiveControl: any; configureTestSuite(); @@ -1793,17 +1795,18 @@ describe('IgxSimpleCombo', () => { fixture = TestBed.createComponent(IgxSimpleComboInReactiveFormComponent); fixture.detectChanges(); combo = fixture.componentInstance.reactiveCombo; + reactiveForm = fixture.componentInstance.comboForm; + reactiveControl = reactiveForm.controls.comboValue }); - // TODO: this test makes no sense - reactive combo with values equal to null, undefined and empty string 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: 'empty string', value: [''] }, - { field: 'null', value: [null] }, - { field: 'NaN', value: [NaN] }, - { field: 'undefined', value: [undefined] }, + { field: 'false', value: false }, + { field: 'empty string', value: ''}, + { field: 'null', value: null }, + { field: 'NaN', value: NaN }, + { field: 'undefined', value: undefined }, ]; fixture.componentInstance.reactiveForm.resetForm(); @@ -1814,6 +1817,8 @@ describe('IgxSimpleCombo', () => { 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(); @@ -1823,10 +1828,12 @@ describe('IgxSimpleCombo', () => { fixture.detectChanges(); expect(combo.displayValue).toEqual([]); - expect(combo.selection).toEqual([{ value: '' }]); - expect(combo.value).toEqual(['']); - expect(combo.valid).toEqual(IgxComboState.VALID); - expect(combo.comboInput.valid).toEqual(IgxInputState.VALID); + expect(combo.selection).toEqual([]); + 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(); @@ -1836,10 +1843,12 @@ describe('IgxSimpleCombo', () => { fixture.detectChanges(); expect(combo.displayValue).toEqual([]); - expect(combo.selection).toEqual([{ value: null }]); - expect(combo.value).toEqual([null]); - expect(combo.valid).toEqual(IgxComboState.VALID); - expect(combo.comboInput.valid).toEqual(IgxInputState.VALID); + expect(combo.selection).toEqual([]); + 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(); @@ -1849,10 +1858,12 @@ describe('IgxSimpleCombo', () => { fixture.detectChanges(); expect(combo.displayValue).toEqual([]); - expect(combo.selection).toEqual([{ value: undefined }]); - expect(combo.value).toEqual([undefined]); - expect(combo.valid).toEqual(IgxComboState.VALID); - expect(combo.comboInput.valid).toEqual(IgxInputState.VALID); + expect(combo.selection).toEqual([]); + 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; @@ -1866,6 +1877,8 @@ describe('IgxSimpleCombo', () => { 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(); @@ -1879,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(); @@ -1892,6 +1907,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 not select null, undefined and empty string with "writeValue" method in a reactive form with required', () => { // array of objects From 0daddfb67e274828c5abcbf51092d1b635cff514 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Tue, 7 Nov 2023 15:31:42 +0200 Subject: [PATCH 13/13] test(simple-combo): fix more reactive form related tests --- .../simple-combo.component.spec.ts | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) 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 f0be0f8bb92..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,7 +49,7 @@ describe('IgxSimpleCombo', () => { let fixture: ComponentFixture; let combo: IgxSimpleComboComponent; let input: DebugElement; - let reactiveForm: any; + let reactiveForm: NgForm; let reactiveControl: any; configureTestSuite(); @@ -1795,8 +1795,8 @@ describe('IgxSimpleCombo', () => { fixture = TestBed.createComponent(IgxSimpleComboInReactiveFormComponent); fixture.detectChanges(); combo = fixture.componentInstance.reactiveCombo; - reactiveForm = fixture.componentInstance.comboForm; - reactiveControl = reactiveForm.controls.comboValue + reactiveForm = fixture.componentInstance.reactiveForm; + reactiveControl = reactiveForm.form.controls['comboValue']; }); it('should not select null, undefined and empty string in a reactive form with required', fakeAsync(() => { // array of objects @@ -1809,7 +1809,7 @@ describe('IgxSimpleCombo', () => { { field: 'undefined', value: undefined }, ]; - fixture.componentInstance.reactiveForm.resetForm(); + reactiveForm.resetForm(); fixture.detectChanges(); expect(combo.displayValue).toEqual([]); @@ -1870,7 +1870,7 @@ describe('IgxSimpleCombo', () => { 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([]); @@ -1915,17 +1915,22 @@ describe('IgxSimpleCombo', () => { 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.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(); @@ -1936,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([]); @@ -1943,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([]); @@ -1950,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(); @@ -1973,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([]); @@ -1980,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([]); @@ -1987,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(() => {