• 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

85.38
/projects/igniteui-angular/src/lib/select/select.component.ts
1
import {
2
    AfterContentChecked,
3
    AfterContentInit,
4
    AfterViewInit,
5
    booleanAttribute,
6
    ChangeDetectorRef,
7
    Component,
8
    ContentChild,
9
    ContentChildren,
10
    Directive,
11
    ElementRef,
12
    EventEmitter,
13
    forwardRef,
14
    HostBinding,
15
    Inject,
16
    Injector,
17
    Input,
18
    OnDestroy,
19
    OnInit,
20
    Optional,
21
    Output,
22
    QueryList,
23
    TemplateRef,
24
    ViewChild
25
} from '@angular/core';
26
import { DOCUMENT, NgIf, NgTemplateOutlet } from '@angular/common';
27
import { AbstractControl, ControlValueAccessor, NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
28
import { noop } from 'rxjs';
29
import { takeUntil } from 'rxjs/operators';
30

31
import { EditorProvider } from '../core/edit-provider';
32
import { IgxSelectionAPIService } from '../core/selection';
33
import { IBaseCancelableBrowserEventArgs, IBaseEventArgs } from '../core/utils';
34
import { IgxLabelDirective } from '../directives/label/label.directive';
35
import { IgxDropDownItemBaseDirective } from '../drop-down/drop-down-item.base';
36
import { IGX_DROPDOWN_BASE, ISelectionEventArgs, Navigate } from '../drop-down/drop-down.common';
37
import { IgxInputGroupComponent } from '../input-group/input-group.component';
38
import { AbsoluteScrollStrategy } from '../services/overlay/scroll/absolute-scroll-strategy';
39
import { OverlaySettings } from '../services/overlay/utilities';
40
import { IgxDropDownComponent } from './../drop-down/drop-down.component';
41
import { IgxSelectItemComponent } from './select-item.component';
42
import { SelectPositioningStrategy } from './select-positioning-strategy';
43
import { IgxSelectBase } from './select.common';
44
import { IgxHintDirective, IgxInputGroupType, IgxPrefixDirective, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api';
45
import { ToggleViewCancelableEventArgs, ToggleViewEventArgs, IgxToggleDirective } from '../directives/toggle/toggle.directive';
46
import { IgxOverlayService } from '../services/overlay/overlay';
47
import { IgxIconComponent } from '../icon/icon.component';
48
import { IgxSuffixDirective } from '../directives/suffix/suffix.directive';
49
import { IgxSelectItemNavigationDirective } from './select-navigation.directive';
50
import { IgxInputDirective, IgxInputState } from '../directives/input/input.directive';
51

52
/** @hidden @internal */
53
@Directive({
54
    selector: '[igxSelectToggleIcon]',
55
    standalone: true
56
})
57
export class IgxSelectToggleIconDirective {
2✔
58
}
59

60
/** @hidden @internal */
61
@Directive({
62
    selector: '[igxSelectHeader]',
63
    standalone: true
64
})
65
export class IgxSelectHeaderDirective {
2✔
66
}
67

68
/** @hidden @internal */
69
@Directive({
70
    selector: '[igxSelectFooter]',
71
    standalone: true
72
})
73
export class IgxSelectFooterDirective {
2✔
74
}
75

76
/**
77
 * **Ignite UI for Angular Select** -
78
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/select)
79
 *
80
 * The `igxSelect` provides an input with dropdown list allowing selection of a single item.
81
 *
82
 * Example:
83
 * ```html
84
 * <igx-select #select1 [placeholder]="'Pick One'">
85
 *   <label igxLabel>Select Label</label>
86
 *   <igx-select-item *ngFor="let item of items" [value]="item.field">
87
 *     {{ item.field }}
88
 *   </igx-select-item>
89
 * </igx-select>
90
 * ```
91
 */
92
@Component({
93
    selector: 'igx-select',
94
    templateUrl: './select.component.html',
95
    providers: [
96
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxSelectComponent, multi: true },
97
        { provide: IGX_DROPDOWN_BASE, useExisting: IgxSelectComponent }
98
    ],
99
    styles: [`
100
        :host {
101
            display: block;
102
        }
103
    `],
104
    imports: [IgxInputGroupComponent, IgxInputDirective, IgxSelectItemNavigationDirective, IgxSuffixDirective, NgIf, NgTemplateOutlet, IgxIconComponent, IgxToggleDirective]
105
})
106
export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor,
2✔
107
    AfterContentInit, OnInit, AfterViewInit, OnDestroy, EditorProvider, AfterContentChecked {
108

109
    /** @hidden @internal */
110
    @ViewChild('inputGroup', { read: IgxInputGroupComponent, static: true }) public inputGroup: IgxInputGroupComponent;
111

112
    /** @hidden @internal */
113
    @ViewChild('input', { read: IgxInputDirective, static: true }) public input: IgxInputDirective;
114

115
    /** @hidden @internal */
116
    @ContentChildren(forwardRef(() => IgxSelectItemComponent), { descendants: true })
2✔
117
    public override children: QueryList<IgxSelectItemComponent>;
118

119
    @ContentChildren(IgxPrefixDirective, { descendants: true })
120
    protected prefixes: QueryList<IgxPrefixDirective>;
121

122
    @ContentChildren(IgxSuffixDirective, { descendants: true })
123
    protected suffixes: QueryList<IgxSuffixDirective>;
124

125
    /** @hidden @internal */
126
    @ContentChild(forwardRef(() => IgxLabelDirective), { static: true }) public label: IgxLabelDirective;
2✔
127

128
    /**
129
     * Sets input placeholder.
130
     *
131
     */
132
    @Input() public placeholder;
133

134

135
    /**
136
     * Disables the component.
137
     * ```html
138
     * <igx-select [disabled]="'true'"></igx-select>
139
     * ```
140
     */
141
    @Input({ transform: booleanAttribute }) public disabled = false;
94✔
142

143
    /**
144
     * Sets custom OverlaySettings `IgxSelectComponent`.
145
     * ```html
146
     * <igx-select [overlaySettings] = "customOverlaySettings"></igx-select>
147
     * ```
148
     */
149
    @Input()
150
    public overlaySettings: OverlaySettings;
151

152
    /** @hidden @internal */
153
    @HostBinding('style.maxHeight')
154
    public override maxHeight = '256px';
94✔
155

156
    /**
157
     * Emitted before the dropdown is opened
158
     *
159
     * ```html
160
     * <igx-select opening='handleOpening($event)'></igx-select>
161
     * ```
162
     */
163
    @Output()
164
    public override opening = new EventEmitter<IBaseCancelableBrowserEventArgs>();
94✔
165

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

176
    /**
177
     * Emitted before the dropdown is closed
178
     *
179
     * ```html
180
     * <igx-select (closing)='handleClosing($event)'></igx-select>
181
     * ```
182
     */
183
    @Output()
184
    public override closing = new EventEmitter<IBaseCancelableBrowserEventArgs>();
94✔
185

186
    /**
187
     * Emitted after the dropdown is closed
188
     *
189
     * ```html
190
     * <igx-select (closed)='handleClosed($event)'></igx-select>
191
     * ```
192
     */
193
    @Output()
194
    public override closed = new EventEmitter<IBaseEventArgs>();
94✔
195

196
    /**
197
     * The custom template, if any, that should be used when rendering the select TOGGLE(open/close) button
198
     *
199
     * ```typescript
200
     * // Set in typescript
201
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
202
     * myComponent.select.toggleIconTemplate = myCustomTemplate;
203
     * ```
204
     * ```html
205
     * <!-- Set in markup -->
206
     *  <igx-select #select>
207
     *      ...
208
     *      <ng-template igxSelectToggleIcon let-collapsed>
209
     *          <igx-icon>{{ collapsed ? 'remove_circle' : 'remove_circle_outline'}}</igx-icon>
210
     *      </ng-template>
211
     *  </igx-select>
212
     * ```
213
     */
214
    @ContentChild(IgxSelectToggleIconDirective, { read: TemplateRef })
215
    public toggleIconTemplate: TemplateRef<any> = null;
94✔
216

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

240
    /**
241
     * The custom template, if any, that should be used when rendering the FOOTER for the select items list
242
     *
243
     * ```typescript
244
     * // Set in typescript
245
     * const myCustomTemplate: TemplateRef<any> = myComponent.customTemplate;
246
     * myComponent.select.footerTemplate = myCustomTemplate;
247
     * ```
248
     * ```html
249
     * <!-- Set in markup -->
250
     *  <igx-select #select>
251
     *      ...
252
     *      <ng-template igxSelectFooter>
253
     *          <div class="select__footer">
254
     *              This is a custom footer
255
     *          </div>
256
     *      </ng-template>
257
     *  </igx-select>
258
     * ```
259
     */
260
    @ContentChild(IgxSelectFooterDirective, { read: TemplateRef, static: false })
261
    public footerTemplate: TemplateRef<any> = null;
94✔
262

263
    @ContentChild(IgxHintDirective, { read: ElementRef }) private hintElement: ElementRef;
264

265
    /** @hidden @internal */
266
    public override width: string;
267

268
    /** @hidden @internal do not use the drop-down container class */
269
    public override cssClass = false;
94✔
270

271
    /** @hidden @internal */
272
    public override allowItemsFocus = false;
94✔
273

274
    /** @hidden @internal */
275
    public override height: string;
276

277
    private ngControl: NgControl = null;
94✔
278
    private _overlayDefaults: OverlaySettings;
279
    private _value: any;
280
    private _type = null;
94✔
281

282
    /**
283
     * Gets/Sets the component value.
284
     *
285
     * ```typescript
286
     * // get
287
     * let selectValue = this.select.value;
288
     * ```
289
     *
290
     * ```typescript
291
     * // set
292
     * this.select.value = 'London';
293
     * ```
294
     * ```html
295
     * <igx-select [value]="value"></igx-select>
296
     * ```
297
     */
298
    @Input()
299
    public get value(): any {
300
        return this._value;
1,212✔
301
    }
302
    public set value(v: any) {
303
        if (this._value === v) {
234✔
304
            return;
72✔
305
        }
306
        this._value = v;
162✔
307
        this.setSelection(this.items.find(x => x.value === this.value));
162✔
308
    }
309

310
    /**
311
     * Sets how the select will be styled.
312
     * The allowed values are `line`, `box` and `border`. The input-group default is `line`.
313
     * ```html
314
     * <igx-select [type]="'box'"></igx-select>
315
     * ```
316
     */
317
    @Input()
318
    public get type(): IgxInputGroupType {
319
        return this._type || this._inputGroupType || 'line';
3,802!
320
    }
321

322
    public set type(val: IgxInputGroupType) {
323
        this._type = val;
94✔
324
    }
325

326
    /** @hidden @internal */
327
    public get selectionValue() {
328
        const selectedItem = this.selectedItem;
1,901✔
329
        return selectedItem ? selectedItem.itemText : '';
1,901✔
330
    }
331

332
    /** @hidden @internal */
333
    public override get selectedItem(): IgxSelectItemComponent {
334
        return this.selection.first_item(this.id);
2,131✔
335
    }
336

337
    private _onChangeCallback: (_: any) => void = noop;
94✔
338
    private _onTouchedCallback: () => void = noop;
94✔
339

340
    constructor(
341
        elementRef: ElementRef,
342
        cdr: ChangeDetectorRef,
343
        @Inject(DOCUMENT) document: any,
344
        selection: IgxSelectionAPIService,
345
        @Inject(IgxOverlayService) protected overlayService: IgxOverlayService,
94✔
346
        @Optional() @Inject(IGX_INPUT_GROUP_TYPE) private _inputGroupType: IgxInputGroupType,
94✔
347
        private _injector: Injector,
94✔
348
    ) {
349
        super(elementRef, cdr, document, selection);
94✔
350
    }
351

352
    //#region ControlValueAccessor
353

354
    /** @hidden @internal */
355
    public writeValue = (value: any) => {
94✔
356
        this.value = value;
234✔
357
    };
358

359
    /** @hidden @internal */
360
    public registerOnChange(fn: any): void {
361
        this._onChangeCallback = fn;
94✔
362
    }
363

364
    /** @hidden @internal */
365
    public registerOnTouched(fn: any): void {
366
        this._onTouchedCallback = fn;
94✔
367
    }
368

369
    /** @hidden @internal */
370
    public setDisabledState(isDisabled: boolean): void {
371
        this.disabled = isDisabled;
136✔
372
    }
373
    //#endregion
374

375
    /** @hidden @internal */
376
    public getEditElement(): HTMLInputElement {
377
        return this.input.nativeElement;
94✔
378
    }
379

380
    /** @hidden @internal */
381
    public override selectItem(newSelection: IgxDropDownItemBaseDirective, event?) {
382
        const oldSelection = this.selectedItem ?? <IgxDropDownItemBaseDirective>{};
37✔
383

384
        if (newSelection === null || newSelection.disabled || newSelection.isHeader) {
37!
UNCOV
385
            return;
×
386
        }
387

388
        if (newSelection === oldSelection) {
37!
UNCOV
389
            this.toggleDirective.close();
×
UNCOV
390
            return;
×
391
        }
392

393
        const args: ISelectionEventArgs = { oldSelection, newSelection, cancel: false, owner: this };
37✔
394
        this.selectionChanging.emit(args);
37✔
395

396
        if (args.cancel) {
37✔
397
            return;
18✔
398
        }
399

400
        this.setSelection(newSelection);
19✔
401
        this._value = newSelection.value;
19✔
402

403
        if (event) {
19✔
404
            this.toggleDirective.close();
19✔
405
        }
406

407
        this.cdr.detectChanges();
19✔
408
        this._onChangeCallback(this.value);
19✔
409
    }
410

411
    /** @hidden @internal */
412
    public getFirstItemElement(): HTMLElement {
UNCOV
413
        return this.children.first.element.nativeElement;
×
414
    }
415

416
    /**
417
     * Opens the select
418
     *
419
     * ```typescript
420
     * this.select.open();
421
     * ```
422
     */
423
    public override open(overlaySettings?: OverlaySettings) {
424
        if (this.disabled || this.items.length === 0) {
39!
UNCOV
425
            return;
×
426
        }
427
        if (!this.selectedItem) {
39✔
428
            this.navigateFirst();
39✔
429
        }
430

431
        super.open(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
39✔
432
    }
433

434
    public inputGroupClick(event: MouseEvent, overlaySettings?: OverlaySettings) {
435
        const targetElement = event.target as HTMLElement;
43✔
436

437
        if (this.hintElement && targetElement.contains(this.hintElement.nativeElement)) {
43!
438
            return;
×
439
        }
440
        this.toggle(Object.assign({}, this._overlayDefaults, this.overlaySettings, overlaySettings));
43✔
441
    }
442

443
    /** @hidden @internal */
444
    public ngAfterContentInit() {
445
        this._overlayDefaults = {
94✔
446
            target: this.getEditElement(),
447
            modal: false,
448
            positionStrategy: new SelectPositioningStrategy(this),
449
            scrollStrategy: new AbsoluteScrollStrategy(),
450
            excludeFromOutsideClick: [this.inputGroup.element.nativeElement as HTMLElement]
451
        };
452
        const changes$ = this.children.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
94✔
453
            this.setSelection(this.items.find(x => x.value === this.value));
622✔
454
            this.cdr.detectChanges();
300✔
455
        });
456
        Promise.resolve().then(() => {
94✔
457
            if (!changes$.closed) {
94✔
458
                this.children.notifyOnChanges();
94✔
459
            }
460
        });
461
    }
462

463
    /**
464
     * Event handlers
465
     *
466
     * @hidden @internal
467
     */
468
    public handleOpening(e: ToggleViewCancelableEventArgs) {
469
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
39✔
470
        this.opening.emit(args);
39✔
471

472
        e.cancel = args.cancel;
39✔
473
        if (args.cancel) {
39!
474
            return;
×
475
        }
476
    }
477

478
    /** @hidden @internal */
479
    public override onToggleContentAppended(event: ToggleViewEventArgs) {
480
        const info = this.overlayService.getOverlayById(event.id);
39✔
481
        if (info?.settings?.positionStrategy instanceof SelectPositioningStrategy) {
39!
482
            return;
×
483
        }
484
        super.onToggleContentAppended(event);
39✔
485
    }
486

487
    /** @hidden @internal */
488
    public handleOpened() {
489
        this.updateItemFocus();
39✔
490
        this.opened.emit({ owner: this });
39✔
491
    }
492

493
    /** @hidden @internal */
494
    public handleClosing(e: ToggleViewCancelableEventArgs) {
495
        const args: IBaseCancelableBrowserEventArgs = { owner: this, event: e.event, cancel: e.cancel };
37✔
496
        this.closing.emit(args);
37✔
497
        e.cancel = args.cancel;
37✔
498
    }
499

500
    /** @hidden @internal */
501
    public handleClosed() {
502
        this.focusItem(false);
37✔
503
        this.closed.emit({ owner: this });
37✔
504
    }
505

506
    /** @hidden @internal */
507
    public onBlur(): void {
508
        this._onTouchedCallback();
22✔
509
        if (this.ngControl && this.ngControl.invalid) {
22!
UNCOV
510
            this.input.valid = IgxInputState.INVALID;
×
511
        } else {
512
            this.input.valid = IgxInputState.INITIAL;
22✔
513
        }
514
    }
515

516
    /** @hidden @internal */
517
    public onFocus(): void {
518
        this._onTouchedCallback();
41✔
519
    }
520

521
    /**
522
     * @hidden @internal
523
     */
524
    public override ngOnInit() {
525
        this.ngControl = this._injector.get<NgControl>(NgControl, null);
94✔
526
    }
527

528
    /**
529
     * @hidden @internal
530
     */
531
    public override ngAfterViewInit() {
532
        super.ngAfterViewInit();
94✔
533

534
        if (this.ngControl) {
94✔
535
            this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.onStatusChanged.bind(this));
94✔
536
            this.manageRequiredAsterisk();
94✔
537
        }
538

539
        this.cdr.detectChanges();
94✔
540
    }
