• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 29572405564

17 Jul 2026 10:07AM UTC coverage: 90.116% (-0.02%) from 90.135%
29572405564

Pull #15125

github

web-flow
Merge 637b7229e into 8ebabbb38
Pull Request #15125: refactor(*): bundle styles with components

14920 of 17397 branches covered (85.76%)

Branch coverage included in aggregate %.

30030 of 32483 relevant lines covered (92.45%)

34495.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.12
/projects/igniteui-angular/simple-combo/src/simple-combo/simple-combo.component.ts
1
import { NgTemplateOutlet } from '@angular/common';
2
import { AfterViewInit, Component, DoCheck, EventEmitter, HostListener, Output, ViewChild, ViewEncapsulation, inject, ChangeDetectionStrategy } from '@angular/core';
3
import { ControlValueAccessor, FormGroupDirective, NG_VALUE_ACCESSOR } from '@angular/forms';
4
import { takeUntil } from 'rxjs/operators';
5

6
import { CancelableEventArgs, IBaseCancelableBrowserEventArgs, IBaseEventArgs, PlatformUtil } from 'igniteui-angular/core';
7
import { IgxButtonDirective } from 'igniteui-angular/directives';
8
import { IgxForOfDirective } from 'igniteui-angular/directives';
9
import { IgxRippleDirective } from 'igniteui-angular/directives';
10
import { IgxTextSelectionDirective } from 'igniteui-angular/directives';
11
import { IgxInputGroupComponent, IgxInputDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
12
import { IgxIconComponent } from 'igniteui-angular/icon';
13
import { IGX_COMBO_COMPONENT, IgxComboAddItemComponent, IgxComboAPIService, IgxComboBaseDirective, IgxComboDropDownComponent, IgxComboFilteringPipe, IgxComboGroupingPipe, IgxComboItemComponent } from 'igniteui-angular/combo';
14
import { IgxDropDownItemNavigationDirective } from 'igniteui-angular/drop-down';
15

16
/** Emitted when the Combo's selection has changed. */
17
export interface ISimpleComboSelectionChangedEventArgs extends IBaseEventArgs {
18
    /** The old selection value */
19
    oldValue: any;
20
    /** The new selection value */
21
    newValue: any;
22
    /** The old selection item */
23
    oldSelection: any;
24
    /** The new selection item */
25
    newSelection: any;
26
    /** The display text of the combo text box */
27
    displayText: string;
28
}
29

30
/** Emitted when the Combo's selection is changing. */
31
export interface ISimpleComboSelectionChangingEventArgs extends ISimpleComboSelectionChangedEventArgs, CancelableEventArgs {}
32

33
/**
34
 * Represents a drop-down list that provides filtering functionality, allowing users to choose a single option from a predefined list.
35
 *
36
 * @igxModule IgxSimpleComboModule
37
 * @igxTheme igx-combo-theme
38
 * @igxKeywords combobox, single combo selection
39
 * @igxGroup Grids & Lists
40
 *
41
 * @remarks
42
 * It provides the ability to filter items as well as perform single selection on the provided data.
43
 * Additionally, it exposes keyboard navigation and custom styling capabilities.
44
 * @example
45
 * ```html
46
 * <igx-simple-combo [itemsMaxHeight]="250" [data]="locationData"
47
 *  [displayKey]="'field'" [valueKey]="'field'"
48
 *  placeholder="Location" searchPlaceholder="Search...">
49
 * </igx-simple-combo>
50
 * ```
51
 */
52
@Component({
53
    selector: 'igx-simple-combo',
54
    templateUrl: 'simple-combo.component.html',
55
    styleUrls: ['../../../combo/src/combo/combo.component.css'],
56
    providers: [
57
        IgxComboAPIService,
58
        { provide: IGX_COMBO_COMPONENT, useExisting: IgxSimpleComboComponent },
59
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxSimpleComboComponent, multi: true }
60
    ],
61
    encapsulation: ViewEncapsulation.None,
62
    changeDetection: ChangeDetectionStrategy.Eager,
63
    imports: [IgxInputGroupComponent, IgxInputDirective, IgxTextSelectionDirective, IgxSuffixDirective, NgTemplateOutlet, IgxIconComponent, IgxComboDropDownComponent, IgxDropDownItemNavigationDirective, IgxForOfDirective, IgxComboItemComponent, IgxComboAddItemComponent, IgxButtonDirective, IgxRippleDirective, IgxComboFilteringPipe, IgxComboGroupingPipe]
64
})
65
export class IgxSimpleComboComponent extends IgxComboBaseDirective implements ControlValueAccessor, AfterViewInit, DoCheck {
3✔
66
    private platformUtil = inject(PlatformUtil);
147✔
67
    private formGroupDirective = inject(FormGroupDirective, { optional: true });
147✔
68

69
    /** @hidden @internal */
70
    @ViewChild(IgxComboDropDownComponent, { static: true })
71
    public dropdown: IgxComboDropDownComponent;
72

73
    /** @hidden @internal */
74
    @ViewChild(IgxComboAddItemComponent)
75
    public addItem: IgxComboAddItemComponent;
76

77
    /**
78
     * Emitted when item selection is changing, before the selection completes
79
     *
80
     * ```html
81
     * <igx-simple-combo (selectionChanging)='handleSelection()'></igx-simple-combo>
82
     * ```
83
     */
84
    @Output()
85
    public selectionChanging = new EventEmitter<ISimpleComboSelectionChangingEventArgs>();
147✔
86

87
    /**
88
     * Emitted when item selection is changed, after the selection completes
89
     *
90
     * ```html
91
     * <igx-simple-combo (selectionChanged)='handleSelection()'></igx-simple-combo>
92
     * ```
93
     */
94
    @Output()
95
    public selectionChanged = new EventEmitter<ISimpleComboSelectionChangedEventArgs>();
147✔
96

97
    @ViewChild(IgxTextSelectionDirective, { static: true })
98
    private textSelection: IgxTextSelectionDirective;
99

100
    public override get value(): any {
101
        return this._value[0];
249✔
102
    }
103

104
    /**
105
     * Get current selection state
106
     *
107
     * @returns The selected item, if any
108
     * ```typescript
109
     * let mySelection = this.combo.selection;
110
     * ```
111
     */
112
    public override get selection(): any {
113
        return super.selection[0];
247✔
114
    }
115

116
    /** @hidden @internal */
117
    public composing = false;
147✔
118

119
    private _updateInput = true;
147✔
120

121
    private _collapsing = false;
147✔
122

123
    /** @hidden @internal */
124
    public get filteredData(): any[] | null {
125
        return this._filteredData;
1,779✔
126
    }
127
    /** @hidden @internal */
128
    public set filteredData(val: any[] | null) {
129
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
8,245!
130
        this.checkMatch();
322✔
131
    }
132

133
    /** @hidden @internal */
134
    public override get searchValue(): string {
135
        return this._searchValue;
15,689✔
136
    }
137
    public override set searchValue(val: string) {
138
        this._searchValue = val;
336✔
139
    }
140

141
    private get selectedItem(): any {
142
        return this.selectionService.get(this.id).values().next().value;
156✔
143
    }
144

145
    protected get hasSelectedItem(): boolean {
146
        return !!this.selectionService.get(this.id).size;
1,177✔
147
    }
148

149
    constructor() {
150
        super();
147✔
151
        this.comboAPI.register(this);
147✔
152
    }
153

154
    /** @hidden @internal */
155
    @HostListener('keydown.ArrowDown', ['$event'])
156
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
157
    public onArrowDown(event: Event): void {
158
        if (this.collapsed) {
8✔
159
            event.preventDefault();
3✔
160
            event.stopPropagation();
3✔
161
            this.open();
3✔
162
        } else {
163
            if (this.virtDir.igxForOf.length > 0 && !this.hasSelectedItem) {
5✔
164
                this.dropdown.navigateNext();
4✔
165
                this.dropdownContainer.nativeElement.focus();
4✔
166
            } else if (this.allowCustomValues) {
1✔
167
                this.addItem?.element.nativeElement.focus();
1✔
168
            }
169
        }
170
    }
171

172
    /**
173
     * Select a defined item
174
     *
175
     * @param item the item to be selected
176
     * ```typescript
177
     * this.combo.select("New York");
178
     * ```
179
     */
180
    public select(item: any): void {
181
        if (item !== undefined) {
83✔
182
            const newSelection = this.selectionService.add_items(this.id, item instanceof Array ? item : [item], true);
83✔
183
            this.setSelection(newSelection);
83✔
184
        }
185
    }
186

187
    /**
188
     * Deselect the currently selected item
189
     *
190
     * @param item the items to be deselected
191
     * ```typescript
192
     * this.combo.deselect("New York");
193
     * ```
194
     */
195
    public deselect(): void {
196
        this.clearSelection();
4✔
197
    }
198

199
    /** @hidden @internal */
200
    public writeValue(value: any): void {
201
        const oldSelection = super.selection;
107✔
202
        this.selectionService.select_items(this.id, this.isValid(value) ? [value] : [], true);
107✔
203
        this.cdr.markForCheck();
107✔
204
        this._displayValue = this.createDisplayText(super.selection, oldSelection);
107✔
205
        this._value = this.valueKey ? super.selection.map(item => item[this.valueKey]) : super.selection;
107✔
206
        this.searchValue = this.filterValue = this._displayValue?.toString() || '';
107✔
207
    }
208

209
    /** @hidden @internal */
210
    public override ngAfterViewInit(): void {
211
        this.virtDir.contentSizeChange.pipe(takeUntil(this.destroy$)).subscribe(() => {
129✔
212
            if (super.selection.length > 0) {
1✔
213
                const index = this.virtDir.igxForOf.findIndex(e => {
1✔
214
                    let current = e ? e[this.valueKey] : undefined;
10!
215
                    if (this.valueKey === null || this.valueKey === undefined) {
10!
216
                        current = e;
×
217
                    }
218
                    return current === super.selection[0];
10✔
219
                });
220
                if (!this.isRemote) {
1!
221
                    // navigate to item only if we have local data
222
                    // as with remote data this will fiddle with igxFor's scroll handler
223
                    // and will trigger another chunk load which will break the visualization
224
                    this.dropdown.navigateItem(index);
×
225
                }
226
            }
227
        });
228
        this.dropdown.opening.pipe(takeUntil(this.destroy$)).subscribe((args) => {
129✔
229
            if (args.cancel) {
75!
230
                return;
×
231
            }
232
            this._collapsing = false;
75✔
233
            const filtered = this.filteredData.find(this.findAllMatches);
75✔
234
            if (filtered === undefined || filtered === null) {
75✔
235
                this.filterValue = this.searchValue = this.comboInput.value;
46✔
236
                return;
46✔
237
            }
238
        });
239
        this.dropdown.opened.pipe(takeUntil(this.destroy$)).subscribe(() => {
129✔
240
            if (this.composing) {
29✔
241
                this.comboInput.focus();
4✔
242
            }
243
        });
244
        this.dropdown.closing.pipe(takeUntil(this.destroy$)).subscribe((args) => {
129✔
245
            if (args.cancel) {
39!
246
                return;
×
247
            }
248
            if (this.getEditElement() && !args.event) {
39✔
249
                this._collapsing = true;
36✔
250
                // Only focus back when programmatically closing (no user event)
251
                // to avoid focus loops when user clicks on another combo
252
                this.comboInput.focus();
36✔
253
            } else {
254
                this.clearOnBlur();
3✔
255
                this._onTouchedCallback();
3✔
256
            }
257
        });
258

259
        // in reactive form the control is not present initially
260
        // and sets the selection to an invalid value in writeValue method
261
        if (!this.isValid(this.selectedItem)) {
129✔
262
            this.selectionService.clear(this.id);
108✔
263
            this._displayValue = '';
108✔
264
        }
265

266
        super.ngAfterViewInit();
129✔
267
    }
268

269
    /** @hidden @internal */
270
    public ngDoCheck(): void {
271
        if (this.data?.length && super.selection.length && !this._displayValue) {
499✔
272
            this._displayValue = this.createDisplayText(super.selection, []);
49✔
273
            this._value = this.valueKey ? super.selection.map(item => item[this.valueKey]) : super.selection;
49✔
274
        }
275
    }
276

277
    /** @hidden @internal */
278
    public override handleInputChange(event?: any): void {
279
        if (this.collapsed && this.comboInput.focused) {
80✔
280
            this.open();
11✔
281
        }
282
        if (event !== undefined) {
80✔
283
            this.filterValue = this.searchValue = typeof event === 'string' ? event : event.target.value;
78✔
284
        }
285
        if (!this.comboInput.value.trim() && super.selection.length) {
80✔
286
            // handle clearing of input by space
287
            this.clearSelection();
1✔
288
            this._onChangeCallback(null);
1✔
289
            this.filterValue = '';
1✔
290
        }
291
        if (super.selection.length) {
80✔
292
            const args: ISimpleComboSelectionChangingEventArgs = {
9✔
293
                newValue: undefined,
294
                oldValue: this.selectedItem,
295
                newSelection: undefined,
296
                oldSelection: this.selection,
297
                displayText: typeof event === 'string' ? event : event?.target?.value,
9✔
298
                owner: this,
299
                cancel: false
300
            };
301
            this.selectionChanging.emit(args);
9✔
302
            if (!args.cancel) {
9✔
303
                this.selectionService.select_items(this.id, [], true);
5✔
304
                const changedArgs: ISimpleComboSelectionChangedEventArgs = {
5✔
305
                    newValue: undefined,
306
                    oldValue: this.selectedItem,
307
                    newSelection: undefined,
308
                    oldSelection: this.selection,
309
                    displayText: typeof event === 'string' ? event : event?.target?.value,
5✔
310
                    owner: this
311
                };
312
                this.selectionChanged.emit(changedArgs);
5✔
313
            }
314
        }
315
        // when filtering the focused item should be the first item or the currently selected item
316
        if (!this.dropdown.focusedItem || this.dropdown.focusedItem.id !== this.dropdown.items[0].id) {
80✔
317
            this.dropdown.navigateFirst();
29✔
318
        }
319
        super.handleInputChange(event);
80✔
320
        this.composing = true;
80✔
321
    }
322

323
    /** @hidden @internal */
324
    public handleInputClick(): void {
325
        if (this.collapsed) {
4✔
326
            this.open();
2✔
327
            this.comboInput.focus();
2✔
328
        }
329
    }
330

331
    /** @hidden @internal */
332
    public override handleKeyDown(event: KeyboardEvent): void {
333
        if (event.key === this.platformUtil.KEYMAP.ENTER) {
102✔
334
            const filtered = this.filteredData.find(this.findAllMatches);
9✔
335
            if (filtered === null || filtered === undefined) {
9!
336
                return;
×
337
            }
338
            if (!this.dropdown.collapsed) {
9✔
339
                const focusedItem = this.dropdown.focusedItem;
8✔
340
                if (focusedItem && !focusedItem.isHeader) {
8✔
341
                    this.select(focusedItem.itemID);
7✔
342
                    event.preventDefault();
7✔
343
                    event.stopPropagation();
7✔
344
                    this.close();
7✔
345
                } else {
346
                    event.preventDefault();
1✔
347
                    event.stopPropagation();
1✔
348
                    this.comboInput.focus();
1✔
349
                }
350
            }
351
            // manually trigger text selection as it will not be triggered during editing
352
            this.textSelection.trigger();
9✔
353
            return;
9✔
354
        }
355
        if (event.key === this.platformUtil.KEYMAP.BACKSPACE
93✔
356
            || event.key === this.platformUtil.KEYMAP.DELETE) {
357
            this._updateInput = false;
2✔
358
            this.clearSelection(true);
2✔
359
        }
360
        if (!this.collapsed && event.key === this.platformUtil.KEYMAP.TAB) {
93✔
361
            const filtered = this.filteredData.find(this.findAllMatches);
14✔
362
            if (filtered === null || filtered === undefined) {
14✔
363
                this.clearOnBlur();
6✔
364
                this.close();
6✔
365
                return;
6✔
366
            }
367
            const focusedItem = this.dropdown.focusedItem;
8✔
368
            if (focusedItem && !focusedItem.isHeader) {
8✔
369
                this.select(focusedItem.itemID);
7✔
370
                this.close();
7✔
371
                this.textSelection.trigger();
7✔
372
            } else {
373
                this.clearOnBlur();
1✔
374
                this.close();
1✔
375
            }
376
        }
377
        if (event.key === this.platformUtil.KEYMAP.ESCAPE) {
87✔
378
            event.stopPropagation();
3✔
379
            if (this.collapsed) {
3✔
380
                const oldSelection = this.selection;
2✔
381
                this.clearSelection(true);
2✔
382
                if (this.selection !== oldSelection) {
2✔
383
                    this.comboInput.value = this.filterValue = this.searchValue = '';
2✔
384
                }
385
                this.dropdown.focusedItem = null;
2✔
386
                this.comboInput.focus();
2✔
387
            } else {
388
                this.close();
1✔
389
            }
390
        }
391
        this.composing = false;
87✔
392
        super.handleKeyDown(event);
87✔
393
    }
394

395
    /** @hidden @internal */
396
    public handleKeyUp(event: KeyboardEvent): void {
397
        if (event.key === this.platformUtil.KEYMAP.ARROW_DOWN) {
67✔
398
            this.dropdown.focusedItem = this.hasSelectedItem && this.filteredData.length > 0
3!
399
                ? this.dropdown.items.find(i => i.itemID === this.selectedItem)
×
400
                : this.dropdown.items[0];
401
            this.dropdownContainer.nativeElement.focus();
3✔
402
        }
403
    }
404

405
    /** @hidden @internal */
406
    public handleItemKeyDown(event: KeyboardEvent): void {
407
        if (event.key === this.platformUtil.KEYMAP.ARROW_UP && event.altKey) {
8!
408
            this.close();
×
409
            this.comboInput.focus();
×
410
            return;
×
411
        }
412
        if (event.key === this.platformUtil.KEYMAP.ENTER) {
8!
413
            this.comboInput.focus();
×
414
        }
415
    }
416

417
    /** @hidden @internal */
418
    public handleItemClick(): void {
419
        this.close();
27✔
420
        this.comboInput.focus();
27✔
421
    }
422

423
    /** @hidden @internal */
424
    public override onBlur(): void {
425
        // when clicking the toggle button to close the combo and immediately clicking outside of it
426
        // the collapsed state is not modified as the dropdown is still not closed
427
        if (this.collapsed || this._collapsing) {
15✔
428
            this.clearOnBlur();
10✔
429
        }
430
        super.onBlur();
15✔
431
    }
432

433
    /** @hidden @internal */
434
    public getEditElement(): HTMLElement {
435
        return this.comboInput.nativeElement;
39✔
436
    }
437

438
    /** @hidden @internal */
439
    public handleClear(event: Event): void {
440
        if (this.disabled) {
7✔
441
            return;
1✔
442
        }
443

444
        const oldSelection = this.selection;
6✔
445
        this.clearSelection(true);
6✔
446

447
        if (!this.collapsed) {
6✔
448
            this.focusSearchInput(true);
2✔
449
        }
450
        event.stopPropagation();
6✔
451

452
        if (this.selection !== oldSelection) {
6✔
453
            this.comboInput.value = this.filterValue = this.searchValue = '';
5✔
454
        }
455

456
        this.dropdown.focusedItem = null;
6✔
457
        this.composing = false;
6✔
458
        this.comboInput.focus();
6✔
459
    }
460

461
    /** @hidden @internal */
462
    public handleOpened(): void {
463
        this.triggerCheck();
29✔
464
        if (!this.comboInput.focused) {
29✔
465
            this.dropdownContainer.nativeElement.focus();
16✔
466
        }
467
        this.opened.emit({ owner: this });
29✔
468
    }
469

470
    /** @hidden @internal */
471
    public override handleClosing(e: IBaseCancelableBrowserEventArgs): void {
472
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
41✔
473
        this.closing.emit(args);
41✔
474
        e.cancel = args.cancel;
41✔
475
        if (e.cancel) {
41✔
476
            return;
1✔
477
        }
478

479
        this.composing = false;
40✔
480
        // explicitly update selection so that we don't have to force CD
481
        const isTab = (e.event as KeyboardEvent)?.key === this.platformUtil.KEYMAP.TAB;
40✔
482
        this.textSelection.selected = !isTab;
40✔
483
    }
484

485
    /** @hidden @internal */
486
    public focusSearchInput(opening?: boolean): void {
487
        if (opening) {
8✔
488
            this.dropdownContainer.nativeElement.focus();
2✔
489
        } else {
490
            this.comboInput.nativeElement.focus();
6✔
491
        }
492
    }
493

494
    /** @hidden @internal */
495
    public override onClick(event: Event): void {
496
        super.onClick(event);
8✔
497
        if (this.comboInput.value.length === 0) {
8✔
498
            this.virtDir.scrollTo(0);
6✔
499
        }
500
    }
501

502
    protected findAllMatches = (element: any): boolean => {
147✔
503
        const value = this.displayKey ? element[this.displayKey] : element;
1,975✔
504
        if (value === null || value === undefined || value === '') {
1,975✔
505
            // we can accept null, undefined and empty strings as empty display values
506
            return true;
13✔
507
        }
508
        const searchValue = this.searchValue || this.comboInput.value;
1,962✔
509
        return !!searchValue && value.toString().toLowerCase().includes(searchValue.toLowerCase());
1,962✔
510
    };
511

512
    protected setSelection(newSelection: any): void {
513
        const newValueAsArray = newSelection ? Array.from(newSelection) as IgxComboItemComponent[] : [];
100✔
514
        const oldValueAsArray = Array.from(this.selectionService.get(this.id) || []);
100!
515
        const newItems = this.convertKeysToItems(newValueAsArray);
100✔
516
        const oldItems = this.convertKeysToItems(oldValueAsArray);
100✔
517
        const displayText = this.createDisplayText(this.convertKeysToItems(newValueAsArray), oldValueAsArray);
100✔
518
        const args: ISimpleComboSelectionChangingEventArgs = {
100✔
519
            newValue: newValueAsArray[0],
520
            oldValue: oldValueAsArray[0],
521
            newSelection: newItems[0],
522
            oldSelection: oldItems[0],
523
            displayText,
524
            owner: this,
525
            cancel: false
526
        };
527
        const shouldEmitSelectionEvents = args.newSelection !== args.oldSelection;
100✔
528
        if (shouldEmitSelectionEvents) {
100✔
529
            this.selectionChanging.emit(args);
95✔
530
        }
531
        // TODO: refactor below code as it sets the selection and the display text
532
        if (!args.cancel) {
100✔
533
            let argsSelection = this.isValid(args.newValue)
97✔
534
                ? args.newValue
535
                : [];
536
            argsSelection = Array.isArray(argsSelection) ? argsSelection : [argsSelection];
97✔
537
            this.selectionService.select_items(this.id, argsSelection, true);
97✔
538
            this._value = argsSelection;
97✔
539
            if (this._updateInput) {
97✔
540
                this.comboInput.value = this._displayValue = this.searchValue = displayText !== args.displayText
95✔
541
                    ? args.displayText
542
                    : this.createDisplayText(super.selection, [args.oldValue]);
543
            }
544
            this._onChangeCallback(this.value);
97✔
545
            if (shouldEmitSelectionEvents) {
97✔
546
                const changedArgs: ISimpleComboSelectionChangedEventArgs = {
92✔
547
                    newValue: this.value,
548
                    oldValue: oldValueAsArray[0],
549
                    newSelection: this.selection,
550
                    oldSelection: oldItems[0],
551
                    displayText: this._displayValue,
552
                    owner: this
553
                };
554
                this.selectionChanged.emit(changedArgs);
92✔
555
            }
556
            this._updateInput = true;
97✔
557
        } else if (this.isRemote) {
3✔
558
            this.registerRemoteEntries(newValueAsArray, false);
1✔
559
        } else {
560
            args.displayText = this.createDisplayText(oldItems, []);
2✔
561

562
            const oldSelectionArray = args.oldSelection ? [args.oldSelection] : [];
2✔
563
            this.comboInput.value = this._displayValue = this.searchValue = this.createDisplayText(oldSelectionArray, []);
2✔
564

565
            if (this.isRemote) {
2!
566
                this.registerRemoteEntries(newValueAsArray, false);
×
567
            }
568
        }
569
    }
570

571
    protected createDisplayText(newSelection: any[], oldSelection: any[]): string {
572
        if (this.isRemote) {
354✔
573
            const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
31!
574
            return this.getRemoteSelection(selection, oldSelection);
31✔
575
        }
576

577
        if (this.displayKey !== null
323✔
578
            && this.displayKey !== undefined
579
            && newSelection.length > 0) {
580
            return newSelection.filter(e => e).map(e => e[this.displayKey])[0]?.toString() || '';
148✔
581
        }
582

583
        return newSelection[0]?.toString() || '';
177✔
584
    }
585

586
    protected override getRemoteSelection(newSelection: any[], oldSelection: any[]): string {
587
        if (!newSelection.length) {
31✔
588
            this.registerRemoteEntries(oldSelection, false);
6✔
589
            return '';
6✔
590
        }
591

592
        this.registerRemoteEntries(oldSelection, false);
25✔
593
        this.registerRemoteEntries(newSelection);
25✔
594
        return Object.keys(this._remoteSelection).map(e => this._remoteSelection[e])[0] || '';
25✔
595
    }
596

597
    /** Contains key-value pairs of the selected valueKeys and their resp. displayKeys */
598
    protected override registerRemoteEntries(ids: any[], add = true) {
25✔
599
        const selection = this.getValueDisplayPairs(ids)[0];
57✔
600

601
        if (add && selection) {
57✔
602
            this._remoteSelection[selection[this.valueKey]] = selection[this.displayKey].toString();
23✔
603
        } else {
604
            this._remoteSelection = {};
34✔
605
        }
606
    }
607

608
    private clearSelection(ignoreFilter?: boolean): void {
609
        let newSelection = this.selectionService.get_empty();
31✔
610
        if (this.filteredData.length !== this.data.length && !ignoreFilter) {
31✔
611
            newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
1✔
612
        }
613
        if (this.selectionService.get(this.id).size > 0 || this.comboInput.value.trim()) {
31✔
614
            this.setSelection(newSelection);
17✔
615
        }
616
    }
617

618
    private clearOnBlur(): void {
619
        if (this.isRemote) {
20✔
620
            const searchValue = this.searchValue || this.comboInput.value;
1!
621
            const remoteValue = Object.keys(this._remoteSelection).map(e => this._remoteSelection[e])[0] || '';
1!
622
            if (searchValue !== remoteValue) {
1!
623
                this.clear();
×
624
            }
625
            return;
1✔
626
        }
627

628
        const filtered = this.filteredData.find(this.findMatch);
19✔
629
        // selecting null in primitive data returns undefined as the search text is '', but the item is null
630
        if (filtered === undefined && this.selectedItem !== null || !super.selection.length) {
19✔
631
            this.clear();
16✔
632
        }
633
    }
634

635
    private getElementVal(element: any): string {
636
        const elementVal = this.displayKey ? element[this.displayKey] : element;
×
637
        return String(elementVal);
×
638
    }
639

640
    private clear(): void {
641
        this.clearSelection(true);
16✔
642
        const oldSelection = this.selection;
16✔
643
        if (this.selection !== oldSelection) {
16!
644
            this.comboInput.value = this._displayValue = this.searchValue = '';
×
645
        }
646
    }
647

648
    private isValid(value: any): boolean {
649
        if (this.formGroupDirective && value === null) {
333✔
650
            return false;
11✔
651
        }
652

653
        if (this.required) {
322✔
654
            return value !== null && value !== '' && value !== undefined
59✔
655
        }
656

657
        return value !== undefined;
263✔
658
    }
659
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc