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

IgniteUI / igniteui-angular / 28358286454

29 Jun 2026 08:17AM UTC coverage: 90.127% (-0.05%) from 90.174%
28358286454

Pull #17246

github

web-flow
Merge 21687d939 into 07bdcd752
Pull Request #17246: fix(pivot-grid): fix date format based on the localization

14921 of 17392 branches covered (85.79%)

Branch coverage included in aggregate %.

29 of 32 new or added lines in 4 files covered. (90.63%)

735 existing lines in 76 files now uncovered.

29992 of 32441 relevant lines covered (92.45%)

34632.46 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, ChangeDetectionStrategy } 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 = [];
495✔
67
    set1.forEach(entry => {
495✔
68
        if (!set2.has(entry)) {
1,264✔
69
            results.push(entry);
748✔
70
        }
71
    });
72
    return results;
495✔
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
    changeDetection: ChangeDetectionStrategy.Eager,
103
    imports: [
104
        NgTemplateOutlet,
105
        NgClass,
106
        FormsModule,
107
        IgxInputGroupComponent,
108
        IgxInputDirective,
109
        IgxSuffixDirective,
110
        IgxIconComponent,
111
        IgxComboDropDownComponent,
112
        IgxDropDownItemNavigationDirective,
113
        IgxForOfDirective,
114
        IgxComboItemComponent,
115
        IgxComboAddItemComponent,
116
        IgxButtonDirective,
117
        IgxRippleDirective,
118
        IgxReadOnlyInputDirective,
119
        IgxComboFilteringPipe,
120
        IgxComboGroupingPipe
121
    ]
122
})
123
export class IgxComboComponent extends IgxComboBaseDirective implements AfterViewInit, ControlValueAccessor, OnInit,
3✔
124
    OnDestroy, DoCheck, EditorProvider {
125
    /**
126
     * Whether the combo's search box should be focused after the dropdown is opened.
127
     * When `false`, the combo's list item container will be focused instead
128
     */
129
    @Input({ transform: booleanAttribute })
130
    public autoFocusSearch = true;
321✔
131

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

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

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

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

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

184
    protected _prevInputValue = '';
321✔
185

186
    private _displayText: string;
187

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

470
    /** Returns a string that should be populated in the combo's text box */
471
    private concatDisplayText(selection: any[]): string {
472
        const value = this.displayKey !== null && this.displayKey !== undefined ?
1,965✔
473
            selection.map(entry => entry[this.displayKey]).join(', ') :
6,861✔
474
            selection.join(', ');
475
        return value;
1,965✔
476
    }
477
}
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