• 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

94.89
/projects/igniteui-angular/combo/src/combo/combo.component.ts
1
import { NgClass, NgTemplateOutlet } from '@angular/common';
2
import {
3
    AfterViewInit,
4
    Component,
5
    OnInit,
6
    OnDestroy,
7
    ViewChild,
8
    Input,
9
    Output,
10
    EventEmitter,
11
    HostListener,
12
    DoCheck,
13
    booleanAttribute,
14
    ChangeDetectionStrategy,
15
    ViewEncapsulation
16
} from '@angular/core';
17

18
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
19

20
import {
21
    IBaseEventArgs,
22
    IBaseCancelableEventArgs,
23
    CancelableEventArgs,
24
    EditorProvider
25
} from 'igniteui-angular/core';
26
import { IgxForOfDirective } from 'igniteui-angular/directives';
27
import { IgxRippleDirective } from 'igniteui-angular/directives';
28
import { IgxButtonDirective } from 'igniteui-angular/directives';
29
import { IgxComboItemComponent } from './combo-item.component';
30
import { IgxComboDropDownComponent } from './combo-dropdown.component';
31
import { IgxComboFilteringPipe, IgxComboGroupingPipe } from './combo.pipes';
32
import { IGX_COMBO_COMPONENT, IgxComboBaseDirective } from './combo.common';
33
import { IgxComboAddItemComponent } from './combo-add-item.component';
34
import { IgxComboAPIService } from './combo.api';
35
import { IgxInputGroupComponent, IgxInputDirective, IgxReadOnlyInputDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
36
import { IgxIconComponent } from 'igniteui-angular/icon';
37
import { IgxDropDownItemNavigationDirective } from 'igniteui-angular/drop-down';
38

39
/** Event emitted when the Combo's selection has changed */
40
export interface IComboSelectionChangedEventArgs extends IBaseEventArgs {
41
    /** An array containing the old selection values */
42
    oldValue: any[];
43
    /** An array containing the new selection items */
44
    newValue: any[];
45
    /** An array containing the old selection items */
46
    oldSelection: any[];
47
    /** An array containing the new selection items */
48
    newSelection: any[];
49
    /** An array containing the items added to the selection (if any) */
50
    added: any[];
51
    /** An array containing the items removed from the selection (if any) */
52
    removed: any[];
53
    /** The display text of the combo text box */
54
    displayText: string;
55
    /** The user interaction that triggered the selection change */
56
    event?: Event;
57
}
58

59
/** Event emitted when the Combo's selection is changing */
60
export interface IComboSelectionChangingEventArgs extends IComboSelectionChangedEventArgs, IBaseCancelableEventArgs {}
61

62
/** Event emitted when the igx-combo's search input changes */
63
export interface IComboSearchInputEventArgs extends IBaseCancelableEventArgs {
64
    /** The text that has been typed into the search input */
65
    searchText: string;
66
}
67

68
export interface IComboItemAdditionEvent extends IBaseEventArgs, CancelableEventArgs {
69
    oldCollection: any[];
70
    addedItem: any;
71
    newCollection: any[];
72
}
73

74
/**
75
 * When called with sets A & B, returns A - B (as array);
76
 *
77
 * @hidden
78
 */
79
const diffInSets = (set1: Set<any>, set2: Set<any>): any[] => {
3✔
80
    const results = [];
495✔
81
    set1.forEach(entry => {
495✔
82
        if (!set2.has(entry)) {
1,264✔
83
            results.push(entry);
748✔
84
        }
85
    });
86
    return results;
495✔
87
};
88

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

148
    /**
149
     * Defines the placeholder value for the combo dropdown search field
150
     *
151
     * @deprecated in version 18.2.0. Replaced with values in the localization resource strings.
152
     *
153
     * ```typescript
154
     * // get
155
     * let myComboSearchPlaceholder = this.combo.searchPlaceholder;
156
     * ```
157
     *
158
     * ```html
159
     * <!--set-->
160
     * <igx-combo [searchPlaceholder]='newPlaceHolder'></igx-combo>
161
     * ```
162
     */
163
    @Input()
164
    public searchPlaceholder: string;
165

166
    /**
167
     * Emitted when item selection is changing, before the selection completes
168
     *
169
     * ```html
170
     * <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
171
     * ```
172
     */
173
    @Output()
174
    public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
321✔
175

176
    /**
177
     * Emitted when item selection is changed, after the selection completes
178
     *
179
     * ```html
180
     * <igx-combo (selectionChanged)='handleSelection()'></igx-combo>
181
     * ```
182
     */
183
    @Output()
184
    public selectionChanged = new EventEmitter<IComboSelectionChangedEventArgs>();
321✔
185

186
    /** @hidden @internal */
187
    @ViewChild(IgxComboDropDownComponent, { static: true })
188
    public dropdown: IgxComboDropDownComponent;
189

190
    /** @hidden @internal */
191
    public get filteredData(): any[] | null {
192
        return this.disableFiltering ? this.data : this._filteredData;
9,486✔
193
    }
194
    /** @hidden @internal */
195
    public set filteredData(val: any[] | null) {
196
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
10,647!
197
        this.checkMatch();
928✔
198
    }
199

200
    protected _prevInputValue = '';
321✔
201

202
    private _displayText: string;
203

204
    constructor() {
205
        super();
321✔
206
        this.comboAPI.register(this);
321✔
207
    }
208

209
    @HostListener('keydown.ArrowDown', ['$event'])
210
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
211
    public onArrowDown(event: Event) {
212
        event.preventDefault();
2✔
213
        event.stopPropagation();
2✔
214
        this.open();
2✔
215
    }
216

217
    @HostListener('keydown.Escape', ['$event'])
218
    public onEscape(event: Event) {
219
        event.stopPropagation();
3✔
220
        if (this.collapsed) {
3✔
221
            this.deselectAllItems(true, event);
2✔
222
        }
223
    }
224

225
    /** @hidden @internal */
226
    public get displaySearchInput(): boolean {
227
        return !this.disableFiltering || this.allowCustomValues;
7,802✔
228
    }
229

230
    /**
231
     * @hidden @internal
232
     */
233
    public handleKeyUp(event: KeyboardEvent): void {
234
        // TODO: use PlatformUtil for keyboard navigation
235
        if (event.key === 'ArrowDown' || event.key === 'Down') {
12✔
236
            this.dropdown.focusedItem = this.dropdown.items[0];
6✔
237
            this.dropdownContainer.nativeElement.focus();
6✔
238
        } else if (event.key === 'Escape' || event.key === 'Esc') {
6✔
239
            this.toggle();
2✔
240
        }
241
    }
242

243
    /**
244
     * @hidden @internal
245
     */
246
    public handleSelectAll(evt) {
247
        if (evt.checked) {
2✔
248
            this.selectAllItems();
1✔
249
        } else {
250
            this.deselectAllItems();
1✔
251
        }
252
    }
253

254
    /**
255
     * @hidden @internal
256
     */
257
    public writeValue(value: any[]): void {
258
        const selection = Array.isArray(value) ? value.filter(x => x !== undefined) : [];
1,109✔
259
        const oldSelection = this.selection;
551✔
260
        this.selectionService.select_items(this.id, selection, true);
551✔
261
        this.cdr.markForCheck();
551✔
262
        this._displayValue = this.createDisplayText(this.selection, oldSelection);
551✔
263
        this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
1,078✔
264
    }
265

266
    /** @hidden @internal */
267
    public ngDoCheck(): void {
268
        if (this.data?.length && this.selection.length) {
3,797✔
269
            this._displayValue = this._displayText || this.createDisplayText(this.selection, []);
1,212✔
270
            this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
5,204✔
271
        }
272
    }
273

274
    /**
275
     * @hidden
276
     */
277
    public getEditElement(): HTMLElement {
278
        return this.comboInput.nativeElement;
4✔
279
    }
280

281
    /**
282
     * @hidden @internal
283
     */
284
    public get context(): any {
285
        return {
×
286
            $implicit: this
287
        };
288
    }
289

290
    /**
291
     * @hidden @internal
292
     */
293
    public handleClearItems(event: Event): void {
294
        if (this.disabled) {
9✔
295
            return;
1✔
296
        }
297
        this.deselectAllItems(true, event);
8✔
298
        if (this.collapsed) {
8✔
299
            this.getEditElement().focus();
4✔
300
        } else {
301
            this.focusSearchInput(true);
4✔
302
        }
303
        event.stopPropagation();
8✔
304
    }
305

306
    /**
307
     * Select defined items
308
     *
309
     * @param newItems new items to be selected
310
     * @param clearCurrentSelection if true clear previous selected items
311
     * ```typescript
312
     * this.combo.select(["New York", "New Jersey"]);
313
     * ```
314
     */
315
    public select(newItems: Array<any>, clearCurrentSelection?: boolean, event?: Event) {
316
        if (newItems) {
87✔
317
            const newSelection = this.selectionService.add_items(this.id, newItems, clearCurrentSelection);
87✔
318
            this.setSelection(newSelection, event);
87✔
319
        }
320
    }
321

322
    /**
323
     * Deselect defined items
324
     *
325
     * @param items items to deselected
326
     * ```typescript
327
     * this.combo.deselect(["New York", "New Jersey"]);
328
     * ```
329
     */
330
    public deselect(items: Array<any>, event?: Event) {
331
        if (items) {
19✔
332
            const newSelection = this.selectionService.delete_items(this.id, items);
19✔
333
            this.setSelection(newSelection, event);
19✔
334
        }
335
    }
336

337
    /**
338
     * Select all (filtered) items
339
     *
340
     * @param ignoreFilter if set to true, selects all items, otherwise selects only the filtered ones.
341
     * ```typescript
342
     * this.combo.selectAllItems();
343
     * ```
344
     */
345
    public selectAllItems(ignoreFilter?: boolean, event?: Event) {
346
        const allVisible = this.selectionService.get_all_ids(ignoreFilter ? this.data : this.filteredData, this.valueKey);
5✔
347
        const newSelection = this.selectionService.add_items(this.id, allVisible);
5✔
348
        this.setSelection(newSelection, event);
5✔
349
    }
350

351
    /**
352
     * Deselect all (filtered) items
353
     *
354
     * @param ignoreFilter if set to true, deselects all items, otherwise deselects only the filtered ones.
355
     * ```typescript
356
     * this.combo.deselectAllItems();
357
     * ```
358
     */
359
    public deselectAllItems(ignoreFilter?: boolean, event?: Event): void {
360
        let newSelection = this.selectionService.get_empty();
14✔
361
        if (this.filteredData.length !== this.data.length && !ignoreFilter) {
14✔
362
            newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
1✔
363
        }
364
        this.setSelection(newSelection, event);
14✔
365
    }
366

367
    /**
368
     * Selects/Deselects a single item
369
     *
370
     * @param itemID the itemID of the specific item
371
     * @param select If the item should be selected (true) or deselected (false)
372
     *
373
     * Without specified valueKey;
374
     * ```typescript
375
     * this.combo.valueKey = null;
376
     * const items: { field: string, region: string}[] = data;
377
     * this.combo.setSelectedItem(items[0], true);
378
     * ```
379
     * With specified valueKey;
380
     * ```typescript
381
     * this.combo.valueKey = 'field';
382
     * const items: { field: string, region: string}[] = data;
383
     * this.combo.setSelectedItem('Connecticut', true);
384
     * ```
385
     */
386
    public setSelectedItem(itemID: any, select = true, event?: Event): void {
×
387
        if (itemID === undefined) {
7!
388
            return;
×
389
        }
390
        if (select) {
7✔
391
            this.select([itemID], false, event);
4✔
392
        } else {
393
            this.deselect([itemID], event);
3✔
394
        }
395
    }
396

397
    /** @hidden @internal */
398
    public handleOpened() {
399
        this.triggerCheck();
61✔
400

401
        // Disabling focus of the search input should happen only when drop down opens.
402
        // During keyboard navigation input should receive focus, even the autoFocusSearch is disabled.
403
        // That is why in such cases focusing of the dropdownContainer happens outside focusSearchInput method.
404
        if (this.autoFocusSearch) {
61✔
405
            this.focusSearchInput(true);
58✔
406
        } else {
407
            this.dropdownContainer.nativeElement.focus();
3✔
408
        }
409
        this.opened.emit({ owner: this });
61✔
410
    }
411

412
    /** @hidden @internal */
413
    public focusSearchInput(opening?: boolean): void {
414
        if (this.displaySearchInput && this.searchInput) {
63✔
415
            this.searchInput.nativeElement.focus();
62✔
416
        } else {
417
            if (opening) {
1!
418
                this.dropdownContainer.nativeElement.focus();
1✔
419
            } else {
420
                this.comboInput.nativeElement.focus();
×
421
                this.toggle();
×
422
            }
423
        }
424
    }
425

426
    protected setSelection(selection: Set<any>, event?: Event): void {
427
        const currentSelection = this.selectionService.get(this.id);
125✔
428
        const removed = this.convertKeysToItems(diffInSets(currentSelection, selection));
125✔
429
        const added = this.convertKeysToItems(diffInSets(selection, currentSelection));
125✔
430
        const newValue = Array.from(selection);
125✔
431
        const oldValue = Array.from(currentSelection || []);
125!
432
        const newSelection = this.convertKeysToItems(newValue);
125✔
433
        const oldSelection = this.convertKeysToItems(oldValue);
125✔
434
        const displayText = this.createDisplayText(this.convertKeysToItems(newValue), oldValue);
125✔
435
        const args: IComboSelectionChangingEventArgs = {
125✔
436
            newValue,
437
            oldValue,
438
            newSelection,
439
            oldSelection,
440
            added,
441
            removed,
442
            event,
443
            owner: this,
444
            displayText,
445
            cancel: false
446
        };
447
        this.selectionChanging.emit(args);
125✔
448
        if (!args.cancel) {
125✔
449
            this.selectionService.select_items(this.id, args.newValue, true);
122✔
450
            this._value = args.newValue;
122✔
451
            if (displayText !== args.displayText) {
122✔
452
                this._displayValue = this._displayText = args.displayText;
3✔
453
            } else {
454
                this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
119✔
455
            }
456
            this._onChangeCallback(this.value);
122✔
457
            const changedArgs: IComboSelectionChangedEventArgs = {
122✔
458
                newValue: this.value,
459
                oldValue,
460
                newSelection: this.selection,
461
                oldSelection,
462
                added: this.convertKeysToItems(diffInSets(new Set(this.value), new Set(oldValue))),
463
                removed: this.convertKeysToItems(diffInSets(new Set(oldValue), new Set(this.value))),
464
                event,
465
                owner: this,
466
                displayText: this._displayValue
467
            };
468
            this.selectionChanged.emit(changedArgs);
122✔
469
        } else if (this.isRemote) {
3✔
470
            this.registerRemoteEntries(diffInSets(selection, currentSelection), false);
1✔
471
        }
472
    }
473

474
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
475
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
6,850✔
476
        return this.isRemote
2,006✔
477
            ? this.getRemoteSelection(selection, oldSelection)
478
            : this.concatDisplayText(newSelection);
479
    }
480

481
    protected getSearchPlaceholderText(): string {
482
        return this.searchPlaceholder ||
7,721✔
483
            (this.disableFiltering ? this.resourceStrings.igx_combo_addCustomValues_placeholder : this.resourceStrings.igx_combo_filter_search_placeholder);
1,350✔
484
    }
485

486
    /** Returns a string that should be populated in the combo's text box */
487
    private concatDisplayText(selection: any[]): string {
488
        const value = this.displayKey !== null && this.displayKey !== undefined ?
1,965✔
489
            selection.map(entry => entry[this.displayKey]).join(', ') :
6,861✔
490
            selection.join(', ');
491
        return value;
1,965✔
492
    }
493
}
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