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

IgniteUI / igniteui-angular / 16193564787

10 Jul 2025 11:13AM UTC coverage: 4.657% (-87.0%) from 91.64%
16193564787

Pull #16028

github

web-flow
Merge ad90a37c4 into 87246e3ce
Pull Request #16028: fix(radio-group): dynamically added radio buttons do not initialize

178 of 15764 branches covered (1.13%)

18 of 19 new or added lines in 2 files covered. (94.74%)

25721 existing lines in 324 files now uncovered.

1377 of 29570 relevant lines covered (4.66%)

0.53 hits per line

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

2.36
/projects/igniteui-angular/src/lib/buttonGroup/buttonGroup.component.ts
1
import {
2
    AfterViewInit,
3
    Component,
4
    ContentChildren,
5
    ChangeDetectorRef,
6
    EventEmitter,
7
    HostBinding,
8
    Input,
9
    Output,
10
    QueryList,
11
    Renderer2,
12
    ViewChildren,
13
    OnDestroy,
14
    ElementRef,
15
    booleanAttribute
16
} from '@angular/core';
17
import { Subject } from 'rxjs';
18
import { IgxButtonDirective } from '../directives/button/button.directive';
19
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
20

21
import { takeUntil } from 'rxjs/operators';
22
import { IBaseEventArgs } from '../core/utils';
23
import { mkenum } from '../core/utils';
24
import { IgxIconComponent } from '../icon/icon.component';
25

26
/**
27
 * Determines the Button Group alignment
28
 */
29
export const ButtonGroupAlignment = mkenum({
3✔
30
    horizontal: 'horizontal',
31
    vertical: 'vertical'
32
});
33
export type ButtonGroupAlignment = typeof ButtonGroupAlignment[keyof typeof ButtonGroupAlignment];
34

35
let NEXT_ID = 0;
3✔
36

37
/**
38
 * **Ignite UI for Angular Button Group** -
39
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/buttongroup.html)
40
 *
41
 * The Ignite UI Button Group displays a group of buttons either vertically or horizontally. The group supports
42
 * single, multi and singleRequired selection.
43
 *
44
 * Example:
45
 * ```html
46
 * <igx-buttongroup selectionMode="multi" [values]="fontOptions">
47
 * </igx-buttongroup>
48
 * ```
49
 * The `fontOptions` value shown above is defined as:
50
 * ```typescript
51
 * this.fontOptions = [
52
 *   { icon: 'format_bold', selected: false },
53
 *   { icon: 'format_italic', selected: false },
54
 *   { icon: 'format_underlined', selected: false }];
55
 * ```
56
 */
