• 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

84.3
/projects/igniteui-angular/button-group/src/button-group/button-group.component.ts
1
import { AfterViewInit, Component, ContentChildren, ChangeDetectorRef, EventEmitter, HostBinding, Input, Output, QueryList, Renderer2, ViewChildren, OnDestroy, ElementRef, booleanAttribute, inject, ChangeDetectionStrategy } from '@angular/core';
2
import { Subject } from 'rxjs';
3
import { IgxButtonDirective } from 'igniteui-angular/directives';
4
import { IgxRippleDirective } from 'igniteui-angular/directives';
5

6
import { takeUntil } from 'rxjs/operators';
7
import { IBaseEventArgs } from 'igniteui-angular/core';
8
import { IgxIconComponent } from 'igniteui-angular/icon';
9

10
/**
11
 * Determines the Button Group alignment
12
 */
13
export const ButtonGroupAlignment = {
3✔
14
    horizontal: 'horizontal',
15
    vertical: 'vertical'
16
} as const;
17
export type ButtonGroupAlignment = typeof ButtonGroupAlignment[keyof typeof ButtonGroupAlignment];
18

19
let NEXT_ID = 0;
3✔
20

21
/**
22
 * **Ignite UI for Angular Button Group** -
23
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/buttongroup.html)
24
 *
25
 * The Ignite UI Button Group displays a group of buttons either vertically or horizontally. The group supports
26
 * single, multi and singleRequired selection.
27
 *
28
 * Example:
29
 * ```html
30
 * <igx-buttongroup selectionMode="multi" [values]="fontOptions">
31
 * </igx-buttongroup>
32
 * ```
33
 * The `fontOptions` value shown above is defined as:
34
 * ```typescript
35
 * this.fontOptions = [
36
 *   { icon: 'format_bold', selected: false },
37
 *   { icon: 'format_italic', selected: false },
38
 *   { icon: 'format_underlined', selected: false }];
39
 * ```
40
 */
