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

IgniteUI / igniteui-angular / 13287444581

12 Feb 2025 02:18PM UTC coverage: 10.56% (-81.0%) from 91.606%
13287444581

Pull #15359

github

web-flow
Merge a24969adb into 32cfe83f6
Pull Request #15359: fix(time-picker): exclude from SSR toggle events #15135

933 of 15233 branches covered (6.12%)

3037 of 28759 relevant lines covered (10.56%)

352.42 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, 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✔
67
    const results = [];
×
68
    set1.forEach(entry => {
×
69
        if (!set2.has(entry)) {
×
70
            results.push(entry);
×
71
        }
72
    });
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;
×
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;
×
138
    }
139
    public set disableFiltering(value: boolean) {
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>();
×
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;
×
178
    }
179
    /** @hidden @internal */
180
    public set filteredData(val: any[] | null) {
181
        this._filteredData = this.groupKey ? (val || []).filter((e) => e.isHeader !== true) : val;
×
182
        this.checkMatch();
×
183
    }
184

185
    protected _prevInputValue = '';
×
186

187
    private _displayText: string;
188
    private _disableFiltering = false;
×
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);
×
201
        this.comboAPI.register(this);
×
202
    }
203

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

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

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

230
    /**
231
     * @hidden @internal
232
     */
233
    public handleSelectAll(evt) {
234
        if (evt.checked) {
×
235
            this.selectAllItems();
×
236
        } else {
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) : [];
×
246
        const oldSelection = this.selection;
×
247
        this.selectionService.select_items(this.id, selection, true);
×
248
        this.cdr.markForCheck();
×
249
        this._displayValue = this.createDisplayText(this.selection, oldSelection);
×
250
        this._value = this.valueKey ? this.selection.map(item => item[this.valueKey]) : this.selection;
×
251
    }
252

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

261
    /**
262
     * @hidden
263
     */
264
    public getEditElement(): HTMLElement {
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 {
281
        if (this.disabled) {
×
282
            return;
×
283
        }
284
        this.deselectAllItems(true, event);
×
285
        if (this.collapsed) {
×
286
            this.getEditElement().focus();
×
287
        } else {
288
            this.focusSearchInput(true);
×
289
        }
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) {
303
        if (newItems) {
×
304
            const newSelection = this.selectionService.add_items(this.id, newItems, clearCurrentSelection);
×
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) {
318
        if (items) {
×
319
            const newSelection = this.selectionService.delete_items(this.id, items);
×
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) {
333
        const allVisible = this.selectionService.get_all_ids(ignoreFilter ? this.data : this.filteredData, this.valueKey);
×
334
        const newSelection = this.selectionService.add_items(this.id, allVisible);
×
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 {
347
        let newSelection = this.selectionService.get_empty();
×
348
        if (this.filteredData.length !== this.data.length && !ignoreFilter) {
×
349
            newSelection = this.selectionService.delete_items(this.id, this.selectionService.get_all_ids(this.filteredData, this.valueKey));
×
350
        }
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 {
×
374
        if (itemID === undefined) {
×
375
            return;
×
376
        }
377
        if (select) {
×
378
            this.select([itemID], false, event);
×
379
        } else {
380
            this.deselect([itemID], event);
×
381
        }
382
    }
383

384
    /** @hidden @internal */
385
    public handleOpened() {
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.
391
        if (this.autoFocusSearch) {
×
392
            this.focusSearchInput(true);
×
393
        } else {
394
            this.dropdownContainer.nativeElement.focus();
×
395
        }
396
        this.opened.emit({ owner: this });
×
397
    }
398

399
    /** @hidden @internal */
400
    public focusSearchInput(opening?: boolean): void {
401
        if (this.displaySearchInput && this.searchInput) {
×
402
            this.searchInput.nativeElement.focus();
×
403
        } else {
404
            if (opening) {
×
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 {
414
        const currentSelection = this.selectionService.get(this.id);
×
415
        const removed = this.convertKeysToItems(diffInSets(currentSelection, selection));
×
416
        const added = this.convertKeysToItems(diffInSets(selection, currentSelection));
×
417
        const newValue = Array.from(selection);
×
418
        const oldValue = Array.from(currentSelection || []);
×
419
        const newSelection = this.convertKeysToItems(newValue);
×
420
        const oldSelection = this.convertKeysToItems(oldValue);
×
421
        const displayText = this.createDisplayText(this.convertKeysToItems(newValue), oldValue);
×
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
        };
434
        this.selectionChanging.emit(args);
×
435
        if (!args.cancel) {
×
436
            this.selectionService.select_items(this.id, args.newValue, true);
×
437
            this._value = args.newValue;
×
438
            if (displayText !== args.displayText) {
×
439
                this._displayValue = this._displayText = args.displayText;
×
440
            } else {
441
                this._displayValue = this.createDisplayText(this.selection, args.oldSelection);
×
442
            }
443
            this._onChangeCallback(args.newValue);
×
444
        } else if (this.isRemote) {
×
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;
×
451
        return this.isRemote
×
452
            ? this.getRemoteSelection(selection, oldSelection)
453
            : this.concatDisplayText(newSelection);
454
    }
455

456
    protected getSearchPlaceholderText(): string {
457
        return this.searchPlaceholder ||
×
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 ?
×
464
            selection.map(entry => entry[this.displayKey]).join(', ') :
×
465
            selection.join(', ');
466
        return value;
×
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

© 2026 Coveralls, Inc