57
@Component({
58
    selector: 'igx-buttongroup',
59
    templateUrl: 'buttongroup-content.component.html',
60
    imports: [IgxButtonDirective, IgxRippleDirective, IgxIconComponent]
61
})
62
export class IgxButtonGroupComponent implements AfterViewInit, OnDestroy {
3✔
63
    /**
64
     * A collection containing all buttons inside the button group.
65
     */
66
    public get buttons(): IgxButtonDirective[] {
UNCOV
67
        return [...this.viewButtons.toArray(), ...this.templateButtons.toArray()];
×
68
    }
69

70
    /**
71
     * Gets/Sets the value of the `id` attribute. If not set it will be automatically generated.
72
     * ```html
73
     *  <igx-buttongroup [id]="'igx-dialog-56'" [selectionMode]="'multi'" [values]="alignOptions">
74
     * ```
75
     */
76
    @HostBinding('attr.id')
77
    @Input()
UNCOV
78
    public id = `igx-buttongroup-${NEXT_ID++}`;
×
79

80
    /**
81
     * @hidden
82
     */
83
    @HostBinding('style.zIndex')
UNCOV
84
    public zIndex = 0;
×
85

86
    /**
87
     * Allows you to set a style using the `itemContentCssClass` input.
88
     * The value should be the CSS class name that will be applied to the button group.
89
     * ```typescript
90
     * public style1 = "styleClass";
91
     *  //..
92
     * ```
93
     *  ```html
94
     * <igx-buttongroup [itemContentCssClass]="style1" [selectionMode]="'multi'" [values]="alignOptions">
95
     * ```
96
     */
97
    @Input()
98
    public set itemContentCssClass(value: string) {
UNCOV
99
        this._itemContentCssClass = value || this._itemContentCssClass;
×
100
    }
101

102
    /**
103
     * Returns the CSS class of the item content of the `IgxButtonGroup`.
104
     * ```typescript
105
     *  @ViewChild("MyChild")
106
     * public buttonG: IgxButtonGroupComponent;
107
     * ngAfterViewInit(){
108
     *    let buttonSelect = this.buttonG.itemContentCssClass;
109
     * }
110
     * ```
111
     */
112
    public get itemContentCssClass(): string {
UNCOV
113
        return this._itemContentCssClass;
×
114
    }
115

116
    /**
117
     * Enables selecting multiple buttons. By default, multi-selection is false.
118
     *
119
     * @deprecated in version 16.1.0. Use the `selectionMode` property instead.
120
     */
121
    @Input()
122
    public get multiSelection() {
123
        if (this.selectionMode === 'multi') {
×
124
            return true;
×
125
        } else {
126
            return false;
×
127
        }
128
    }
129
    public set multiSelection(selectionMode: boolean) {
130
        if (selectionMode) {
×
131
            this.selectionMode = 'multi';
×
132
        } else {
133
            this.selectionMode = 'single';
×
134
        }
135
    }
136

137
    /**
138
     * Gets/Sets the selection mode to 'single', 'singleRequired' or 'multi' of the buttons. By default, the selection mode is 'single'.
139
     * ```html
140
     * <igx-buttongroup [selectionMode]="'multi'" [alignment]="alignment"></igx-buttongroup>
141
     * ```
142
     */
143
    @Input()
144
    public get selectionMode() {
UNCOV
145
        return this._selectionMode;
×
146
    }
147
    public set selectionMode(selectionMode: 'single' | 'singleRequired' | 'multi') {
UNCOV
148
        if (this.viewButtons && selectionMode !== this._selectionMode) {
×
UNCOV
149
            this.buttons.forEach((b,i) => {
×
UNCOV
150
                this.deselectButton(i);
×
151
            });
UNCOV
152
            this._selectionMode = selectionMode;
×
153
        } else {
UNCOV
154
            this._selectionMode = selectionMode;
×
155
        }
156
    }
157

158
    /**
159
     * Property that configures the buttons in the button group using a collection of `Button` objects.
160
     * ```typescript
161
     *  public ngOnInit() {
162
     *      this.cities = [
163
     *        new Button({
164
     *          label: "Sofia"
165
     *      }),
166
     *        new Button({
167
     *          label: "London"
168
     *      }),
169
     *        new Button({
170
     *          label: "New York",
171
     *          selected: true
172
     *      }),
173
     *        new Button({
174
     *          label: "Tokyo"
175
     *      })
176
     *  ];
177
     *  }
178
     *  //..
179
     * ```
180
     * ```html
181
     *  <igx-buttongroup [selectionMode]="'single'" [values]="cities"></igx-buttongroup>
182
     * ```
183
     */
184
    @Input() public values: any;
185

186
    /**
187
     * Disables the `igx-buttongroup` component. By default it's false.
188
     * ```html
189
     * <igx-buttongroup [disabled]="true" [selectionMode]="'multi'" [values]="fontOptions"></igx-buttongroup>
190
     * ```
191
     */
192
    @Input({ transform: booleanAttribute })
193
    public get disabled(): boolean {
UNCOV
194
        return this._disabled;
×
195
    }
196
    public set disabled(value: boolean) {
197
        if (this._disabled !== value) {
×
198
            this._disabled = value;
×
199

200
            if (this.viewButtons && this.templateButtons) {
×
201
                this.buttons.forEach((b) => (b.disabled = this._disabled));
×
202
            }
203
        }
204
    }
205

206
    /**
207
     * Allows you to set the button group alignment.
208
     * Available options are `ButtonGroupAlignment.horizontal` (default) and `ButtonGroupAlignment.vertical`.
209
     * ```typescript
210
     * public alignment = ButtonGroupAlignment.vertical;
211
     * //..
212
     * ```
213
     * ```html
214
     * <igx-buttongroup [selectionMode]="'single'" [values]="cities" [alignment]="alignment"></igx-buttongroup>
215
     * ```
216
     */
217
    @Input()
218
    public set alignment(value: ButtonGroupAlignment) {
UNCOV
219
        this._isVertical = value === ButtonGroupAlignment.vertical;
×
220
    }
221
    /**
222
     * Returns the alignment of the `igx-buttongroup`.
223
     * ```typescript
224
     * @ViewChild("MyChild")
225
     * public buttonG: IgxButtonGroupComponent;
226
     * ngAfterViewInit(){
227
     *    let buttonAlignment = this.buttonG.alignment;
228
     * }
229
     * ```
230
     */
231
    public get alignment(): ButtonGroupAlignment {
UNCOV
232
        return this._isVertical ? ButtonGroupAlignment.vertical : ButtonGroupAlignment.horizontal;
×
233
    }
234

235
    /**
236
     * An @Ouput property that emits an event when a button is selected.
237
     * ```typescript
238
     * @ViewChild("toast")
239
     * private toast: IgxToastComponent;
240
     * public selectedHandler(buttongroup) {
241
     *     this.toast.open()
242
     * }
243
     *  //...
244
     * ```
245
     * ```html
246
     * <igx-buttongroup #MyChild [selectionMode]="'multi'" (selected)="selectedHandler($event)"></igx-buttongroup>
247
     * <igx-toast #toast>You have made a selection!</igx-toast>
248
     * ```
249
     */
250
    @Output()
UNCOV
251
    public selected = new EventEmitter<IButtonGroupEventArgs>();
×
252

253
    /**
254
     * An @Ouput property that emits an event when a button is deselected.
255
     * ```typescript
256
     *  @ViewChild("toast")
257
     *  private toast: IgxToastComponent;
258
     *  public deselectedHandler(buttongroup){
259
     *     this.toast.open()
260
     * }
261
     *  //...
262
     * ```
263
     * ```html
264
     * <igx-buttongroup> #MyChild [selectionMode]="'multi'" (deselected)="deselectedHandler($event)"></igx-buttongroup>
265
     * <igx-toast #toast>You have deselected a button!</igx-toast>
266
     * ```
267
     */
268
    @Output()
UNCOV
269
    public deselected = new EventEmitter<IButtonGroupEventArgs>();
×
270

271
    @ViewChildren(IgxButtonDirective) private viewButtons: QueryList<IgxButtonDirective>;
272
    @ContentChildren(IgxButtonDirective) private templateButtons: QueryList<IgxButtonDirective>;
273

274
    /**
275
     * Returns true if the `igx-buttongroup` alignment is vertical.
276
     * Note that in order for the accessor to work correctly the property should be set explicitly.
277
     * ```html
278
     * <igx-buttongroup #MyChild [alignment]="alignment" [values]="alignOptions">
279
     * ```
280
     * ```typescript
281
     * //...
282
     * @ViewChild("MyChild")
283
     * private buttonG: IgxButtonGroupComponent;
284
     * ngAfterViewInit(){
285
     *    let orientation = this.buttonG.isVertical;
286
     * }
287
     * ```
288
     */
289
    public get isVertical(): boolean {
UNCOV
290
        return this._isVertical;
×
291
    }
292

293
    /**
294
     * @hidden
295
     */
UNCOV
296
    public selectedIndexes: number[] = [];
×
297

UNCOV
298
    protected buttonClickNotifier$ = new Subject<void>();
×
UNCOV
299
    protected queryListNotifier$ = new Subject<void>();
×
300

301
    private _isVertical: boolean;
302
    private _itemContentCssClass: string;
UNCOV
303
    private _disabled = false;
×
UNCOV
304
    private _selectionMode: 'single' | 'singleRequired' | 'multi' = 'single';
×
305

306
    private mutationObserver: MutationObserver;
UNCOV
307
    private observerConfig: MutationObserverInit = {
×
308
      attributeFilter: ["data-selected"],
309
      childList: true,
310
      subtree: true,
311
    };
312

313
    constructor(
UNCOV
314
        private _cdr: ChangeDetectorRef,
×
UNCOV
315
        private _renderer: Renderer2,
×
UNCOV
316
        private _el: ElementRef
×
317
    ) {}
318

319
    /**
320
     * Gets the selected button/buttons.
321
     * ```typescript
322
     * @ViewChild("MyChild")
323
     * private buttonG: IgxButtonGroupComponent;
324
     * ngAfterViewInit(){
325
     *    let selectedButton = this.buttonG.selectedButtons;
326
     * }
327
     * ```
328
     */
329
    public get selectedButtons(): IgxButtonDirective[] {
UNCOV
330
        return this.buttons.filter((_, i) => this.selectedIndexes.indexOf(i) !== -1);
×
331
    }
332

333
    /**
334
     * Selects a button by its index.
335
     * ```typescript
336
     * @ViewChild("MyChild")
337
     * private buttonG: IgxButtonGroupComponent;
338
     * ngAfterViewInit(){
339
     *    this.buttonG.selectButton(2);
340
     *    this.cdr.detectChanges();
341
     * }
342
     * ```
343
     *
344
     * @memberOf {@link IgxButtonGroupComponent}
345
     */
346
    public selectButton(index: number) {
UNCOV
347
        if (index >= this.buttons.length || index < 0) {
×
UNCOV
348
            return;
×
349
        }
350

UNCOV
351
        const button = this.buttons[index];
×
UNCOV
352
        button.select();
×
353
    }
354

355
    /**
356
     * @hidden
357
     * @internal
358
     */
359
    public updateSelected(index: number) {
UNCOV
360
        const button = this.buttons[index];
×
361

UNCOV
362
        if (this.selectedIndexes.indexOf(index) === -1) {
×
UNCOV
363
            this.selectedIndexes.push(index);
×
364
        }
365

UNCOV
366
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'true');
×
UNCOV
367
        this._renderer.addClass(button.nativeElement, 'igx-button-group__item--selected');
×
368

UNCOV
369
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
×
UNCOV
370
        if (indexInViewButtons !== -1) {
×
UNCOV
371
            this.values[indexInViewButtons].selected = true;
×
372
        }
373

374
        // deselect other buttons if selectionMode is not multi
UNCOV
375
        if (this.selectionMode !== 'multi' && this.selectedIndexes.length > 1) {
×
UNCOV
376
            this.buttons.forEach((_, i) => {
×
UNCOV
377
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
×
UNCOV
378
                    this.deselectButton(i);
×
UNCOV
379
                    this.updateDeselected(i);
×
380
                }
381
            });
382
        }
