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

IgniteUI / igniteui-angular / 16193550997

10 Jul 2025 11:12AM UTC coverage: 4.657% (-87.0%) from 91.64%
16193550997

Pull #16028

github

web-flow
Merge f7a9963b8 into 87246e3ce
Pull Request #16028: fix(radio-group): dynamically added radio buttons do not initialize

178 of 15764 branches covered (1.13%)

18 of 19 new or added lines in 2 files covered. (94.74%)

25721 existing lines in 324 files now uncovered.

1377 of 29570 relevant lines covered (4.66%)

0.53 hits per line

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

1.96
/projects/igniteui-angular/src/lib/combo/combo.component.ts
1
import { DOCUMENT, NgClass, NgTemplateOutlet } from '@angular/common';
2
import {
3
    AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit, OnDestroy,
4
    Optional, Inject, Injector, ViewChild, Input, Output, EventEmitter, HostListener, DoCheck, booleanAttribute
5
} from '@angular/core';
6

7
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
8

9
import { IgxSelectionAPIService } from '../core/selection';
10
import { IBaseEventArgs, IBaseCancelableEventArgs, CancelableEventArgs } from '../core/utils';
11
import { IgxForOfDirective } from '../directives/for-of/for_of.directive';
12
import { IgxIconService } from '../icon/icon.service';
13
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
14
import { IgxButtonDirective } from '../directives/button/button.directive';
15
import { IgxInputGroupComponent } from '../input-group/input-group.component';
16
import { IgxComboItemComponent } from './combo-item.component';
17
import { IgxComboDropDownComponent } from './combo-dropdown.component';
18
import { IgxComboFilteringPipe, IgxComboGroupingPipe } from './combo.pipes';
19
import { IGX_COMBO_COMPONENT, IgxComboBaseDirective } from './combo.common';
20
import { IgxComboAddItemComponent } from './combo-add-item.component';
21
import { IgxComboAPIService } from './combo.api';
22
import { EditorProvider } from '../core/edit-provider';
23
import { IgxInputGroupType, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
24
import { IgxDropDownItemNavigationDirective } from '../drop-down/drop-down-navigation.directive';
25
import { IgxIconComponent } from '../icon/icon.component';
26
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
27
import { IgxInputDirective } from '../directives/input/input.directive';
28

29
/** Event emitted when an igx-combo's selection is changing */
30
export interface IComboSelectionChangingEventArgs extends IBaseCancelableEventArgs {
31
    /** An array containing the values that are currently selected */
32
    oldValue: any[];
33
    /** An array containing the values that will be selected after this event */
34
    newValue: any[];
35
    /** An array containing the items that are currently selected */
36
    oldSelection: any[];
37
    /** An array containing the items that will be selected after this event */
38
    newSelection: any[];
39
    /** An array containing the items that will be added to the selection (if any) */
40
    added: any[];
41
    /** An array containing the items that will be removed from the selection (if any) */
42
    removed: any[];
43
    /** The text that will be displayed in the combo text box */
44
    displayText: string;
45
    /** The user interaction that triggered the selection change */
46
    event?: Event;
47
}
48

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

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

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

76
/**
77
 *  Represents a drop-down list that provides editable functionalities, allowing users to choose an option from a predefined list.
78
 *
79
 * @igxModule IgxComboModule
80
 * @igxTheme igx-combo-theme
81
 * @igxKeywords combobox, combo selection
82
 * @igxGroup Grids & Lists
83
 *
84
 * @remarks
85
 * It provides the ability to filter items as well as perform selection with the provided data.
86
 * Additionally, it exposes keyboard navigation and custom styling capabilities.
87
 * @example
88
 * ```html
89
 * <igx-combo [itemsMaxHeight]="250" [data]="locationData"
90
 *  [displayKey]="'field'" [valueKey]="'field'"
91
 *  placeholder="Location(s)" searchPlaceholder="Search...">
92
 * </igx-combo>
93
 * ```
94
 */
95
@Component({
96
    selector: 'igx-combo',
97
    templateUrl: 'combo.component.html',
98
    providers: [
99
        IgxComboAPIService,
100
        { provide: IGX_COMBO_COMPONENT, useExisting: IgxComboComponent },
101
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxComboComponent, multi: true }
102
    ],
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
        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 })
UNCOV
129
    public autoFocusSearch = true;
×
130

131
    /**
132
     * Enables/disables filtering in the list. The default is `false`.
133
     */
134
    @Input({ transform: booleanAttribute })
135
    public get disableFiltering(): boolean {
UNCOV
136
        return this._disableFiltering || this.filteringOptions.filterable === false;
×
137
    }
138
    public set disableFiltering(value: boolean) {
UNCOV
139
        this._disableFiltering = value;
×
140
    }
141

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

160
    /**
161
     * Emitted when item selection is changing, before the selection completes
162
     *
163
     * ```html
164
     * <igx-combo (selectionChanging)='handleSelection()'></igx-combo>
165
     * ```
166
     */
167
    @Output()
UNCOV
168
    public selectionChanging = new EventEmitter<IComboSelectionChangingEventArgs>();
×
169

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

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

UNCOV
184
    protected _prevInputValue = '';
×
185