541

542
    /** @hidden @internal */
543
    public ngAfterContentChecked() {
544
        if (this.inputGroup && this.prefixes?.length > 0) {
769✔
545
            this.inputGroup.prefixes = this.prefixes;
55✔
546
        }
547

548
        if (this.inputGroup && this.suffixes?.length > 0) {
769!
UNCOV
549
            this.inputGroup.suffixes = this.suffixes;
×
550
        }
551
    }
552

553
    /** @hidden @internal */
554
    public get toggleIcon(): string {
555
        return this.collapsed ? 'input_expand' : 'input_collapse';
1,901✔
556
    }
557

558
    /**
559
     * @hidden @internal
560
     * Prevent input blur - closing the items container on Header/Footer Template click.
561
     */
562
    public mousedownHandler(event) {
563
        event.preventDefault();
×
564
    }
565

566
    protected onStatusChanged() {
567
        this.manageRequiredAsterisk();
201✔
568
        if (this.ngControl && !this.disabled && this.isTouchedOrDirty) {
201✔
569
            if (this.hasValidators && this.inputGroup.isFocused) {
91!
UNCOV
570
                this.input.valid = this.ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
×
571
            } else {
572
                // B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
573
                this.input.valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
91!
574
            }
575
        } else {
576
            this.input.valid = IgxInputState.INITIAL;
110✔
577
        }
578
    }