41
@Component({
42
    selector: 'igx-buttongroup',
43
    templateUrl: 'button-group-content.component.html',
44
    changeDetection: ChangeDetectionStrategy.Eager,
45
    imports: [IgxButtonDirective, IgxRippleDirective, IgxIconComponent]
46
})
47
export class IgxButtonGroupComponent implements AfterViewInit, OnDestroy {
3✔
48
    private _cdr = inject(ChangeDetectorRef);
97✔
49
    private _renderer = inject(Renderer2);
97✔
50
    private _el = inject(ElementRef);
97✔
51

52
    /**
53
     * A collection containing all buttons inside the button group.
54
     */
55
    public get buttons(): IgxButtonDirective[] {
56
        return [...this.viewButtons.toArray(), ...this.templateButtons.toArray()];
947✔
57
    }
58

59
    /**
60
     * Gets/Sets the value of the `id` attribute. If not set it will be automatically generated.
61
     * ```html
62
     *  <igx-buttongroup [id]="'igx-dialog-56'" [selectionMode]="'multi'" [values]="alignOptions">
63
     * ```
64
     */
65
    @HostBinding('attr.id')
66
    @Input()
67
    public id = `igx-buttongroup-${NEXT_ID++}`;
97✔
68

69
    /**
70
     * @hidden
71
     */
72
    @HostBinding('style.zIndex')
73
    public zIndex = 0;
97✔
74

75
    /**
76
     * Allows you to set a style using the `itemContentCssClass` input.
77
     * The value should be the CSS class name that will be applied to the button group.
78
     * ```typescript
79
     * public style1 = "styleClass";
80
     *  //..
81
     * ```
82
     *  ```html
83
     * <igx-buttongroup [itemContentCssClass]="style1" [selectionMode]="'multi'" [values]="alignOptions">
84
     * ```
85
     */
86
    @Input()
87
    public set itemContentCssClass(value: string) {
88
        this._itemContentCssClass = value || this._itemContentCssClass;
4!
89
    }
90

91
    /**
92
     * Returns the CSS class of the item content of the button group.
93
     * ```typescript
94
     *  @ViewChild("MyChild")
95
     * public buttonG: IgxButtonGroupComponent;
96
     * ngAfterViewInit(){
97
     *    let buttonSelect = this.buttonG.itemContentCssClass;
98
     * }
99
     * ```
100
     */
101
    public get itemContentCssClass(): string {
102
        return this._itemContentCssClass;
104✔
103
    }
104

105
    /**
106
     * Enables selecting multiple buttons. By default, multi-selection is false.
107
     *
108
     * @deprecated in version 16.1.0. Use the `selectionMode` property instead.
109
     */
110
    @Input()
111
    public get multiSelection() {
112
        if (this.selectionMode === 'multi') {
×
113
            return true;
×
114
        } else {
115
            return false;
×
116
        }
117
    }
118
    public set multiSelection(selectionMode: boolean) {
119
        if (selectionMode) {
×
120
            this.selectionMode = 'multi';
×
121
        } else {
122
            this.selectionMode = 'single';
×
123
        }
124
    }
125

126
    /**
127
     * Gets/Sets the selection mode to 'single', 'singleRequired' or 'multi' of the buttons. By default, the selection mode is 'single'.
128
     * ```html
129
     * <igx-buttongroup [selectionMode]="'multi'" [alignment]="alignment"></igx-buttongroup>
130
     * ```
131
     */
132
    @Input()
133
    public get selectionMode() {
134
        return this._selectionMode;
156✔
135
    }
136
    public set selectionMode(selectionMode: 'single' | 'singleRequired' | 'multi') {
137
        if (this.viewButtons && selectionMode !== this._selectionMode) {
12✔
138
            this.buttons.forEach((_b, i) => {
4✔
139
                this.deselectButton(i);
13✔
140
            });
141
            this._selectionMode = selectionMode;
4✔
142
        } else {
143
            this._selectionMode = selectionMode;
8✔
144
        }
145
    }
146

147
    /**
148
     * Property that configures the buttons in the button group using a collection of `Button` objects.
149
     * ```typescript
150
     *  public ngOnInit() {
151
     *      this.cities = [
152
     *        new Button({
153
     *          label: "Sofia"
154
     *      }),
155
     *        new Button({
156
     *          label: "London"
157
     *      }),
158
     *        new Button({
159
     *          label: "New York",
160
     *          selected: true
161
     *      }),
162
     *        new Button({
163
     *          label: "Tokyo"
164
     *      })
165
     *  ];
166
     *  }
167
     *  //..
168
     * ```
169
     * ```html
170
     *  <igx-buttongroup [selectionMode]="'single'" [values]="cities"></igx-buttongroup>
171
     * ```
172
     */
173
    @Input() public values: any;
174

175
    /**
176
     * Disables the `igx-buttongroup` component. By default it's false.
177
     * ```html
178
     * <igx-buttongroup [disabled]="true" [selectionMode]="'multi'" [values]="fontOptions"></igx-buttongroup>
179
     * ```
180
     */
181
    @Input({ transform: booleanAttribute })
182
    public get disabled(): boolean {
183
        return this._disabled;
326✔
184
    }
185
    public set disabled(value: boolean) {
186
        if (this._disabled !== value) {
×
187
            this._disabled = value;
×
188

189
            if (this.viewButtons && this.templateButtons) {
×
190
                this.buttons.forEach((b) => (b.disabled = this._disabled));
×
191
            }
192
        }
193
    }
194

195
    /**
196
     * Allows you to set the button group alignment.
197
     * Available options are `ButtonGroupAlignment.horizontal` (default) and `ButtonGroupAlignment.vertical`.
198
     * ```typescript
199
     * public alignment = ButtonGroupAlignment.vertical;
200
     * //..
201
     * ```
202
     * ```html
203
     * <igx-buttongroup [selectionMode]="'single'" [values]="cities" [alignment]="alignment"></igx-buttongroup>
204
     * ```
205
     */
206
    @Input()
207
    public set alignment(value: ButtonGroupAlignment) {
208
        this._isVertical = value === ButtonGroupAlignment.vertical;
7✔
209
    }
210
    /**
211
     * Returns the alignment of the `igx-buttongroup`.
212
     * ```typescript
213
     * @ViewChild("MyChild")
214
     * public buttonG: IgxButtonGroupComponent;
215
     * ngAfterViewInit(){
216
     *    let buttonAlignment = this.buttonG.alignment;
217
     * }
218
     * ```
219
     */
220
    public get alignment(): ButtonGroupAlignment {
221
        return this._isVertical ? ButtonGroupAlignment.vertical : ButtonGroupAlignment.horizontal;
2✔
222
    }
223

224
    /**
225
     * An @Ouput property that emits an event when a button is selected.
226
     * ```typescript
227
     * @ViewChild("toast")
228
     * private toast: IgxToastComponent;
229
     * public selectedHandler(buttongroup) {
230
     *     this.toast.open()
231
     * }
232
     *  //...
233
     * ```
234
     * ```html
235
     * <igx-buttongroup #MyChild [selectionMode]="'multi'" (selected)="selectedHandler($event)"></igx-buttongroup>
236
     * <igx-toast #toast>You have made a selection!</igx-toast>
237
     * ```
238
     */
239
    @Output()
240
    public selected = new EventEmitter<IButtonGroupEventArgs>();
97✔
241

242
    /**
243
     * An @Ouput property that emits an event when a button is deselected.
244
     * ```typescript
245
     *  @ViewChild("toast")
246
     *  private toast: IgxToastComponent;
247
     *  public deselectedHandler(buttongroup){
248
     *     this.toast.open()
249
     * }
250
     *  //...
251
     * ```
252
     * ```html
253
     * <igx-buttongroup> #MyChild [selectionMode]="'multi'" (deselected)="deselectedHandler($event)"></igx-buttongroup>
254
     * <igx-toast #toast>You have deselected a button!</igx-toast>
255
     * ```
256
     */
257
    @Output()
258
    public deselected = new EventEmitter<IButtonGroupEventArgs>();
97✔
259

260
    @ViewChildren(IgxButtonDirective) private viewButtons: QueryList<IgxButtonDirective>;
261
    @ContentChildren(IgxButtonDirective) private templateButtons: QueryList<IgxButtonDirective>;
262

263
    /**
264
     * Returns true if the `igx-buttongroup` alignment is vertical.
265
     * Note that in order for the accessor to work correctly the property should be set explicitly.
266
     * ```html
267
     * <igx-buttongroup #MyChild [alignment]="alignment" [values]="alignOptions">
268
     * ```
269
     * ```typescript
270
     * //...
271
     * @ViewChild("MyChild")
272
     * private buttonG: IgxButtonGroupComponent;
273
     * ngAfterViewInit(){
274
     *    let orientation = this.buttonG.isVertical;
275
     * }
276
     * ```
277
     */
278
    public get isVertical(): boolean {
279
        return this._isVertical;
605✔
280
    }
281

282
    /**
283
     * @hidden
284
     */
285
    public selectedIndexes: number[] = [];
97✔
286

287
    protected buttonClickNotifier$ = new Subject<void>();
97✔
288
    protected queryListNotifier$ = new Subject<void>();
97✔
289

290
    private _isVertical: boolean;
291
    private _itemContentCssClass: string;
292
    private _disabled = false;
97✔
293
    private _selectionMode: 'single' | 'singleRequired' | 'multi' = 'single';
97✔
294

295
    private mutationObserver: MutationObserver;
296
    private observerConfig: MutationObserverInit = {
97✔
297
      attributeFilter: ["data-selected"],
298
      childList: true,
299
      subtree: true,
300
    };
301

302
    /**
303
     * Gets the selected button/buttons.
304
     * ```typescript
305
     * @ViewChild("MyChild")
306
     * private buttonG: IgxButtonGroupComponent;
307
     * ngAfterViewInit(){
308
     *    let selectedButton = this.buttonG.selectedButtons;
309
     * }
310
     * ```
311
     */
312
    public get selectedButtons(): IgxButtonDirective[] {
313
        return this.buttons.filter((_, i) => this.selectedIndexes.indexOf(i) !== -1);
124✔
314
    }
315

316
    /**
317
     * Selects a button by its index.
318
     * ```typescript
319
     * @ViewChild("MyChild")
320
     * private buttonG: IgxButtonGroupComponent;
321
     * ngAfterViewInit(){
322
     *    this.buttonG.selectButton(2);
323
     *    this.cdr.detectChanges();
324
     * }
325
     * ```
326
     *
327
     * @memberOf {@link IgxButtonGroupComponent}
328
     */
329
    public selectButton(index: number) {
330
        if (index >= this.buttons.length || index < 0) {
61✔
331
            return;
2✔
332
        }
333

334
        const button = this.buttons[index];
59✔
335
        button.select();
59✔
336
    }
337

338
    /**
339
     * @hidden
340
     * @internal
341
     */
342
    public updateSelected(index: number) {
343
        const button = this.buttons[index];
106✔
344

345
        if (this.selectedIndexes.indexOf(index) === -1) {
106✔
346
            this.selectedIndexes.push(index);
95✔
347
        }
348

349
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'true');
106✔
350
        this._renderer.addClass(button.nativeElement, 'igx-button-group__item--selected');
106✔
351

352
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
106✔
353
        if (indexInViewButtons !== -1) {
106✔
354
            this.values[indexInViewButtons].selected = true;
17✔
355
        }
356

357
        // deselect other buttons if selectionMode is not multi
358
        if (this.selectionMode !== 'multi' && this.selectedIndexes.length > 1) {
106✔
359
            this.buttons.forEach((_, i) => {
14✔
360
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
41✔
361
                    this.deselectButton(i);
14✔
362
                    this.updateDeselected(i);
14✔
363
                }
364
            });
365
        }
