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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM UTC 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

1.92
/projects/igniteui-angular/src/lib/directives/radio/radio-group.directive.ts
1
import {
2
    AfterContentInit,
3
    AfterViewInit,
4
    ChangeDetectorRef,
5
    ContentChildren, Directive, DoCheck, EventEmitter, HostBinding, HostListener, Input, OnDestroy, Optional, Output, QueryList, Self, booleanAttribute
6
} from '@angular/core';
7
import { ControlValueAccessor, NgControl, Validators } from '@angular/forms';
8
import { fromEvent, noop, Subject } from 'rxjs';
9
import { startWith, takeUntil } from 'rxjs/operators';
10
import { mkenum } from '../../core/utils';
11
import { IgxRadioComponent } from '../../radio/radio.component';
12
import { IgxDirectionality } from '../../services/direction/directionality';
13
import { IChangeCheckboxEventArgs } from '../../checkbox/public_api';
14

15
/**
16
 * Determines the Radio Group alignment
17
 */
18
export const RadioGroupAlignment = mkenum({
2✔
19
    horizontal: 'horizontal',
20
    vertical: 'vertical'
21
});
22
export type RadioGroupAlignment = typeof RadioGroupAlignment[keyof typeof RadioGroupAlignment];
23

24
let nextId = 0;
2✔
25

26
/**
27
 * Radio group directive renders set of radio buttons.
28
 *
29
 * @igxModule IgxRadioModule
30
 *
31
 * @igxTheme igx-radio-theme
32
 *
33
 * @igxKeywords radiogroup, radio, button, input
34
 *
35
 * @igxGroup Data Entry & Display
36
 *
37
 * @remarks
38
 * The Ignite UI Radio Group allows the user to select a single option from an available set of options that are listed side by side.
39
 *
40
 * @example:
41
 * ```html
42
 * <igx-radio-group name="radioGroup">
43
 *   <igx-radio *ngFor="let item of ['Foo', 'Bar', 'Baz']" value="{{item}}">
44
 *      {{item}}
45
 *   </igx-radio>
46
 * </igx-radio-group>
47
 * ```
48
 */
