Skip to content

Commit 70c0fc6

Browse files
authored
fix(simple-combo): preserve selection when bound to remote data - 15.0.x (#12385)
* fix(simple-combo): preserve selection when bound to remote data #12289 * fix(combo-common): allow custom displayText on selection #12222 * test(combos): add tests covering the code * chore(*): add remote simple combo sample * fix(simple-combo): register remote entry if selection is truthy * refactor(simple-combo): remove unnecessary return
1 parent 4225982 commit 70c0fc6

File tree

6 files changed

+206
-16
lines changed

6 files changed

+206
-16
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -986,12 +986,12 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement
986986
});
987987
}
988988

989-
/** @hidden @internal */
990-
public ngDoCheck() {
991-
if (this.data?.length && this.selection.length) {
992-
this._value = this.createDisplayText(this.selection, []);
993-
}
989+
/** @hidden @internal */
990+
public ngDoCheck() {
991+
if (this.data?.length && this.selection.length && !this._value) {
992+
this._value = this.createDisplayText(this.selection, []);
994993
}
994+
}
995995

996996
/** @hidden @internal */
997997
public ngOnDestroy() {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2243,6 +2243,29 @@ describe('igxCombo', () => {
22432243
const expectedOutput = 'One';
22442244
expect(input.nativeElement.value).toEqual(expectedOutput);
22452245
}));
2246+
it('should display custom displayText on selection/deselection', () => {
2247+
combo.valueKey = 'key';
2248+
combo.displayKey = 'value';
2249+
combo.data = [
2250+
{ key: 1, value: 'One' },
2251+
{ key: 2, value: 'Two' },
2252+
{ key: 3, value: 'Three' },
2253+
];
2254+
2255+
spyOn(combo.selectionChanging, 'emit').and.callFake(
2256+
(event: IComboSelectionChangingEventArgs) => event.displayText = `Selected Count: ${event.newSelection.length}`);
2257+
2258+
combo.select([1]);
2259+
fixture.detectChanges();
2260+
2261+
expect(combo.selection).toEqual([1]);
2262+
expect(combo.value).toBe('Selected Count: 1');
2263+
2264+
combo.deselect([1]);
2265+
2266+
expect(combo.selection).toEqual([]);
2267+
expect(combo.value).toBe('Selected Count: 0');
2268+
});
22462269
});
22472270
describe('Grouping tests: ', () => {
22482271
configureTestSuite();

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

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('IgxSimpleCombo', () => {
5353
let fixture: ComponentFixture<any>;
5454
let combo: IgxSimpleComboComponent;
5555
let input: DebugElement;
56-
56+
5757
configureTestSuite();
5858

5959
describe('Unit tests: ', () => {
@@ -1291,7 +1291,7 @@ describe('IgxSimpleCombo', () => {
12911291

12921292
expect(toggleIcon.nativeElement.textContent).toBe('search');
12931293
expect(combo.collapsed).toBeTruthy();
1294-
1294+
12951295
toggleIcon.nativeElement.click();
12961296
tick();
12971297
fixture.detectChanges();
@@ -1304,6 +1304,20 @@ describe('IgxSimpleCombo', () => {
13041304

13051305
expect(combo.collapsed).toBeTruthy();
13061306
}));
1307+
1308+
it('should clear the selection when typing in the input', () => {
1309+
combo.select('Wisconsin');
1310+
fixture.detectChanges();
1311+
1312+
expect(combo.selection.length).toEqual(1);
1313+
1314+
input.triggerEventHandler('focus', {});
1315+
fixture.detectChanges();
1316+
1317+
UIInteractions.simulateTyping('L', input, 9, 10);
1318+
fixture.detectChanges();
1319+
expect(combo.selection.length).toEqual(0);
1320+
});
13071321
});
13081322

13091323
describe('Display density', () => {
@@ -1783,6 +1797,7 @@ describe('IgxSimpleCombo', () => {
17831797
fixture = TestBed.createComponent(IgxComboRemoteDataComponent);
17841798
fixture.detectChanges();
17851799
combo = fixture.componentInstance.instance;
1800+
input = fixture.debugElement.query(By.css(`.${CSS_CLASS_COMBO_INPUTGROUP}`));
17861801
});
17871802
it('should prevent registration of remote entries when selectionChanging is cancelled', () => {
17881803
spyOn(combo.selectionChanging, 'emit').and.callFake((event: IComboSelectionChangingEventArgs) => event.cancel = true);
@@ -1807,6 +1822,29 @@ describe('IgxSimpleCombo', () => {
18071822
const expectedOutput = 'One';
18081823
expect(input.nativeElement.value).toEqual(expectedOutput);
18091824
}));
1825+
it('should not clear selection when bound to remote data and item is out of view', (async () => {
1826+
expect(combo.valueKey).toBeDefined();
1827+
expect(combo.selection.length).toEqual(0);
1828+
1829+
let selectedItem = combo.data[1];
1830+
combo.toggle();
1831+
combo.select(combo.data[1][combo.valueKey]);
1832+
1833+
// Scroll selected item out of view
1834+
combo.virtualScrollContainer.scrollTo(40);
1835+
await wait();
1836+
fixture.detectChanges();
1837+
1838+
input.nativeElement.focus();
1839+
fixture.detectChanges();
1840+
1841+
UIInteractions.triggerEventHandlerKeyDown('Tab', input);
1842+
fixture.detectChanges();
1843+
1844+
expect(combo.selection.length).toEqual(1);
1845+
expect(combo.value).toEqual(`${selectedItem[combo.displayKey]}`);
1846+
expect(combo.selection).toEqual([selectedItem[combo.valueKey]]);
1847+
}));
18101848
});
18111849
});
18121850

@@ -1898,8 +1936,8 @@ export class IgxSimpleComboIconTemplatesComponent {
18981936
public combo: IgxSimpleComboComponent;
18991937

19001938
public data: any[] = [
1901-
{ name: 'Sofia', id: '1' },
1902-
{ name: 'London', id: '2' },
1939+
{ name: 'Sofia', id: '1' },
1940+
{ name: 'London', id: '2' },
19031941
];;
19041942
public name!: string;
19051943
}

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ export class IgxSimpleComboComponent extends IgxComboBaseDirective implements Co
261261
this.clearSelection();
262262
this._onChangeCallback(null);
263263
}
264+
if (this.selection.length) {
265+
this.selectionService.clear(this.id);
266+
}
264267
// when filtering the focused item should be the first item or the currently selected item
265268
if (!this.dropdown.focusedItem || this.dropdown.focusedItem.id !== this.dropdown.items[0].id) {
266269
this.dropdown.navigateFirst();
@@ -464,6 +467,28 @@ export class IgxSimpleComboComponent extends IgxComboBaseDirective implements Co
464467
return newSelection[0]?.toString() || '';
465468
}
466469

470+
protected getRemoteSelection(newSelection: any[], oldSelection: any[]): string {
471+
if (!newSelection.length) {
472+
this.registerRemoteEntries(oldSelection, false);
473+
return '';
474+
}
475+
476+
this.registerRemoteEntries(oldSelection, false);
477+
this.registerRemoteEntries(newSelection);
478+
return Object.keys(this._remoteSelection).map(e => this._remoteSelection[e])[0];
479+
}
480+
481+
/** Contains key-value pairs of the selected valueKeys and their resp. displayKeys */
482+
protected registerRemoteEntries(ids: any[], add = true) {
483+
const selection = this.getValueDisplayPairs(ids)[0];
484+
485+
if (add && selection) {
486+
this._remoteSelection[selection[this.valueKey]] = selection[this.displayKey].toString();
487+
} else {
488+
delete this._remoteSelection[ids[0]];
489+
}
490+
}
491+
467492
private clearSelection(ignoreFilter?: boolean): void {
468493
let newSelection = this.selectionService.get_empty();
469494
if (this.filteredData.length !== this.data.length && !ignoreFilter) {
@@ -473,11 +498,19 @@ export class IgxSimpleComboComponent extends IgxComboBaseDirective implements Co
473498
}
474499

475500
private clearOnBlur(): void {
501+
if (this.isRemote) {
502+
const searchValue = this.searchValue || this.comboInput.value;
503+
const remoteValue = Object.keys(this._remoteSelection).map(e => this._remoteSelection[e])[0];
504+
if (remoteValue && searchValue !== remoteValue) {
505+
this.clear();
506+
}
507+
return;
508+
}
509+
476510
const filtered = this.filteredData.find(this.findMatch);
477511
// selecting null in primitive data returns undefined as the search text is '', but the item is null
478512
if (filtered === undefined && this.selectedItem !== null || !this.selection.length) {
479513
this.clear();
480-
return;
481514
}
482515
}
483516

@@ -488,8 +521,7 @@ export class IgxSimpleComboComponent extends IgxComboBaseDirective implements Co
488521

489522
private clear(): void {
490523
this.clearSelection(true);
491-
this._internalFilter = '';
492-
this.searchValue = '';
524+
this.comboInput.value = this._internalFilter = this._value = this.searchValue = '';
493525
}
494526

495527
private isValid(value: any): boolean {

src/app/combo/combo.sample.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,22 @@ <h5 class="sample-title">Reactive Form</h5>
6868
<div>
6969
<h5 class="sample-title">Remote Binding</h5>
7070
<igx-combo #remoteCombo class="input-container" [itemsMaxHeight]="250" [itemHeight]="48"
71-
[data]="rData | async" [valueKey]="'ProductID'" [displayKey]="'ProductName'"
71+
[data]="rData | async" [valueKey]="'ProductID'" [displayKey]="'ProductName'" placeholder="Product(s)"
7272
(dataPreLoad)="dataLoading()" (searchInputUpdate)="searchInput($event)"
7373
(selectionChanging)="handleSelectionChanging($event)" (closing)="onClosing()"
74-
(opening)="onOpening()" placeholder="Location(s)">
74+
(opening)="onOpening()">
75+
<label igxLabel>Combo</label>
7576
</igx-combo>
7677
<igx-toast #loadingToast></igx-toast>
7778

79+
<igx-simple-combo #remoteSimpleCombo class="input-container" [itemsMaxHeight]="250" [itemHeight]="48"
80+
[data]="rData | async" [valueKey]="'ProductID'" [displayKey]="'ProductName'" placeholder="Product"
81+
(dataPreLoad)="onSimpleComboDataLoading()" (closing)="onSimpleComboClosing()"
82+
(opened)="onSimpleComboOpened()" (closed)="onSimpleComboClosed()"
83+
(selectionChanging)="onSimpleComboSelectionChanging($event)"
84+
(searchInputUpdate)="onSimpleComboSearchInputUpdate($event)">
85+
<label igxLabel>Simple Combo</label>
86+
</igx-simple-combo>
7887
</div>
7988

8089
<div class="input-row">

src/app/combo/combo.sample.ts

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
IChangeSwitchEventArgs,
1111
IComboSearchInputEventArgs,
1212
IComboSelectionChangingEventArgs,
13+
ISimpleComboSelectionChangingEventArgs,
1314
IForOfState,
1415
IgxComboComponent,
1516
IgxSimpleComboComponent,
@@ -40,6 +41,9 @@ export class ComboSampleComponent implements OnInit, AfterViewInit {
4041
@ViewChild('remoteCombo')
4142
public remoteCombo: IgxComboComponent;
4243

44+
@ViewChild('remoteSimpleCombo')
45+
public remoteSimpleCombo: IgxSimpleComboComponent;
46+
4347
@ViewChild('playgroundCombo', { read: ElementRef, static: true })
4448
private comboRef: ElementRef;
4549

@@ -59,11 +63,17 @@ export class ComboSampleComponent implements OnInit, AfterViewInit {
5963
public singleValue = 'Arizona';
6064
public values2: Array<any>;
6165
public isDisabled = false;
66+
6267
public rData: any;
68+
public prevRequest: any;
69+
public simpleComboPrevRequest: any;
6370
private searchText: string = '';
6471
private defaultVirtState: IForOfState = { chunkSize: 6, startIndex: 0 };
72+
private currentVirtState: IForOfState = { chunkSize: 6, startIndex: 0 };
6573
private hasSelection: boolean;
66-
public prevRequest: any;
74+
private additionalScroll: number = 0;
75+
private itemID = 1;
76+
private itemCount: number = 0;
6777

6878
public valueKeyVar = 'field';
6979
public currentDataType = '';
@@ -219,7 +229,8 @@ export class ComboSampleComponent implements OnInit, AfterViewInit {
219229
chunkSize: Math.ceil(250 / this.remoteCombo.itemHeight),
220230
};
221231
this.remoteService.getData(initSize, null, (data) => {
222-
this.remoteCombo.totalItemCount = data['@odata.count'];
232+
this.remoteCombo.totalItemCount = this.remoteSimpleCombo.totalItemCount = data['@odata.count'];
233+
this.itemCount = this.remoteSimpleCombo.totalItemCount;
223234
});
224235
}
225236

@@ -319,4 +330,81 @@ export class ComboSampleComponent implements OnInit, AfterViewInit {
319330
public handleSelectionChanging(evt: IComboSelectionChangingEventArgs) {
320331
this.hasSelection = !!evt?.newSelection.length;
321332
}
333+
334+
public onSimpleComboDataLoading() {
335+
if (this.simpleComboPrevRequest) {
336+
this.simpleComboPrevRequest.unsubscribe();
337+
}
338+
this.loadingToast.positionSettings.verticalDirection = VerticalAlignment.Middle;
339+
this.loadingToast.autoHide = false;
340+
this.loadingToast.open('Loading Remote Data...');
341+
this.cdr.detectChanges();
342+
343+
this.simpleComboPrevRequest = this.remoteService.getData(
344+
this.remoteSimpleCombo.virtualizationState,
345+
this.searchText,
346+
(data) => {
347+
this.remoteSimpleCombo.totalItemCount = data['@odata.count'];
348+
this.loadingToast.close();
349+
this.cdr.detectChanges();
350+
}
351+
);
352+
}
353+
354+
public onSimpleComboOpened() {
355+
const scroll: number = this.remoteSimpleCombo.virtualScrollContainer.getScrollForIndex(this.itemID - 1);
356+
this.remoteSimpleCombo.virtualScrollContainer.scrollPosition = scroll + this.additionalScroll;
357+
this.cdr.detectChanges();
358+
}
359+
360+
public onSimpleComboClosing() {
361+
this.searchText = '';
362+
}
363+
364+
public onSimpleComboClosed() {
365+
this.currentVirtState.startIndex = (this.itemID || 1) - 1;
366+
this.remoteService.getData(
367+
this.currentVirtState,
368+
this.searchText,
369+
(data) => {
370+
this.remoteSimpleCombo.totalItemCount = data['@odata.count'];
371+
this.cdr.detectChanges();
372+
}
373+
);
374+
}
375+
376+
public onSimpleComboSelectionChanging(evt: ISimpleComboSelectionChangingEventArgs) {
377+
this.hasSelection = evt.newSelection !== undefined;
378+
379+
if (!this.hasSelection) {
380+
this.itemID = 1;
381+
this.currentVirtState = this.defaultVirtState;
382+
return;
383+
}
384+
385+
this.currentVirtState.chunkSize = Math.ceil(this.remoteSimpleCombo.itemsMaxHeight / this.remoteSimpleCombo.itemHeight);
386+
387+
this.itemCount === evt.newSelection ?
388+
this.additionalScroll = this.remoteSimpleCombo.itemHeight :
389+
this.additionalScroll = 0;
390+
391+
if (this.itemCount - evt.newSelection >= this.currentVirtState.chunkSize - 1) {
392+
this.itemID = this.currentVirtState.startIndex = evt.newSelection;
393+
} else {
394+
this.itemID = this.currentVirtState.startIndex = this.itemCount - (this.currentVirtState.chunkSize - 1);
395+
}
396+
}
397+
398+
public onSimpleComboSearchInputUpdate(searchData: IComboSearchInputEventArgs) {
399+
this.currentVirtState.startIndex = 0;
400+
this.currentVirtState.chunkSize = Math.ceil(this.remoteSimpleCombo.itemsMaxHeight / this.remoteSimpleCombo.itemHeight);
401+
this.searchText = searchData?.searchText || '';
402+
this.remoteService.getData(
403+
this.searchText ? this.currentVirtState : this.defaultVirtState,
404+
this.searchText,
405+
(data) => {
406+
this.remoteSimpleCombo.totalItemCount = data['@odata.count'];
407+
}
408+
);
409+
}
322410
}

0 commit comments

Comments
 (0)