366

367
    }
368

369
    public updateDeselected(index: number) {
370
        const button = this.buttons[index];
35✔
371
        if (this.selectedIndexes.indexOf(index) !== -1) {
35✔
372
            this.selectedIndexes.splice(this.selectedIndexes.indexOf(index), 1);
31✔
373
        }
374

375
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'false');
35✔
376
        this._renderer.removeClass(button.nativeElement, 'igx-button-group__item--selected');
35✔
377

378
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
35✔
379
        if (indexInViewButtons !== -1) {
35✔
380
            this.values[indexInViewButtons].selected = false;
10✔
381
        }
382
    }
383

384
    /**
385
     * Deselects a button by its index.
386
     * ```typescript
387
     * @ViewChild("MyChild")
388
     * private buttonG: IgxButtonGroupComponent;
389
     * ngAfterViewInit(){
390
     *    this.buttonG.deselectButton(2);
391
     *    this.cdr.detectChanges();
392
     * }
393
     * ```
394
     *
395
     * @memberOf {@link IgxButtonGroupComponent}
396
     */
397
    public deselectButton(index: number) {
398
        if (index >= this.buttons.length || index < 0) {
125✔
399
            return;
2✔
400
        }
401

402
        const button = this.buttons[index];
123✔
403
        button.deselect();
123✔
404
    }
405

406
    /**
407
     * @hidden
408
     */
409
    public ngAfterViewInit() {
410
        const initButtons = () => {
99✔
411
            // Cancel any existing buttonClick subscriptions
412
            this.buttonClickNotifier$.next();
99✔
413

414
            this.selectedIndexes.splice(0, this.selectedIndexes.length);
99✔
415

416
            // initial configuration
417
            this.buttons.forEach((button, index) => {
99✔
418
                const buttonElement = button.nativeElement;
222✔
419
                this._renderer.addClass(buttonElement, 'igx-button-group__item');
222✔
420

421
                if (this.disabled) {
222!
422
                    button.disabled = true;
×
423
                }
424

425
                if (button.selected) {
222✔
426
                    this.updateSelected(index);
59✔
427
                }
428

429
                button.buttonClick.pipe(takeUntil(this.buttonClickNotifier$)).subscribe((_) => this._clickHandler(index));
222✔
430
            });
431
        };
432

433
        this.mutationObserver = this.setMutationsObserver();
99✔
434

435
        this.viewButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
99✔
436
            this.mutationObserver.disconnect();
×
437
            initButtons();
×
438
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
439
        });
440
        this.templateButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
99✔
441
            this.mutationObserver.disconnect();
×
442
            initButtons();
