• 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

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

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

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

36
let NEXT_ID = 0;
2✔
37

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

385
    }
386

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
542
        return updated;
×
543
    }
544

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

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