383

384
    }
385

386
    public updateDeselected(index: number) {
UNCOV
387
        const button = this.buttons[index];
×
UNCOV
388
        if (this.selectedIndexes.indexOf(index) !== -1) {
×
UNCOV
389
            this.selectedIndexes.splice(this.selectedIndexes.indexOf(index), 1);
×
390
        }
391

UNCOV
392
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'false');
×
UNCOV
393
        this._renderer.removeClass(button.nativeElement, 'igx-button-group__item--selected');
×
394

UNCOV
395
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
×
UNCOV
396
        if (indexInViewButtons !== -1) {
×
UNCOV
397
            this.values[indexInViewButtons].selected = false;
×
398
        }
399
    }
400

401
    /**
402
     * Deselects a button by its index.
403
     * ```typescript
404
     * @ViewChild("MyChild")
405
     * private buttonG: IgxButtonGroupComponent;
406
     * ngAfterViewInit(){
407
     *    this.buttonG.deselectButton(2);
408
     *    this.cdr.detectChanges();
409
     * }
410
     * ```
411
     *
412
     * @memberOf {@link IgxButtonGroupComponent}
413
     */
414
    public deselectButton(index: number) {
UNCOV
415
        if (index >= this.buttons.length || index < 0) {
×
UNCOV
416
            return;
×
417
        }
418

UNCOV
419
        const button = this.buttons[index];
×
UNCOV
420
        button.deselect();
×
421
    }
