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

IgniteUI / igniteui-angular / 23848129113

01 Apr 2026 12:17PM UTC coverage: 90.163% (+0.7%) from 89.452%
23848129113

Pull #16784

github

web-flow
Merge b19ccc7d2 into f00a27f08
Pull Request #16784: fix(grid-pinning): remove hide call in scrollToRow - master

14823 of 17264 branches covered (85.86%)

Branch coverage included in aggregate %.

15 of 17 new or added lines in 2 files covered. (88.24%)

264 existing lines in 15 files now uncovered.

29886 of 32323 relevant lines covered (92.46%)

34071.84 hits per line

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

94.89
/projects/igniteui-angular/combo/src/combo/combo.component.ts
1
import { NgClass, NgTemplateOutlet } from '@angular/common';
2
import { AfterViewInit, Component, OnInit, OnDestroy, ViewChild, Input, Output, EventEmitter, HostListener, DoCheck, booleanAttribute } from '@angular/core';
3

4
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
5

6
import {
7
    IBaseEventArgs,
8
    IBaseCancelableEventArgs,
9
    CancelableEventArgs,
10
    EditorProvider
11
} from 'igniteui-angular/core';
12
import { IgxForOfDirective } from 'igniteui-angular/directives';
13
import { IgxRippleDirective } from 'igniteui-angular/directives';
14
import { IgxButtonDirective } from 'igniteui-angular/directives';
15
import { IgxComboItemComponent } from './combo-item.component';
16
import { IgxComboDropDownComponent } from './combo-dropdown.component';
17
import { IgxComboFilteringPipe, IgxComboGroupingPipe } from './combo.pipes';
18
import { IGX_COMBO_COMPONENT, IgxComboBaseDirective } from './combo.common';
19
import { IgxComboAddItemComponent } from './combo-add-item.component';
20
import { IgxComboAPIService } from './combo.api';
21
import { IgxInputGroupComponent, IgxInputDirective, IgxReadOnlyInputDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
22
import { IgxIconComponent } from 'igniteui-angular/icon';
23
import { IgxDropDownItemNavigationDirective } from 'igniteui-angular/drop-down';
24

25
/** Event emitted when the Combo's selection has changed */
26
export interface IComboSelectionChangedEventArgs extends IBaseEventArgs {
27
    /** An array containing the old selection values */
28
    oldValue: any[];
29
    /** An array containing the new selection items */
30
    newValue: any[];
31
    /** An array containing the old selection items */
32
    oldSelection: any[];
33
    /** An array containing the new selection items */
34
    newSelection: any[];
35
    /** An array containing the items added to the selection (if any) */
36
    added: any[];
37
    /** An array containing the items removed from the selection (if any) */
38
    removed: any[];
39
    /** The display text of the combo text box */
40
    displayText: string;
41
    /** The user interaction that triggered the selection change */
42
    event?: Event;
43
}
44

45
/** Event emitted when the Combo's selection is changing */
46
export interface IComboSelectionChangingEventArgs extends IComboSelectionChangedEventArgs, IBaseCancelableEventArgs {}
47

48
/** Event emitted when the igx-combo's search input changes */
49
export interface IComboSearchInputEventArgs extends IBaseCancelableEventArgs {
50
    /** The text that has been typed into the search input */
51
    searchText: string;
52
}
53

54
export interface IComboItemAdditionEvent extends IBaseEventArgs, CancelableEventArgs {
55
    oldCollection: any[];
56
    addedItem: any;
57
    newCollection: any[];
58
}
59

60
/**
61
 * When called with sets A & B, returns A - B (as array);
62
 *
63
 * @hidden
64
 */
65
const diffInSets = (set1: Set<any>, set2: Set<any>): any[] => {
3✔
66
    const results = [];
491✔
67
    set1.forEach(entry => {
491✔
68
        if (!set2.has(entry)) {
1,260✔
69
            results.push(entry);
748✔
70
        }
71
    });
72
    return results;
491✔
73
};
74

75
/**
76
 *  Represents a drop-down list that provides editable functionalities, allowing users to choose an option from a predefined list.
77
 *
78
 * @igxModule IgxComboModule
79
 * @igxTheme igx-combo-theme
80
 * @igxKeywords combobox, combo selection
81
 * @igxGroup Grids & Lists
82
 *
83
 * @remarks
84
 * It provides the ability to filter items as well as perform selection with the provided data.
85
 * Additionally, it exposes keyboard navigation and custom styling capabilities.
86
 * @example
87
 * ```html
88
 * <igx-combo [itemsMaxHeight]="250" [data]="locationData"
89
 *  [displayKey]="'field'" [valueKey]="'field'"
90
 *  placeholder="Location(s)" searchPlaceholder="Search...">
91
 * </igx-combo>
92
 * ```
93
 */
94
@Component({
95
    selector: 'igx-combo',
96
    templateUrl: 'combo.component.html',
97
    providers: [
98
        IgxComboAPIService,
99
        { provide: IGX_COMBO_COMPONENT, useExisting: IgxComboComponent },
100
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxComboComponent, multi: true }
101
    ],
102
    imports: [
103
        NgTemplateOutlet,
104
        NgClass,
105
        FormsModule,
106
        IgxInputGroupComponent,
107
        IgxInputDirective,
108
        IgxSuffixDirective,
109
        IgxIconComponent,
110
        IgxComboDropDownComponent,
111
        IgxDropDownItemNavigationDirective,
112
        IgxForOfDirective,
113
        IgxComboItemComponent,
114
        IgxComboAddItemComponent,
115
        IgxButtonDirective,
116
        IgxRippleDirective,
117
        IgxReadOnlyInputDirective,
118
        IgxComboFilteringPipe,
119
        IgxComboGroupingPipe
120
    ]
121
})
122
export class IgxComboComponent extends IgxComboBaseDirective implements AfterViewInit, ControlValueAccessor, OnInit,
3✔
123
    OnDestroy, DoCheck, EditorProvider {
124
    /**
125
     * Whether the combo's search box should be focused after the dropdown is opened.
126
     * When `false`, the combo's list item container will be focused instead
127
     */
128
    @Input({ transform: booleanAttribute })
129
    public autoFocusSearch = true;
317✔
130

131
    /**
132
     * Defines the placeholder value for the combo dropdown search field
133
     *
134
     * @deprecated in version 18.2.0. Replaced with values in the localization resource strings.
135
     *
136
     * ```typescript
137
     * // get
138
     * let myComboSearchPlaceholder = this.combo.searchPlaceholder;
139
     * ```
140
     *
141
     * ```html
142
     * <!--set-->
143
     * <igx-combo [searchPlaceholder]='newPlaceHolder'></igx-combo>
144
     * ```
145
     */
146
    @Input()
147
    public searchPlaceholder: string;
148

149
    /**
150
     * Emitted when item selection is changing, before the selection completes
151
     *
152
     * ```html
153
     * <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
154
     * ```
155
     */
156
    @Output()
157
    public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
317✔
158

159
    /**
160
     * Emitted when item selection is changed, after the selection completes
161
     *
162
     * ```html
163
     * <igx-combo (selectionChanged)='handleSelection()'></igx-combo>
164
     * ```
165
     */
166
    @Output()
167
    public selectionChanged = new EventEmitter<IComboSelectionChangedEventArgs>();
317✔
168

169
    /** @hidden @internal */
170
    @ViewChild(IgxComboDropDownComponent, { static: true })
171
    public dropdown: IgxComboDropDownComponent;
172

173
    /** @hidden @internal */
174
    public get filteredData(): any[] | null {
175
        return this.disableFiltering ? this.data : this._filteredData;
9,436✔
176
    }
177
    /** @hidden @internal */
178
    public set filteredData(val: any[] | null) {
179
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
10,231!
180
        this.checkMatch();
920✔
181
    }
182

183
    protected _prevInputValue = '';
317✔
184

185
    private _displayText: string;
186

187
    constructor() {
188
        super();
317✔
189
        this.comboAPI.register(this);
317✔
190
    }
191

192
    @HostListener('keydown.ArrowDown', ['$event'])
193
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
194
    public onArrowDown(event: Event) {
195
        event.preventDefault();
2✔
196
        event.stopPropagation();
2✔
197
        this.open();
2✔
198
    }
199

200
    @HostListener('keydown.Escape', ['$event'])
201
    public onEscape(event: Event) {
202
        event.stopPropagation();
3✔
203
        if (this.collapsed) {
3✔
204
            this.deselectAllItems(true, event);
2✔
205
        }
206
    }
207

208
    /** @hidden @internal */
209
    public get displaySearchInput(): boolean {
210
        return !this.disableFiltering || this.allowCustomValues;
7,758✔
211
    }
212

213
    /**
214
     * @hidden @internal
215
     */
216
    public handleKeyUp(event: KeyboardEvent): void {
217
        // TODO: use PlatformUtil for keyboard navigation
218
        if (event.key === 'ArrowDown' || event.key === 'Down') {
12✔
219
            this.dropdown.focusedItem = this.dropdown.items[0];
6✔
220
            this.dropdownContainer.nativeElement.focus();
6✔
221
        } else if (event.key === 'Escape' || event.key === 'Esc') {
6✔
222
            this.toggle();
2✔
223
        }
224
    }
225

226
    /**
227
     * @hidden @internal
228
     */
229
    public handleSelectAll(evt) {
230
        if (evt.checked) {
2✔
231
            this.selectAllItems();
1✔
232
        } else {
233
            this.deselectAllItems();
1✔
234
        }
235
    }
236

237
    /**
238
     * @hidden @internal
239
     */
240
    public writeValue(value: any[]): void {
241
        const selection = Array.isArray(value) ? value.filter(x => x !== undefined) : [];
1,107✔
242
        const oldSelection = this.selection;
548✔
243
        this.selectionService.select_items(this.id, selection, true);
548✔
244
        this.cdr.markForCheck();
548✔
245
        this._displayValue = this.createDisplayText(this.selection, oldSelection);
548✔
246
        this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
1,078✔
247
    }
248

249
    /** @hidden @internal */
250
    public ngDoCheck(): void {
251
        if (this.data?.length && this.selection.length) {
3,785✔
252
            this._displayValue = this._displayText || this.createDisplayText(this.selection, []);
1,209✔
253
            this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
5,204✔
254
        }
255
    }
256

257
    /**
258
     * @hidden
259
     */
260
    public getEditElement(): HTMLElement {
261
        return this.comboInput.nativeElement;
4✔
262
    }
263

264
    /**
265
     * @hidden @internal
266
     */
267
    public get context(): any {
UNCOV
268
        return {
×
269
            $implicit: this
270
        };
271
    }
272

273
    /**
274
     * @hidden @internal
275
     */
276
    public handleClearItems(event: Event): void {
277
        if (this.disabled) {
9✔
278
            return;
1✔
279
        }
280
        this.deselectAllItems(true, event);
8✔
281
        if (this.collapsed) {
8✔
282
            this.getEditElement().focus();
4✔
283
        } else {
284
            this.focusSearchInput(true);
4✔
285
        }
286
        event.stopPropagation();
8✔
287
    }
288

289
    /**
290
     * Select defined items
291
     *
292
     * @param newItems new items to be selected
293
     * @param clearCurrentSelection if true clear previous selected items
294
     * ```typescript
295
     * this.combo.select(["New York", "New Jersey"]);
296
     * ```
297
     */
298
    public select(newItems: Array<any>, clearCurrentSelection?: boolean, event?: Event) {
299
        if (newItems) {
86✔
300
            const newSelection = this.selectionService.add_items(this.id, newItems, clearCurrentSelection);
86✔
301
            this.setSelection(newSelection, event);
86✔
302
        }
303
    }
304

305
    /**
306
     * Deselect defined items
307
     *
308
     * @param items items to deselected
309
     * ```typescript
310
     * this.combo.deselect(["New York", "New Jersey"]);
311
     * ```
312
     */
313
    public deselect(items: Array<any>, event?: Event) {
314
        if (items) {
19✔
315
            const newSelection = this.selectionService.delete_items(this.id, items);
19✔
316
            this.setSelection(newSelection, event);
19✔
317
        }
318
    }
319

320
    /**
321
     * Select all (filtered) items
322
     *
323
     * @param ignoreFilter if set to true, selects all items, otherwise selects only the filtered ones.
324
     * ```typescript
325
     * this.combo.selectAllItems();
326
     * ```
327
     */
328
    public selectAllItems(ignoreFilter?: boolean, event?: Event) {
329
        const allVisible = this.selectionService.get_all_ids(ignoreFilter ? this.data : this.filteredData, this.valueKey);
5✔
330
        const newSelection = this.selectionService.add_items(this.id, allVisible);
5✔
331
        this.setSelection(newSelection, event);
5✔
332
    }
333

334
    /**
335
     * Deselect all (filtered) items
336
     *
337
     * @param ignoreFilter if set to true, deselects all items, otherwise deselects only the filtered ones.
338
     * ```typescript
339
     * this.combo.deselectAllItems();
340
     * ```
341
     */
342
    public deselectAllItems(ignoreFilter?: boolean, event?: Event): void {
343
        let newSelection = this.selectionService.get_empty();
14✔
344
        if (this.filteredData.length !== this.data.length && !ignoreFilter) {
14✔
345
            newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
1✔
346
        }
347
        this.setSelection(newSelection, event);
14✔
348
    }
349

350
    /**
351
     * Selects/Deselects a single item
352
     *
353
     * @param itemID the itemID of the specific item
354
     * @param select If the item should be selected (true) or deselected (false)
355
     *
356
     * Without specified valueKey;
357
     * ```typescript
358
     * this.combo.valueKey = null;
359
     * const items: { field: string, region: string}[] = data;
360
     * this.combo.setSelectedItem(items[0], true);
361
     * ```
362
     * With specified valueKey;
363
     * ```typescript
364
     * this.combo.valueKey = 'field';
365
     * const items: { field: string, region: string}[] = data;
366
     * this.combo.setSelectedItem('Connecticut', true);
367
     * ```
368
     */
369
    public setSelectedItem(itemID: any, select = true, event?: Event): void {
×
370
        if (itemID === undefined) {
7!
UNCOV
371
            return;
×
372
        }
373
        if (select) {
7✔
374
            this.select([itemID], false, event);
4✔
375
        } else {
376
            this.deselect([itemID], event);
3✔
377
        }
378
    }
379

380
    /** @hidden @internal */
381
    public handleOpened() {
382
        this.triggerCheck();
59✔
383

384
        // Disabling focus of the search input should happen only when drop down opens.
385
        // During keyboard navigation input should receive focus, even the autoFocusSearch is disabled.
386
        // That is why in such cases focusing of the dropdownContainer happens outside focusSearchInput method.
387
        if (this.autoFocusSearch) {
59✔
388
            this.focusSearchInput(true);
56✔
389
        } else {
390
            this.dropdownContainer.nativeElement.focus();
3✔
391
        }
392
        this.opened.emit({ owner: this });
59✔
393
    }
394

395
    /** @hidden @internal */
396
    public focusSearchInput(opening?: boolean): void {
397
        if (this.displaySearchInput && this.searchInput) {
61✔
398
            this.searchInput.nativeElement.focus();
60✔
399
        } else {
400
            if (opening) {
1!
401
                this.dropdownContainer.nativeElement.focus();
1✔
402
            } else {
403
                this.comboInput.nativeElement.focus();
×
UNCOV
404
                this.toggle();
×
405
            }
406
        }
407
    }
408

409
    protected setSelection(selection: Set<any>, event?: Event): void {
410
        const currentSelection = this.selectionService.get(this.id);
124✔
411
        const removed = this.convertKeysToItems(diffInSets(currentSelection, selection));
124✔
412
        const added = this.convertKeysToItems(diffInSets(selection, currentSelection));
124✔
413
        const newValue = Array.from(selection);
124✔
414
        const oldValue = Array.from(currentSelection || []);
124!
415
        const newSelection = this.convertKeysToItems(newValue);
124✔
416
        const oldSelection = this.convertKeysToItems(oldValue);
124✔
417
        const displayText = this.createDisplayText(this.convertKeysToItems(newValue), oldValue);
124✔
418
        const args: IComboSelectionChangingEventArgs = {
124✔
419
            newValue,
420
            oldValue,
421
            newSelection,
422
            oldSelection,
423
            added,
424
            removed,
425
            event,
426
            owner: this,
427
            displayText,
428
            cancel: false
429
        };
430
        this.selectionChanging.emit(args);
124✔
431
        if (!args.cancel) {
124✔
432
            this.selectionService.select_items(this.id, args.newValue, true);
121✔
433
            this._value = args.newValue;
121✔
434
            if (displayText !== args.displayText) {
121✔
435
                this._displayValue = this._displayText = args.displayText;
3✔
436
            } else {
437
                this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
118✔
438
            }
439
            this._onChangeCallback(this.value);
121✔
440
            const changedArgs: IComboSelectionChangedEventArgs = {
121✔
441
                newValue: this.value,
442
                oldValue,
443
                newSelection: this.selection,
444
                oldSelection,
445
                added: this.convertKeysToItems(diffInSets(new Set(this.value), new Set(oldValue))),
446
                removed: this.convertKeysToItems(diffInSets(new Set(oldValue), new Set(this.value))),
447
                event,
448
                owner: this,
449
                displayText: this._displayValue
450
            };
451
            this.selectionChanged.emit(changedArgs);
121✔
452
        } else if (this.isRemote) {
3✔
453
            this.registerRemoteEntries(diffInSets(selection, currentSelection), false);
1✔
454
        }
455
    }
456

457
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
458
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
6,850✔
459
        return this.isRemote
1,998✔
460
            ? this.getRemoteSelection(selection, oldSelection)
461
            : this.concatDisplayText(newSelection);
462
    }
463

464
    protected getSearchPlaceholderText(): string {
465
        return this.searchPlaceholder ||
7,679✔
466
            (this.disableFiltering ? this.resourceStrings.igx_combo_addCustomValues_placeholder : this.resourceStrings.igx_combo_filter_search_placeholder);
1,308✔
467
    }
468

469
    /** Returns a string that should be populated in the combo's text box */
470
    private concatDisplayText(selection: any[]): string {
471
        const value = this.displayKey !== null && this.displayKey !== undefined ?
1,957✔
472
            selection.map(entry => entry[this.displayKey]).join(', ') :
6,854✔
473
            selection.join(', ');
474
        return value;
1,957✔
475
    }
476
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc