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

IgniteUI / igniteui-angular / 28013325390

23 Jun 2026 08:34AM UTC coverage: 90.139% (-0.02%) from 90.154%
28013325390

Pull #17324

github

web-flow
Merge 690ff31c5 into 01244911c
Pull Request #17324: fix(skills): omit column widths by default in generated grid code

14880 of 17339 branches covered (85.82%)

Branch coverage included in aggregate %.

29947 of 32392 relevant lines covered (92.45%)

34656.81 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, ChangeDetectionStrategy } 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
    changeDetection: ChangeDetectionStrategy.Eager,
77
    imports: [IgxInputGroupComponent, IgxInputDirective, IgxSelectItemNavigationDirective, IgxSuffixDirective, IgxReadOnlyInputDirective, NgTemplateOutlet, IgxIconComponent, IgxToggleDirective]
78
})
79
export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor,
3✔
80
    AfterContentInit, OnInit, AfterViewInit, OnDestroy, EditorProvider, AfterContentChecked {
81
    protected overlayService = inject<IgxOverlayService>(IgxOverlayService);
1,188✔
82
    private _inputGroupType = inject<IgxInputGroupType>(IGX_INPUT_GROUP_TYPE, { optional: true });
1,188✔
83
    private _injector = inject(Injector);
1,188✔
84

85

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

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

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

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

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

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

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

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

114

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

320
    //#region ControlValueAccessor
321

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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