422

423
    /**
424
     * @hidden
425
     */
426
    public ngAfterViewInit() {
UNCOV
427
        const initButtons = () => {
×
428
            // Cancel any existing buttonClick subscriptions
UNCOV
429
            this.buttonClickNotifier$.next();
×
430

UNCOV
431
            this.selectedIndexes.splice(0, this.selectedIndexes.length);
×
432

433
            // initial configuration
UNCOV
434
            this.buttons.forEach((button, index) => {
×
UNCOV
435
                const buttonElement = button.nativeElement;
×
UNCOV
436
                this._renderer.addClass(buttonElement, 'igx-button-group__item');
×
437

UNCOV
438
                if (this.disabled) {
×
439
                    button.disabled = true;
×
440
                }
441

UNCOV
442
                if (button.selected) {
×
UNCOV
443
                    this.updateSelected(index);
×
444
                }
445

UNCOV
446
                button.buttonClick.pipe(takeUntil(this.buttonClickNotifier$)).subscribe((_) => this._clickHandler(index));
×
447
            });
448
        };
449

UNCOV
450
        this.mutationObserver = this.setMutationsObserver();
×
451

UNCOV
452
        this.viewButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
×
453
            this.mutationObserver.disconnect();
×
454
            initButtons();
×
455
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
456
        });