49
@Directive({
50
    exportAs: 'igxRadioGroup',
51
    selector: 'igx-radio-group, [igxRadioGroup]',
52
    standalone: true
53
})
54
export class IgxRadioGroupDirective implements AfterContentInit, AfterViewInit, ControlValueAccessor, OnDestroy, DoCheck {
2✔
55
    /**
56
     * Returns reference to the child radio buttons.
57
     *
58
     * @example
59
     * ```typescript
60
     * let radioButtons =  this.radioGroup.radioButtons;
61
     * ```
62
     */
63
    @ContentChildren(IgxRadioComponent, { descendants: true }) public radioButtons: QueryList<IgxRadioComponent>;
64

65
    /**
66
     * Sets/gets the `value` attribute.
67
     *
68
     * @example
69
     * ```html
70
     * <igx-radio-group [value] = "'radioButtonValue'"></igx-radio-group>
71
     * ```
72
     */
73
    @Input()
74
    public get value(): any {
UNCOV
75
        return this._value;
×
76
    }
77
    public set value(newValue: any) {
UNCOV
78
        if (this._value !== newValue) {
×
UNCOV
79
            this._value = newValue;
×
UNCOV
80
            this._selectRadioButton();
×
81
        }
82
    }
83

84
    /**
85
     * Sets/gets the `name` attribute of the radio group component. All child radio buttons inherits this name.
86
     *
87
     * @example
88
     * ```html
89
     * <igx-radio-group name = "Radio1"></igx-radio-group>
90
     *  ```
91
     */
92
    @Input()
93
    public get name(): string {
UNCOV
94
        return this._name;
×
95
    }
96
    public set name(newValue: string) {
UNCOV
97
        if (this._name !== newValue) {
×
UNCOV
98
            this._name = newValue;
×
UNCOV
99
            this._setRadioButtonNames();
×
100
        }
101
    }
102

103
    /**
104
     * Sets/gets whether the radio group is required.
105
     *
106
     * @remarks
107
     * If not set, `required` will have value `false`.
108
     *
109
     * @example
110
     * ```html
111
     * <igx-radio-group [required] = "true"></igx-radio-group>
112
     * ```
113
     */
114
    @Input({ transform: booleanAttribute })
115
    public get required(): boolean {
116
        return this._required;
×
117
    }
118
    public set required(value: boolean) {
UNCOV
119
        this._required = value;
×
UNCOV
120
        this._setRadioButtonsRequired();
×
121
    }
122

123
    /**
124
     * Sets/gets the selected child radio button.
125
     *
126
     * @example
127
     * ```typescript
128
     * let selectedButton = this.radioGroup.selected;
129
     * this.radioGroup.selected = selectedButton;
130
     * ```
131
     */
132
    @Input()
133
    public get selected() {
UNCOV
134
        return this._selected;
×
135
    }
136
    public set selected(selected: IgxRadioComponent | null) {
UNCOV
137
        if (this._selected !== selected) {
×
UNCOV
138
            this._selected = selected;
×
UNCOV
139
            this.value = selected ? selected.value : null;
×
140
        }
141
    }
142

143
    /**
144
     * Sets/gets whether the radio group is invalid.
145
     *
146
     * @remarks
147
     * If not set, `invalid` will have value `false`.
148
     *
149
     * @example
150
     * ```html
151
     * <igx-radio-group [invalid] = "true"></igx-radio-group>
152
     * ```
153
     */
154
    @Input({ transform: booleanAttribute })
155
    public get invalid(): boolean {
UNCOV
156
        return this._invalid;
×
157
    }
158
    public set invalid(value: boolean) {
UNCOV
159
        this._invalid = value;
×
UNCOV
160
        this._setRadioButtonsInvalid();
×
161
    }
162

163
    /**
164
     * An event that is emitted after the radio group `value` is changed.
165
     *
166
     * @remarks
167
     * Provides references to the selected `IgxRadioComponent` and the `value` property as event arguments.
168
     *
169
     * @example
170
     * ```html
171
     * <igx-radio-group (change)="handler($event)"></igx-radio-group>
172
     * ```
173
     */
174
    // eslint-disable-next-line @angular-eslint/no-output-native
UNCOV
175
    @Output() public readonly change: EventEmitter<IChangeCheckboxEventArgs> = new EventEmitter<IChangeCheckboxEventArgs>();
×
176

177
    /**
178
     * The css class applied to the component.
179
     *
180
     * @hidden
181
     * @internal
182
     */
183
    @HostBinding('class.igx-radio-group')
UNCOV
184
    public cssClass = 'igx-radio-group';
×
185

186
    /**
187
     * Sets vertical alignment to the radio group, if `alignment` is set to `vertical`.
188
     * By default the alignment is horizontal.
189
     *
190
     * @example
191
     * ```html
192
     * <igx-radio-group alignment="vertical"></igx-radio-group>
193
     * ```
194
     */
195
    @HostBinding('class.igx-radio-group--vertical')
UNCOV
196
    private vertical = false;
×
197

198
    @HostListener('click', ['$event'])
199
    protected handleClick(event: MouseEvent) {
UNCOV
200
        event.stopPropagation();
×
201

UNCOV
202
        if (this.selected) {
×
UNCOV
203
            this.selected.nativeElement.focus();
×
204
        }
205
    }
206

207
    @HostListener('keydown', ['$event'])
208
    protected handleKeyDown(event: KeyboardEvent) {
209
        const { key } = event;
×
210
        const buttons = this.radioButtons.filter(radio => !radio.disabled);
×
211
        const checked = buttons.find((radio) => radio.checked);
×
212

213
        if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) {
×
214
            let index = checked ? buttons.indexOf(checked) : -1;
×
215
            const ltr = this._directionality.value === 'ltr';
×
216

217
            switch (key) {
×
218
                case 'ArrowUp':
219
                    index += -1;
×
220
                    break;
×
221
                case 'ArrowLeft':
222
                    index += ltr ? -1 : 1;
×
223
                    break;
×
224
                case 'ArrowRight':
225
                    index += ltr ? 1 : -1;
×
226
                    break;
×
227
                default:
228
                    index += 1;
×
229
            }
230

231
            if (index < 0) index = buttons.length - 1;
×
232
            if (index > buttons.length - 1) index = 0;
×
233

234
            buttons.forEach((radio) => {
×
235
                radio.deselect();
×
236
                radio.nativeElement.blur();
×
237
            });
238

239
            buttons[index].focused = true;
×
240
            buttons[index].nativeElement.focus();
×
241
            buttons[index].select();
×
242
            event.preventDefault();
×
243
        }
244

245
        if (event.key === "Tab") {
×
246
            buttons.forEach((radio) => {
×
247
                if (radio !== checked) {
×
248
                    event.stopPropagation();
×
249
                }
250
            });
251
        }
252
    }
