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

IgniteUI / igniteui-angular / 25866082356

14 May 2026 02:34PM UTC coverage: 90.17% (+0.03%) from 90.145%
25866082356

Pull #17268

github

web-flow
Merge 8ca78091f into cd69e807d
Pull Request #17268: fix(igxGrid): In case zone is already stable emit detect and emit chu…

14840 of 17281 branches covered (85.87%)

Branch coverage included in aggregate %.

4 of 6 new or added lines in 1 file covered. (66.67%)

113 existing lines in 2 files now uncovered.

29894 of 32330 relevant lines covered (92.47%)

34552.16 hits per line

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

95.22
/projects/igniteui-angular/select/src/select/select.component.ts
1
import { AfterContentChecked, AfterContentInit, AfterViewInit, booleanAttribute, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, HostBinding, Injector, Input, OnDestroy, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren, inject } from '@angular/core';
2
import { NgTemplateOutlet } from '@angular/common';
3
import { AbstractControl, ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
4
import { noop } from 'rxjs';
5
import { takeUntil } from 'rxjs/operators';
6

7
import {
8
    EditorProvider,
9
    IBaseCancelableBrowserEventArgs,
10
    IBaseEventArgs,
11
    AbsoluteScrollStrategy,
12
    OverlaySettings
13
} from 'igniteui-angular/core';
14
import { IgxSelectItemComponent } from './select-item.component';
15
import { SelectPositioningStrategy } from './select-positioning-strategy';
16
import { IgxSelectBase } from './select.common';
17
import { IgxHintDirective, IgxInputGroupType, IgxPrefixDirective, IGX_INPUT_GROUP_TYPE, IgxInputGroupComponent, IgxInputDirective, IgxInputState, IgxLabelDirective, IgxReadOnlyInputDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
18
import { ToggleViewCancelableEventArgs, ToggleViewEventArgs, IgxToggleDirective } from 'igniteui-angular/directives';
19
import { IgxOverlayService } from 'igniteui-angular/core';
20
import { IgxIconComponent } from 'igniteui-angular/icon';
21
import { IgxSelectItemNavigationDirective } from './select-navigation.directive';
22
import { IGX_DROPDOWN_BASE, IgxDropDownComponent, IgxDropDownItemBaseDirective, ISelectionEventArgs, Navigate } from 'igniteui-angular/drop-down';
23

24
/** @hidden @internal */
25
@Directive({
26
    selector: '[igxSelectToggleIcon]',
27
    standalone: true
28
})
29
export class IgxSelectToggleIconDirective {
3✔
30
}
31

32
/** @hidden @internal */
33
@Directive({
34
    selector: '[igxSelectHeader]',
35
    standalone: true
36
})
37
export class IgxSelectHeaderDirective {
3✔
38
}
39

40
/** @hidden @internal */
41
@Directive({
42
    selector: '[igxSelectFooter]',
43
    standalone: true
44
})
45
export class IgxSelectFooterDirective {
3✔
46
}
47

48
/**
49
 * **Ignite UI for Angular Select** -
50
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/select)
51
 *
52
 * The `igxSelect` provides an input with dropdown list allowing selection of a single item.
53
 *
54
 * Example:
55
 * ```html
56
 * <igx-select #select1 [placeholder]="'Pick One'">
57
 *   <label igxLabel>Select Label</label>
58
 *   <igx-select-item *ngFor="let item of items" [value]="item.field">
59
 *     {{ item.field }}
60
 *   </igx-select-item>
61
 * </igx-select>
62
 * ```
63
 */
64
@Component({
65
    selector: 'igx-select',
66
    templateUrl: './select.component.html',
67
    providers: [
68
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxSelectComponent, multi: true },
69
        { provide: IGX_DROPDOWN_BASE, useExisting: IgxSelectComponent }
70
    ],
71
    styles: [`
72
        :host {
73
            display: block;
74
        }
75
    `],
76
    imports: [IgxInputGroupComponent, IgxInputDirective, IgxSelectItemNavigationDirective, IgxSuffixDirective, IgxReadOnlyInputDirective, NgTemplateOutlet, IgxIconComponent, IgxToggleDirective]
77
})
78
export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor,
3✔
79
    AfterContentInit, OnInit, AfterViewInit, OnDestroy, EditorProvider, AfterContentChecked {
80
    protected overlayService = inject<IgxOverlayService>(IgxOverlayService);
1,188✔
81
    private _inputGroupType = inject<IgxInputGroupType>(IGX_INPUT_GROUP_TYPE, { optional: true });
1,188✔
82
    private _injector = inject(Injector);
1,188✔
83

84

85
    /** @hidden @internal */
86
    @ViewChild('inputGroup', { read: IgxInputGroupComponent, static: true }) public inputGroup: IgxInputGroupComponent;
87

88
    /** @hidden @internal */
89
    @ViewChild('input', { read: IgxInputDirective, static: true }) public input: IgxInputDirective;
90

91
    /** @hidden @internal */
92
    @ContentChildren(forwardRef(() => IgxSelectItemComponent), { descendants: true })
6✔
93
    public override children: QueryList<IgxSelectItemComponent>;
94

95
    @ContentChildren(IgxPrefixDirective, { descendants: true })
96
    protected prefixes: QueryList<IgxPrefixDirective>;
97

98
    @ContentChildren(IgxSuffixDirective, { descendants: true })
99
    protected suffixes: QueryList<IgxSuffixDirective>;
100

101
    @ViewChildren(IgxSuffixDirective)
102
    protected internalSuffixes: QueryList<IgxSuffixDirective>;
103

104
    /** @hidden @internal */
105
    @ContentChild(forwardRef(() => IgxLabelDirective), { static: true }) public label: IgxLabelDirective;
6✔
106

107
    /**
108
     * Sets input placeholder.
109
     *
110
     */
111
    @Input() public placeholder;
112

113

114
    /**
115
     * Disables the component.
116
     * ```html
117
     * <igx-select [disabled]="'true'"></igx-select>
118
     * ```
119
     */
120
    @Input({ transform: booleanAttribute }) public disabled = false;
1,188✔
121

122
    /**
123
     * Sets custom OverlaySettings `IgxSelectComponent`.
124
     * ```html
125
     * <igx-select [overlaySettings] = "customOverlaySettings"></igx-select>
126
     * ```
127
     */
128
    @Input()
129
    public overlaySettings: OverlaySettings;
130

131
    /** @hidden @internal */
132
    @HostBinding('style.maxHeight')
133
    public override maxHeight = '256px';
1,188✔
134

135
    /**
136
     * Emitted before the dropdown is opened
137
     *
138
     * ```html
139
     * <igx-select opening='handleOpening($event)'></igx-select>
140
     * ```
141
     */
142
    @Output()
143
    public override opening = new EventEmitter<IBaseCancelableBrowserEventArgs>();
1,188✔
144

145
    /**
146
     * Emitted after the dropdown is opened
147
     *
148
     * ```html
149
     * <igx-select (opened)='handleOpened($event)'></igx-select>
150
     * ```
151
     */
152
    @Output()
153
    public override opened = new EventEmitter<IBaseEventArgs>();
1,188✔
154

155
    /**
156
     * Emitted before the dropdown is closed
157
     *
158
     * ```html
159
     * <igx-select (closing)='handleClosing($event)'></igx-select>
160
     * ```
161
     */
162
    @Output()
163
    public override closing = new EventEmitter<IBaseCancelableBrowserEventArgs>();
1,188✔
164

165
    /**
166
     * Emitted after the dropdown is closed
167
     *
168
     * ```html
169
     * <igx-select (closed)='handleClosed($event)'></igx-select>
170
     * ```
171
     */
172
    @Output()
173
    public override closed = new EventEmitter<IBaseEventArgs>();
1,188✔
174

175
    /**
176
     * The custom template, if any, that should be used when rendering the select TOGGLE(open/close) button
177
     *
178
     * ```typescript
179
     * // Set in typescript
180
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
181
     * myComponent.select.toggleIconTemplate = myCustomTemplate;
182
     * ```
183
     * ```html
184
     * <!-- Set in markup -->
185
     *  <igx-select #select>
186
     *      ...
187
     *      <ng-template igxSelectToggleIcon let-collapsed>
188
     *          <igx-icon>{{ collapsed ? 'remove_circle' : 'remove_circle_outline'}}</igx-icon>
189
     *      </ng-template>
190
     *  </igx-select>
191
     * ```
192
     */
193
    @ContentChild(IgxSelectToggleIconDirective, { read: TemplateRef })
194
    public toggleIconTemplate: TemplateRef<any> = null;
1,188✔
195

196
    /**
197
     * The custom template, if any, that should be used when rendering the HEADER for the select items list
198
     *
199
     * ```typescript
200
     * // Set in typescript
201
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
202
     * myComponent.select.headerTemplate = myCustomTemplate;
203
     * ```
204
     * ```html
205
     * <!-- Set in markup -->
206
     *  <igx-select #select>
207
     *      ...
208
     *      <ng-template igxSelectHeader>
209
     *          <div class="select__header">
210
     *              This is a custom header
211
     *          </div>
212
     *      </ng-template>
213
     *  </igx-select>
214
     * ```
215
     */
216
    @ContentChild(IgxSelectHeaderDirective, { read: TemplateRef, static: false })
217
    public headerTemplate: TemplateRef<any> = null;
1,188✔
218

219
    /**
220
     * The custom template, if any, that should be used when rendering the FOOTER for the select items list
221
     *
222
     * ```typescript
223
     * // Set in typescript
224
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
225
     * myComponent.select.footerTemplate = myCustomTemplate;
226
     * ```
227
     * ```html
228
     * <!-- Set in markup -->
229
     *  <igx-select #select>
230
     *      ...
231
     *      <ng-template igxSelectFooter>
232
     *          <div class="select__footer">
233
     *              This is a custom footer
234
     *          </div>
235
     *      </ng-template>
236
     *  </igx-select>
237
     * ```
238
     */
239
    @ContentChild(IgxSelectFooterDirective, { read: TemplateRef, static: false })
240
    public footerTemplate: TemplateRef<any> = null;
1,188✔
241

242
    @ContentChild(IgxHintDirective, { read: ElementRef }) private hintElement: ElementRef;
243

244
    /** @hidden @internal */
245
    public override width: string;
246

247
    /** @hidden @internal do not use the drop-down container class */
248
    public override cssClass = false;
1,188✔
249

250
    /** @hidden @internal */
251
    public override allowItemsFocus = false;
1,188✔
252

253
    /** @hidden @internal */
254
    public override height: string;
255

256
    private ngControl: NgControl = null;
1,188✔
257
    private _overlayDefaults: OverlaySettings;
258
    private _value: any;
259
    private _type = null;
1,188✔
260

261
    /**
262
     * Gets/Sets the component value.
263
     *
264
     * ```typescript
265
     * // get
266
     * let selectValue = this.select.value;
267
     * ```
268
     *
269
     * ```typescript
270
     * // set
271
     * this.select.value = 'London';
272
     * ```
273
     * ```html
274
     * <igx-select [value]="value"></igx-select>
275
     * ```
276
     */
277
    @Input()
278
    public get value(): any {
279
        return this._value;
9,845✔
280
    }
281
    public set value(v: any) {
282
        if (this._value === v) {
2,540✔
283
            return;
308✔
284
        }
285
        this._value = v;
2,232✔
286
        this.setSelection(this.items.find(x => x.value === this.value));
3,313✔
287
    }
288

289
    /**
290
     * Sets how the select will be styled.
291
     * The allowed values are `line`, `box` and `border`. The input-group default is `line`.
292
     * ```html
293
     * <igx-select [type]="'box'"></igx-select>
294
     * ```
295
     */
296
    @Input()
297
    public get type(): IgxInputGroupType {
298
        return this._type || this._inputGroupType || 'line';
59,593✔
299
    }
300

301
    public set type(val: IgxInputGroupType) {
302
        this._type = val;
1,091✔
303
    }
304

305
    /** @hidden @internal */
306
    public get selectionValue() {
307
        const selectedItem = this.selectedItem;
29,795✔
308
        return selectedItem ? selectedItem.itemText : '';
29,795✔
309
    }
310

311
    /** @hidden @internal */
312
    public override get selectedItem(): IgxSelectItemComponent {
313
        return this.selection.first_item(this.id);
31,968✔
314
    }
315

316
    private _onChangeCallback: (_: any) => void = noop;
1,188✔
317
    private _onTouchedCallback: () => void = noop;
1,188✔
318

319
    //#region ControlValueAccessor
320

321
    /** @hidden @internal */
322
    public writeValue = (value: any) => {
1,188✔
323
        this.value = value;
2,524✔
324
    };
325

326
    /** @hidden @internal */
327
    public registerOnChange(fn: any): void {
328
        this._onChangeCallback = fn;
1,099✔
329
    }
330

331
    /** @hidden @internal */
332
    public registerOnTouched(fn: any): void {
333
        this._onTouchedCallback = fn;
1,099✔
334
    }
335

336
    /** @hidden @internal */
337
    public setDisabledState(isDisabled: boolean): void {
338
        this.disabled = isDisabled;
1,257✔
339
    }
340
    //#endregion
341

342
    /** @hidden @internal */
343
    public getEditElement(): HTMLInputElement {
344
        return this.input.nativeElement;
2,036✔
345
    }
346

347
    /** @hidden @internal */
348
    public override selectItem(newSelection: IgxDropDownItemBaseDirective, event?) {
349
        const oldSelection = this.selectedItem ?? <IgxDropDownItemBaseDirective>{};
399✔
350

351
        if (newSelection === null || newSelection.disabled || newSelection.isHeader) {
399✔
352
            return;
2✔
353
        }
354

355
        if (newSelection === oldSelection) {
397✔
356
            this.toggleDirective.close();
24✔
357
            return;
24✔
358
        }
359

360
        const args: ISelectionEventArgs = { oldSelection, newSelection, cancel: false, owner: this };
373✔
361
        this.selectionChanging.emit(args);
373✔
362

363
        if (args.cancel) {
373✔
364
            return;
111✔
365
        }
366

367
        this.setSelection(newSelection);
262✔
368
        this._value = newSelection.value;
262✔
369

370
        if (event) {
262✔
371
            this.toggleDirective.close();
111✔
372
        }
373

374
        this.cdr.detectChanges();
262✔
375
        this._onChangeCallback(this.value);
262✔
376
    }
377

378
    /** @hidden @internal */
379
    public getFirstItemElement(): HTMLElement {
380
        return this.children.first.element.nativeElement;
83✔
381
    }
382

383
    /**
384
     * Opens the select
385
     *
386
     * ```typescript
387
     * this.select.open();
388
     * ```
389
     */
390
    public override open(overlaySettings?: OverlaySettings) {
391
        if (this.disabled || this.items.length === 0) {
307✔
392
            return;
3✔
393
        }
394

395
        if (!this.selectedItem) {
304✔
396
            this.navigateFirst();
212✔
397
        }
398

399
        super.open(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
304✔
400
    }
401

402
    public inputGroupClick(event: MouseEvent, overlaySettings?: OverlaySettings) {
403
        const targetElement = event.target as HTMLElement;
234✔
404

405
        if (this.hintElement && targetElement.contains(this.hintElement.nativeElement)) {
234!
UNCOV
406
            return;
×
407
        }
408
        this.toggle(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
234✔
409
    }
410

411
    /** @hidden @internal */
412
    public ngAfterContentInit() {
413
        this._overlayDefaults = {
1,187✔
414
            target: this.getEditElement(),
415
            modal: false,
416
            positionStrategy: new SelectPositioningStrategy(this),
417
            scrollStrategy: new AbsoluteScrollStrategy(),
418
            excludeFromOutsideClick: [this.inputGroup.element.nativeElement as HTMLElement]
419
        };
420
        const changes$ = this.children.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
1,187✔
421
            this.setSelection(this.items.find(x => x.value === this.value));
3,273✔
422
            this.cdr.detectChanges();
864✔
423
        });
424
        Promise.resolve().then(() => {
1,187✔
425
            if (!changes$.closed) {
1,187✔
426
                this.children.notifyOnChanges();
828✔
427
            }
428
        });
429
    }
430

431
    /**
432
     * Event handlers
433
     *
434
     * @hidden @internal
435
     */
436
    public handleOpening(e: ToggleViewCancelableEventArgs) {
437
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
304✔
438
        this.opening.emit(args);
304✔
439

440
        e.cancel = args.cancel;
304✔
441
        if (args.cancel) {
304!
UNCOV
442
            return;
×
443
        }
444
    }
445

446
    /** @hidden @internal */
447
    public override onToggleContentAppended(event: ToggleViewEventArgs) {
448
        const info = this.overlayService.getOverlayById(event.id);
299✔
449
        if (info?.settings?.positionStrategy instanceof SelectPositioningStrategy) {
299!
UNCOV
450
            return;
×
451
        }
452
        super.onToggleContentAppended(event);
299✔
453
    }
454

455
    /** @hidden @internal */
456
    public handleOpened() {
457
        this.updateItemFocus();
292✔
458
        this.opened.emit({ owner: this });
292✔
459
    }
460

461
    /** @hidden @internal */
462
    public handleClosing(e: ToggleViewCancelableEventArgs) {
463
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
253✔
464
        this.closing.emit(args);
253✔
465
        e.cancel = args.cancel;
253✔
466
    }
467

468
    /** @hidden @internal */
469
    public handleClosed() {
470
        this.focusItem(false);
250✔
471
        this.closed.emit({ owner: this });
250✔
472
    }
473

474
    /** @hidden @internal */
475
    public onBlur(): void {
476
        this._onTouchedCallback();
166✔
477
        if (this.ngControl && this.ngControl.invalid) {
166✔
478
            this.input.valid = IgxInputState.INVALID;
3✔
479
        } else {
480
            this.input.valid = IgxInputState.INITIAL;
163✔
481
        }
482
    }
483

484
    /** @hidden @internal */
485
    public onFocus(): void {
486
        this._onTouchedCallback();
234✔
487
    }
488

489
    /**
490
     * @hidden @internal
491
     */
492
    public override ngOnInit() {
493
        this.ngControl = this._injector.get<NgControl>(NgControl, null);
1,188✔
494
    }
495

496
    /**
497
     * @hidden @internal
498
     */
499
    public override ngAfterViewInit() {
500
        super.ngAfterViewInit();
1,187✔
501

502
        if (this.ngControl) {
1,187✔
503
            this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.onStatusChanged.bind(this));
1,093✔
504
            this.manageRequiredAsterisk();
1,093✔
505
        }
506

507
        this.cdr.detectChanges();
1,187✔
508
    }
509

510
    /** @hidden @internal */
511
    public ngAfterContentChecked() {
512
        if (this.inputGroup && this.prefixes?.length > 0) {
14,076✔
513
            this.inputGroup.prefixes = this.prefixes;
769✔
514
        }
515

516
        if (this.inputGroup) {
14,076✔
517
            const suffixesArray = this.suffixes?.toArray() ?? [];
14,076!
518
            const internalSuffixesArray = this.internalSuffixes?.toArray() ?? [];
14,076✔
519
            const mergedSuffixes = new QueryList<IgxSuffixDirective>();
14,076✔
520
            mergedSuffixes.reset([
14,076✔
521
                ...suffixesArray,
522
                ...internalSuffixesArray
523
            ]);
524
            this.inputGroup.suffixes = mergedSuffixes;
14,076✔
525
        }
526
    }
527

528
    /** @hidden @internal */
529
    public get toggleIcon(): string {
530
        return this.collapsed ? 'input_expand' : 'input_collapse';
29,795✔
531
    }
532

533
    /**
534
     * @hidden @internal
535
     * Prevent input blur - closing the items container on Header/Footer Template click.
536
     */
537
    public mousedownHandler(event) {
UNCOV
538
        event.preventDefault();
×
539
    }
540

541
    protected onStatusChanged() {
542
        this.manageRequiredAsterisk();
1,387✔
543

544
        if (this.ngControl && !this.ngControl.disabled && this.isTouchedOrDirty) {
1,387✔
545
            if (this.hasValidators && this.inputGroup.isFocused) {
516✔
546
                this.input.valid = this.ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
1!
547
            } else {
548
                // B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
549
                this.input.valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
515✔
550
            }
551
        } else {
552
            this.input.valid = IgxInputState.INITIAL;
871✔
553
        }
554
    }
555

556
    private get isTouchedOrDirty(): boolean {
557
        return (this.ngControl.control.touched || this.ngControl.control.dirty);
1,197✔
558
    }
559

560
    private get hasValidators(): boolean {
561
        return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
516✔
562
    }
563

564
    protected override navigate(direction: Navigate, currentIndex?: number) {
565
        if (this.collapsed && this.selectedItem) {
325✔
566
            this.navigateItem(this.selectedItem.itemIndex);
41✔
567
        }
568
        super.navigate(direction, currentIndex);
325✔
569
    }
570

571
    protected manageRequiredAsterisk(): void {
572
        const hasRequiredHTMLAttribute = this.elementRef.nativeElement.hasAttribute('required');
2,480✔
573
        let isRequired = false;
2,480✔
574

575
        if (this.ngControl && this.ngControl.control.validator) {
2,480✔
576
            const error = this.ngControl.control.validator({} as AbstractControl);
20✔
577
            isRequired = !!(error && error.required);
20✔
578
        }
579

580
        this.inputGroup.isRequired = isRequired;
2,480✔
581

582
        if (this.input?.nativeElement) {
2,480✔
583
            this.input.nativeElement.setAttribute('aria-required', isRequired.toString());
2,480✔
584
        }
585

586
        // Handle validator removal case
587
        if (!isRequired && !hasRequiredHTMLAttribute) {
2,480✔
588
            this.input.valid = IgxInputState.INITIAL;
2,456✔
589
        }
590

591
        this.cdr.markForCheck();
2,480✔
592
    }
593

594
    private setSelection(item: IgxDropDownItemBaseDirective) {
595
        if (item && item.value !== undefined && item.value !== null) {
3,358✔
596
            this.selection.set(this.id, new Set([item]));
1,777✔
597
        } else {
598
            this.selection.clear(this.id);
1,581✔
599
        }
600
    }
601
}
602

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