UNCOV
457
        this.templateButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
×
458
            this.mutationObserver.disconnect();
×
459
            initButtons();
×
460
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
461
        });
462

UNCOV
463
        initButtons();
×
UNCOV
464
        this._cdr.detectChanges();
×
UNCOV
465
        this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
466
    }
467

468
    /**
469
     * @hidden
470
     */
471
    public ngOnDestroy() {
UNCOV
472
        this.buttonClickNotifier$.next();
×
UNCOV
473
        this.buttonClickNotifier$.complete();
×
474

UNCOV
475
        this.queryListNotifier$.next();
×
UNCOV
476
        this.queryListNotifier$.complete();
×
477

UNCOV
478
        this.mutationObserver?.disconnect();
×
479
    }
480

481
    /**
482
     * @hidden
483
     */
484
    public _clickHandler(index: number) {
UNCOV
485
        const button = this.buttons[index];
×
UNCOV
486
        const args: IButtonGroupEventArgs = { owner: this, button, index };
×
487

UNCOV
488
        if (this.selectionMode !== 'multi') {
×
UNCOV
489
            this.buttons.forEach((b, i) => {
×
UNCOV
490
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
×
UNCOV
491
                    this.deselected.emit({ owner: this, button: b, index: i });
×
492
                }
493
            });
494
        }
495

UNCOV
496
        if (this.selectedIndexes.indexOf(index) === -1) {
×
UNCOV
497
            this.selectButton(index);
×
UNCOV
498
            this.selected.emit(args);
×
499
        } else {
UNCOV
500
            if (this.selectionMode !== 'singleRequired') {
×
UNCOV
501
                this.deselectButton(index);
×
UNCOV
502
                this.deselected.emit(args);
×
503
            }
504
        }
505
    }
506

507
    private setMutationsObserver() {
UNCOV
508
        if (typeof MutationObserver !== 'undefined') {
×
UNCOV
509
            return new MutationObserver((records, observer) => {
×
510
                // Stop observing while handling changes
UNCOV
511
                observer.disconnect();
×
512

UNCOV
513
                const updatedButtons = this.getUpdatedButtons(records);
×
514

UNCOV
515
                if (updatedButtons.length > 0) {
×
UNCOV
516
                    updatedButtons.forEach((button) => {
×
UNCOV
517
                        const index = this.buttons.map((b) => b.nativeElement).indexOf(button);
×
518

UNCOV
519
                        this.updateButtonSelectionState(index);
×
520
                    });
521
                }
522

523
                // Watch for changes again
UNCOV
524
                observer.observe(this._el.nativeElement, this.observerConfig);
×
525
            });
526
        }
527
    }
528

529
    private getUpdatedButtons(records: MutationRecord[]) {
UNCOV
530
        const updated: HTMLButtonElement[] = [];
×
531

UNCOV
532
        records
×
UNCOV
533
          .filter((x) => x.type === 'attributes')
×
534
          .reduce((prev, curr) => {
UNCOV
535
            prev.push(
×
536
              curr.target as HTMLButtonElement
537
            );
UNCOV
538
            return prev;
×
539
          }, updated);
540

UNCOV
541
        return updated;
×
542
    }
543

544
    private updateButtonSelectionState(index: number) {
UNCOV
545
        if (this.buttons[index].selected) {
×
UNCOV
546
            this.updateSelected(index);
×
547
        } else {
UNCOV
548
            this.updateDeselected(index);
×
549
        }
550
    }
551
}
552

553
export interface IButtonGroupEventArgs extends IBaseEventArgs {
554
    owner: IgxButtonGroupComponent;
555
    button: IgxButtonDirective;
556
    index: number;
557
}
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