×
443
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
444
        });
445

446
        initButtons();
99✔
447
        this._cdr.detectChanges();
99✔
448
        this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
99✔
449
    }
450

451
    /**
452
     * @hidden
453
     */
454
    public ngOnDestroy() {
455
        this.buttonClickNotifier$.next();
97✔
456
        this.buttonClickNotifier$.complete();
97✔
457

458
        this.queryListNotifier$.next();
97✔
459
        this.queryListNotifier$.complete();
97✔
460

461
        this.mutationObserver?.disconnect();
97✔
462
    }
463

464
    /**
465
     * @hidden
466
     */
467
    public _clickHandler(index: number) {
468
        const button = this.buttons[index];
37✔
469
        const args: IButtonGroupEventArgs = { owner: this, button, index };
37✔
470

471
        if (this.selectionMode !== 'multi') {
37✔
472
            this.buttons.forEach((b, i) => {
32✔
473
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
70✔
474
                    this.deselected.emit({ owner: this, button: b, index: i });
6✔
475
                }
476
            });
477
        }
478

479
        if (this.selectedIndexes.indexOf(index) === -1) {
37✔
480
            this.selectButton(index);
30✔
481
            this.selected.emit(args);
30✔
482
        } else {
483
            if (this.selectionMode !== 'singleRequired') {
7✔
484
                this.deselectButton(index);
6✔
485
                this.deselected.emit(args);
6✔
486
            }
487
        }
488
    }
489

490
    private setMutationsObserver() {
491
        if (typeof MutationObserver !== 'undefined') {
99✔
492
            return new MutationObserver((records, observer) => {
99✔
493
                // Stop observing while handling changes
494
                observer.disconnect();
40✔
495

496
                const updatedButtons = this.getUpdatedButtons(records);
40✔
497

498
                if (updatedButtons.length > 0) {
40✔
499
                    updatedButtons.forEach((button) => {
40✔
500
                        const index = this.buttons.map((b) => b.nativeElement).indexOf(button);
204✔
501

502
                        this.updateButtonSelectionState(index);
68✔
503
                    });
504
                }
505

506
                // Watch for changes again
507
                observer.observe(this._el.nativeElement, this.observerConfig);
40✔
508
            });
509
        }
510
    }
511

512
    private getUpdatedButtons(records: MutationRecord[]) {
513
        const updated: HTMLButtonElement[] = [];
40✔
514

515
        records
40✔
516
          .filter((x) => x.type === 'attributes')
68✔
517
          .reduce((prev, curr) => {
518
            prev.push(
68✔
519
              curr.target as HTMLButtonElement
520
            );
521
            return prev;
68✔
522
          }, updated);
523

524
        return updated;
40✔
525
    }
526

527
    private updateButtonSelectionState(index: number) {
528
        if (this.buttons[index].selected) {
68✔
529
            this.updateSelected(index);
47✔
530
        } else {
531
            this.updateDeselected(index);
21✔
532
        }
533
    }
534
}
535

536
export interface IButtonGroupEventArgs extends IBaseEventArgs {
537
    owner: IgxButtonGroupComponent;
538
    button: IgxButtonDirective;
539
    index: number;
540
}
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