579

580
    private get isTouchedOrDirty(): boolean {
581
        return (this.ngControl.control.touched || this.ngControl.control.dirty);
178✔
582
    }
583

584
    private get hasValidators(): boolean {
585
        return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
91✔
586
    }
587

588
    protected override navigate(direction: Navigate, currentIndex?: number) {
589
        if (this.collapsed && this.selectedItem) {
39!
UNCOV
590
            this.navigateItem(this.selectedItem.itemIndex);
×
591
        }
592
        super.navigate(direction, currentIndex);
39✔
593
    }
594

595
    protected manageRequiredAsterisk(): void {
596
        const hasRequiredHTMLAttribute = this.elementRef.nativeElement.hasAttribute('required');
295✔
597
        if (this.ngControl && this.ngControl.control.validator) {
295!
598
            // Run the validation with empty object to check if required is enabled.
UNCOV
599
            const error = this.ngControl.control.validator({} as AbstractControl);
×
UNCOV
600
            this.inputGroup.isRequired = error && error.required;
×
UNCOV
601
            this.cdr.markForCheck();
×
602

603
            // If validator is dynamically cleared and no required HTML attribute is set,
604
            // reset label's required class(asterisk) and IgxInputState #6896
605
        } else if (this.inputGroup.isRequired && this.ngControl && !this.ngControl.control.validator && !hasRequiredHTMLAttribute) {
295!
UNCOV
606
            this.input.valid = IgxInputState.INITIAL;
×
UNCOV
607
            this.inputGroup.isRequired = false;
×
UNCOV
608
            this.cdr.markForCheck();
×
609
        }
610
    }
611

612
    private setSelection(item: IgxDropDownItemBaseDirective) {
613
        if (item && item.value !== undefined && item.value !== null) {
481✔
614
            this.selection.set(this.id, new Set([item]));
67✔
615
        } else {
616
            this.selection.clear(this.id);
414✔
617
        }
618
    }
619
}
620

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