186
    private _displayText: string;
UNCOV
187
    private _disableFiltering = false;
×
188

189
    constructor(
190
        elementRef: ElementRef,
191
        cdr: ChangeDetectorRef,
192
        selectionService: IgxSelectionAPIService,
193
        comboAPI: IgxComboAPIService,
194
        @Inject(DOCUMENT) document: any,
195
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType: IgxInputGroupType,
196
        @Optional() _injector: Injector,
197
        @Optional() @Inject(IgxIconService) _iconService?: IgxIconService,
198
    ) {
UNCOV
199
        super(elementRef, cdr, selectionService, comboAPI, document, _inputGroupType, _injector, _iconService);
×
UNCOV
200
        this.comboAPI.register(this);
×
201
    }
202

203
    @HostListener('keydown.ArrowDown', ['$event'])
204
    @HostListener('keydown.Alt.ArrowDown', ['$event'])
205
    public onArrowDown(event: Event) {
UNCOV
206
        event.preventDefault();
×
UNCOV
207
        event.stopPropagation();
×
UNCOV
208
        this.open();
×
209
    }
210

211
    /** @hidden @internal */
212
    public get displaySearchInput(): boolean {
UNCOV
213
        return !this.disableFiltering || this.allowCustomValues;
×
214
    }
215

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

229
    /**
230
     * @hidden @internal
231
     */
232
    public handleSelectAll(evt) {
UNCOV
233
        if (evt.checked) {
×
UNCOV
234
            this.selectAllItems();
×
235
        } else {
UNCOV
236
            this.deselectAllItems();
×
237
        }
238
    }
239

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

252
    /** @hidden @internal */
253
    public ngDoCheck(): void {
UNCOV
254
        if (this.data?.length && this.selection.length) {
×
UNCOV
255
            this._displayValue = this._displayText || this.createDisplayText(this.selection, []);
×
UNCOV
256
            this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
×
257
        }
258
    }
259

260
    /**
261
     * @hidden
262
     */
263
    public getEditElement(): HTMLElement {
UNCOV
264
        return this.comboInput.nativeElement;
×
265
    }
266

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

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

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

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

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

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

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

383
    /** @hidden @internal */
384
    public handleOpened() {
UNCOV
385
        this.triggerCheck();
×
386

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

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

412
    protected setSelection(selection: Set<any>, event?: Event): void {
UNCOV
413
        const currentSelection = this.selectionService.get(this.id);
×
UNCOV
414
        const removed = this.convertKeysToItems(diffInSets(currentSelection, selection));
×
UNCOV
415
        const added = this.convertKeysToItems(diffInSets(selection, currentSelection));
×
UNCOV
416
        const newValue = Array.from(selection);
×
UNCOV
417
        const oldValue = Array.from(currentSelection || []);
×
UNCOV
418
        const newSelection = this.convertKeysToItems(newValue);
×
UNCOV
419
        const oldSelection = this.convertKeysToItems(oldValue);
×
UNCOV
420
        const displayText = this.createDisplayText(this.convertKeysToItems(newValue), oldValue);
×
UNCOV
421
        const args: IComboSelectionChangingEventArgs = {
×
422
            newValue,
423
            oldValue,
424
            newSelection,
425
            oldSelection,
426
            added,
427
            removed,
428
            event,
429
            owner: this,
430
            displayText,
431
            cancel: false
432
        };
UNCOV
433
        this.selectionChanging.emit(args);
×
UNCOV
434
        if (!args.cancel) {
×
UNCOV
435
            this.selectionService.select_items(this.id, args.newValue, true);
×
UNCOV
436
            this._value = args.newValue;
×
UNCOV
437
            if (displayText !== args.displayText) {
×
UNCOV
438
                this._displayValue = this._displayText = args.displayText;
×
439
            } else {
UNCOV
440
                this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
×
441
            }
UNCOV
442
            this._onChangeCallback(args.newValue);
×
UNCOV
443
        } else if (this.isRemote) {
×
UNCOV
444
            this.registerRemoteEntries(diffInSets(selection, currentSelection), false);
×
445
        }
446
    }
447

448
    protected createDisplayText(newSelection: any[], oldSelection: any[]) {
UNCOV
449
        const selection = this.valueKey ? newSelection.map(item => item[this.valueKey]) : newSelection;
×
UNCOV
450
        return this.isRemote
×
451
            ? this.getRemoteSelection(selection, oldSelection)
452
            : this.concatDisplayText(newSelection);
453
    }
454

455
    protected getSearchPlaceholderText(): string {
UNCOV
456
        return this.searchPlaceholder ||
×
457
            (this.disableFiltering ? this.resourceStrings.igx_combo_addCustomValues_placeholder : this.resourceStrings.igx_combo_filter_search_placeholder);
×
458
    }
459

460
    /** Returns a string that should be populated in the combo's text box */
461
    private concatDisplayText(selection: any[]): string {
UNCOV
462
        const value = this.displayKey !== null && this.displayKey !== undefined ?
×
UNCOV
463
            selection.map(entry => entry[this.displayKey]).join(', ') :
×
464
            selection.join(', ');
UNCOV
465
        return value;
×
466
    }
467
}
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