253

254
    /**
255
     * Returns the alignment of the `igx-radio-group`.
256
     * ```typescript
257
     * @ViewChild("MyRadioGroup")
258
     * public radioGroup: IgxRadioGroupDirective;
259
     * ngAfterViewInit(){
260
     *    let radioAlignment = this.radioGroup.alignment;
261
     * }
262
     * ```
263
     */
264
    @Input()
265
    public get alignment(): RadioGroupAlignment {
266
        return this.vertical ? RadioGroupAlignment.vertical : RadioGroupAlignment.horizontal;
×
267
    }
268
    /**
269
     * Allows you to set the radio group alignment.
270
     * Available options are `RadioGroupAlignment.horizontal` (default) and `RadioGroupAlignment.vertical`.
271
     * ```typescript
272
     * public alignment = RadioGroupAlignment.vertical;
273
     * //..
274
     * ```
275
     * ```html
276
     * <igx-radio-group [alignment]="alignment"></igx-radio-group>
277
     * ```
278
     */
279
    public set alignment(value: RadioGroupAlignment) {
280
        this.vertical = value === RadioGroupAlignment.vertical;
×
281
    }
282

283
    /**
284
     * @hidden
285
     * @internal
286
     */
UNCOV
287
    private _onChangeCallback: (_: any) => void = noop;
×
288
    /**
289
     * @hidden
290
     * @internal
291
     */
UNCOV
292
    private _name = `igx-radio-group-${nextId++}`;
×
293
    /**
294
     * @hidden
295
     * @internal
296
     */
UNCOV
297
    private _value: any = null;
×
298
    /**
299
     * @hidden
300
     * @internal
301
     */
UNCOV
302
    private _selected: IgxRadioComponent | null = null;
×
303
    /**
304
     * @hidden
305
     * @internal
306
     */
UNCOV
307
    private _isInitialized = false;
×
308
    /**
309
     * @hidden
310
     * @internal
311
     */
UNCOV
312
    private _required = false;
×
313
    /**
314
     * @hidden
315
     * @internal
316
     */
UNCOV
317
    private _invalid = false;
×
318
    /**
319
     * @hidden
320
     * @internal
321
     */
UNCOV
322
    private destroy$ = new Subject<boolean>();
×
323
    /**
324
     * @hidden
325
     * @internal
326
     */
UNCOV
327
    private queryChange$ = new Subject<void>();
×
328

329
    /**
330
     * @hidden
331
     * @internal
332
     */
333
    public ngAfterContentInit() {
334
        // The initial value can possibly be set by NgModel and it is possible that
335
        // the OnInit of the NgModel occurs after the OnInit of this class.
UNCOV
336
        this._isInitialized = true;
×
337

UNCOV
338
        this.radioButtons.changes.pipe(startWith(0), takeUntil(this.destroy$)).subscribe(() => {
×
UNCOV
339
            this.queryChange$.next();
×
UNCOV
340
            setTimeout(() => this._initRadioButtons());
×
341
        });
342

343

UNCOV
344
        if (this.ngControl) {
×
UNCOV
345
            this.radioButtons.forEach((button) => {
×
UNCOV
346
                if (this.ngControl.disabled) {
×
347
                    button.disabled = this.ngControl.disabled;
×
348
                }
349
            });
350
        }
351
    }
352

353
    /**
354
     * @hidden
355
     * @internal
356
    */
357
    public ngAfterViewInit() {
UNCOV
358
        if (this.ngControl) {
×
UNCOV
359
            this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
×
UNCOV
360
                this.invalid = false;
×
361
            });
