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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

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

27.45
/projects/igniteui-angular/src/lib/combo/combo.component.ts
1
import { DOCUMENT, NgClass, NgIf, 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[] => {
2✔
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
        NgIf,
105
        NgTemplateOutlet,
106
        NgClass,
107
        FormsModule,
108
        IgxInputGroupComponent,
109
        IgxInputDirective,
110
        IgxSuffixDirective,
111
        IgxIconComponent,
112
        IgxComboDropDownComponent,
113
        IgxDropDownItemNavigationDirective,
114
        IgxForOfDirective,
115
        IgxComboItemComponent,
116
        IgxComboAddItemComponent,
117
        IgxButtonDirective,
118
        IgxRippleDirective,
119
        IgxComboFilteringPipe,
120
        IgxComboGroupingPipe
121
    ]
122
})
123
export class IgxComboComponent extends IgxComboBaseDirective implements AfterViewInit, ControlValueAccessor, OnInit,
2✔
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;
40✔
131

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

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

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

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

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

185
    protected _prevInputValue = '';
40✔
186

187
    private _displayText: string;
188
    private _disableFiltering = false;
40✔
189

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2025 Coveralls, Inc