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

IgniteUI / igniteui-angular / 16053471080

03 Jul 2025 02:41PM UTC coverage: 4.981% (-86.4%) from 91.409%
16053471080

Pull #16021

github

web-flow
Merge 7c49966eb into 7e40671a1
Pull Request #16021: fix(radio-group): dynamically added radio buttons do not initialize

178 of 15753 branches covered (1.13%)

13 of 14 new or added lines in 2 files covered. (92.86%)

25644 existing lines in 324 files now uncovered.

1478 of 29670 relevant lines covered (4.98%)

0.51 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 { IgxIconComponent } from '../icon/icon.component';
24

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

34
let NEXT_ID = 0;
3✔
35

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

383
    }
384

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
540
        return updated;
×
541
    }
542

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

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