362

UNCOV
363
            if (this.ngControl.control.validator || this.ngControl.control.asyncValidator) {
×
364
                this._required = this.ngControl?.control?.hasValidator(Validators.required);
×
365
            }
366
        }
367

UNCOV
368
        if (this.radioButtons) {
×
UNCOV
369
            this.radioButtons.forEach((button) => {
×
UNCOV
370
                button.blurRadio
×
371
                    .pipe(takeUntil(this.destroy$))
372
                    .subscribe(() => {
UNCOV
373
                        this.updateValidityOnBlur()
×
374
                    });
375

UNCOV
376
                fromEvent(button.nativeElement, 'keyup')
×
377
                    .pipe(takeUntil(this.destroy$))
378
                    .subscribe((event: KeyboardEvent) => {
379
                        this.updateOnKeyUp(event)
×
380
                    });
381
            });
382
        }
383
    }
384

385
    /**
386
     * @hidden
387
     * @internal
388
     */
389
    private updateValidityOnBlur() {
UNCOV
390
        this.radioButtons.forEach((button) => {
×
UNCOV
391
            button.focused = false;
×
392

UNCOV
393
            if (button.invalid) {
×
UNCOV
394
                this.invalid = true;
×
395
            }
396
        });
397
    }
398

399
    /**
400
     * @hidden
401
     * @internal
402
     */
403
    private updateOnKeyUp(event: KeyboardEvent) {
404
        const checked = this.radioButtons.find(x => x.checked);
×
405

406
        if (event.key === "Tab") {
×
407
            this.radioButtons.forEach((radio) => {
×
408
                if (radio === checked) {
×
409
                    checked.focused = true;
×
410
                }
411
            });
412
        }
413
    }
414

415
    public ngDoCheck(): void {
UNCOV
416
        this._updateTabIndex();
×
417
    }
418

419
    private _updateTabIndex() {
420
        // Needed so that the keyboard navigation of a radio group
421
        // placed inside a dialog works properly
UNCOV
422
        if (this.radioButtons) {
×
UNCOV
423
            const checked = this.radioButtons.find(x => x.checked);
×
424

UNCOV
425
            if (checked) {
×
UNCOV
426
                this.radioButtons.forEach((button) => {
×
UNCOV
427
                    checked.nativeElement.tabIndex = 0;
×
428

UNCOV
429
                    if (button !== checked) {
×
UNCOV
430
                        button.nativeElement.tabIndex = -1;
×
UNCOV
431
                        button.focused = false;
×
432
                    }
433
                });
434
            }
435
        }
436
    }
437

438
    /**
439
     * Sets the "checked" property value on the radio input element.
440
     *
441
     * @remarks
442
     * Checks whether the provided value is consistent to the current radio button.
443
     * If it is, the checked attribute will have value `true` and selected property will contain the selected `IgxRadioComponent`.
444
     *
445
     * @example
446
     * ```typescript
447
     * this.radioGroup.writeValue('radioButtonValue');
448
     * ```
449
     */
450
    public writeValue(value: any) {
UNCOV
451
        this.value = value;
×
452
    }
453

454
    /**
455
     * Registers a function called when the control value changes.
456
     *
457
     * @hidden
458
     * @internal
459
     */
460
    public registerOnChange(fn: (_: any) => void) {
UNCOV
461
        this._onChangeCallback = fn;
×
462
    }
463

464
    /**
465
     * Registers a function called when the control is touched.
466
     *
467
     * @hidden
468
     * @internal
469
     */
470
    public registerOnTouched(fn: () => void) {
UNCOV
471
        if (this.radioButtons) {
×
UNCOV
472
            this.radioButtons.forEach((button) => {
×
UNCOV
473
                button.registerOnTouched(fn);
×
474
            });
475
        }
476
    }
477

478
    /**
479
     * @hidden
480
     * @internal
481
     */
482
    public ngOnDestroy(): void {
UNCOV
483
        this.destroy$.next(true);
×
UNCOV
484
        this.destroy$.complete();
×
485
    }
486

487
    constructor(
UNCOV
488
        @Optional() @Self() public ngControl: NgControl,
×
UNCOV
489
        private _directionality: IgxDirectionality,
×
UNCOV
490
        private cdr: ChangeDetectorRef,
×
491
    ) {
UNCOV
492
        if (this.ngControl !== null) {
×
UNCOV
493
            this.ngControl.valueAccessor = this;
×
494
        }
495
    }
496

497
    /**
498
     * @hidden
499
     * @internal
500
     */
501
    private _initRadioButtons() {
UNCOV
502
        if (this.radioButtons) {
×
UNCOV
503
            const props = { name: this._name, required: this._required };
×
UNCOV
504
            this.radioButtons.forEach((button) => {
×
UNCOV
505
                Object.assign(button, props);
×
506

UNCOV
507
                if (button.value === this._value) {
×
UNCOV
508
                    button.checked = true;
×
UNCOV
509
                    this._selected = button;
×
UNCOV
510
                    this.cdr.markForCheck();
×
511
                }
512

UNCOV
513
                button.change.pipe(
×
514
                    takeUntil(button.destroy$),
515
                    takeUntil(this.destroy$),
516
                    takeUntil(this.queryChange$)
UNCOV
517
                ).subscribe((ev) => this._selectedRadioButtonChanged(ev));
×
518
            });
519
        }
520
    }
521

522
    /**
523
     * @hidden
524
     * @internal
525
     */
526
    private _selectedRadioButtonChanged(args: IChangeCheckboxEventArgs) {
UNCOV
527
        this.radioButtons.forEach((button) => {
×
UNCOV
528
            button.checked = button.id === args.owner.id;
×
UNCOV
529
            if (button.checked && button.ngControl) {
×
530
                this.invalid = button.ngControl.invalid;
×
UNCOV
531
            } else if (button.checked) {
×
UNCOV
532
                this.invalid = false;
×
533
            }
534
        });
535

UNCOV
536
        this._selected = args.owner;
×
UNCOV
537
        this._value = args.value;
×
538

UNCOV
539
        if (this._isInitialized) {
×
UNCOV
540
            this.change.emit(args);
×
UNCOV
541
            this._onChangeCallback(this.value);
×
542
        }
543
    }
544

545
    /**
546
     * @hidden
547
     * @internal
548
     */
549
    private _setRadioButtonNames() {
UNCOV
550
        if (this.radioButtons) {
×
UNCOV
551
            this.radioButtons.forEach((button) => {
×
UNCOV
552
                button.name = this._name;
×
553
            });
554
        }
555
    }
556

557
    /**
558
     * @hidden
559
     * @internal
560
     */
561
    private _selectRadioButton() {
UNCOV
562
        if (this.radioButtons) {
×
UNCOV
563
            this.radioButtons.forEach((button) => {
×
UNCOV
564
                if (this._value === null) {
×
565
                    // no value - uncheck all radio buttons
566
                    if (button.checked) {
×
567
                        button.checked = false;
×
568
                    }
569
                } else {
UNCOV
570
                    if (this._value === button.value) {
×
571
                        // selected button
UNCOV
572
                        if (this._selected !== button) {
×
UNCOV
573
                            this._selected = button;
×
574
                        }
575

UNCOV
576
                        if (!button.checked) {
×
UNCOV
577
                            button.checked = true;
×
578
                        }
579
                    } else {
580
                        // non-selected button
UNCOV
581
                        if (button.checked) {
×
UNCOV
582
                            button.checked = false;
×
583
                        }
584
                    }
585
                }
586
            });
587
        }
588
    }
589

590
    /**
591
     * @hidden
592
     * @internal
593
     */
594
    private _setRadioButtonsRequired() {
UNCOV
595
        if (this.radioButtons) {
×
UNCOV
596
            this.radioButtons.forEach((button) => {
×
UNCOV
597
                button.required = this._required;
×
598
            });
599
        }
600
    }
601

602
    /**
603
     * @hidden
604
     * @internal
605
     */
606
    private _setRadioButtonsInvalid() {
UNCOV
607
        if (this.radioButtons) {
×
UNCOV
608
            this.radioButtons.forEach((button) => {
×
UNCOV
609
                button.invalid = this._invalid;
×
610
            });
611
        }
612
    }
613
}
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