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

IgniteUI / igniteui-angular / 8328780634

18 Mar 2024 02:57PM UTC coverage: 91.802% (-0.05%) from 91.855%
8328780634

push

github

web-flow
Merge pull request #13972 from IgniteUI/ikitanov/fix-13961-master

Resolve buttons not selected if initialized after the group

12534 of 14606 branches covered (85.81%)

4 of 10 new or added lines in 1 file covered. (40.0%)

9 existing lines in 4 files now uncovered.

25533 of 27813 relevant lines covered (91.8%)

29903.9 hits per line

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

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

25
import { takeUntil } from 'rxjs/operators';
26
import { DisplayDensityBase, DisplayDensityToken, IDisplayDensityOptions } from '../core/density';
27
import { IBaseEventArgs } from '../core/utils';
28
import { mkenum } from '../core/utils';
29
import { IgxIconComponent } from '../icon/icon.component';
30

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

40
let NEXT_ID = 0;
2✔
41

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

76
    /**
77
     * An @Input property that sets the value of the `id` attribute. If not set it will be automatically generated.
78
     * ```html
79
     *  <igx-buttongroup [id]="'igx-dialog-56'" [selectionMode]="'multi'" [values]="alignOptions">
80
     * ```
81
     */
82
    @HostBinding('attr.id')
83
    @Input()
84
    public id = `igx-buttongroup-${NEXT_ID++}`;
90✔
85

86
    /**
87
     * @hidden
88
     */
89
    @HostBinding('style.zIndex')
90
    public zIndex = 0;
90✔
91

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

108
    /**
109
     * Returns the CSS class of the item content of the `IgxButtonGroup`.
110
     * ```typescript
111
     *  @ViewChild("MyChild")
112
     * public buttonG: IgxButtonGroupComponent;
113
     * ngAfterViewInit(){
114
     *    let buttonSelect = this.buttonG.itemContentCssClass;
115
     * }
116
     * ```
117
     */
118
    public get itemContentCssClass(): string {
119
        return this._itemContentCssClass;
234✔
120
    }
121

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

143
    /**
144
     * An @Input property that get/set the selection mode 'single', 'singleRequired' or 'multi' of the buttons. By default, the selection mode is 'single'.
145
     * ```html
146
     * <igx-buttongroup [selectionMode]="'multi'" [alignment]="alignment"></igx-buttongroup>
147
     * ```
148
     */
149
    @Input()
150
    public get selectionMode() {
151
        return this._selectionMode;
170✔
152
    }
153
    public set selectionMode(selectionMode: 'single' | 'singleRequired' | 'multi') {
154
        if (this.viewButtons && selectionMode !== this._selectionMode) {
12✔
155
            this.buttons.forEach((b,i) => {
4✔
156
                this.deselectButton(i);
13✔
157
            });
158
            this._selectionMode = selectionMode;
4✔
159
        } else {
160
            this._selectionMode = selectionMode;
8✔
161
        }
162
    }
163

164
    /**
165
     * An @Input property that allows setting the buttons in the button group.
166
     * ```typescript
167
     *  public ngOnInit() {
168
     *      this.cities = [
169
     *        new Button({
170
     *          label: "Sofia"
171
     *      }),
172
     *        new Button({
173
     *          label: "London"
174
     *      }),
175
     *        new Button({
176
     *          label: "New York",
177
     *          selected: true
178
     *      }),
179
     *        new Button({
180
     *          label: "Tokyo"
181
     *      })
182
     *  ];
183
     *  }
184
     *  //..
185
     * ```
186
     * ```html
187
     *  <igx-buttongroup [selectionMode]="'single'" [values]="cities"></igx-buttongroup>
188
     * ```
189
     */
190
    @Input() public values: any;
191

192
    /**
193
     * An @Input property that allows you to disable the `igx-buttongroup` component. By default it's false.
194
     * ```html
195
     * <igx-buttongroup [disabled]="true" [selectionMode]="'multi'" [values]="fontOptions"></igx-buttongroup>
196
     * ```
197
     */
198
    @Input({ transform: booleanAttribute })
199
    public get disabled(): boolean {
200
        return this._disabled;
442✔
201
    }
202
    public set disabled(value: boolean) {
203
        if (this._disabled !== value) {
×
204
            this._disabled = value;
×
205

206
            if (this.viewButtons && this.templateButtons) {
×
207
                this.buttons.forEach((b) => (b.disabled = this._disabled));
×
208
            }
209
        }
210
    }
211

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

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

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

277
    @ViewChildren(IgxButtonDirective) private viewButtons: QueryList<IgxButtonDirective>;
278
    @ContentChildren(IgxButtonDirective) private templateButtons: QueryList<IgxButtonDirective>;
279

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

299
    /**
300
     * @hidden
301
     */
302
    public selectedIndexes: number[] = [];
90✔
303

304
    protected buttonClickNotifier$ = new Subject<void>();
90✔
305
    protected queryListNotifier$ = new Subject<void>();
90✔
306

307
    private _isVertical: boolean;
308
    private _itemContentCssClass: string;
309
    private _disabled = false;
90✔
310
    private _selectionMode: 'single' | 'singleRequired' | 'multi' = 'single';
90✔
311

312
    private mutationObserver: MutationObserver;
313
    private observerConfig: MutationObserverInit = {
90✔
314
      attributeFilter: ["data-selected"],
315
      childList: true,
316
      subtree: true,
317
    };
318

319
    constructor(
320
        private _cdr: ChangeDetectorRef,
90✔
321
        private _renderer: Renderer2,
90✔
322
        private _el: ElementRef,
90✔
323
        @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions: IDisplayDensityOptions
90✔
324
    ) {
325
        super(_displayDensityOptions, _el);
90✔
326
    }
327

328
    /**
329
     * Gets the selected button/buttons.
330
     * ```typescript
331
     * @ViewChild("MyChild")
332
     * private buttonG: IgxButtonGroupComponent;
333
     * ngAfterViewInit(){
334
     *    let selectedButton = this.buttonG.selectedButtons;
335
     * }
336
     * ```
337
     */
338
    public get selectedButtons(): IgxButtonDirective[] {
339
        return this.buttons.filter((_, i) => this.selectedIndexes.indexOf(i) !== -1);
124✔
340
    }
341

342
    /**
343
     * Selects a button by its index.
344
     * ```typescript
345
     * @ViewChild("MyChild")
346
     * private buttonG: IgxButtonGroupComponent;
347
     * ngAfterViewInit(){
348
     *    this.buttonG.selectButton(2);
349
     *    this.cdr.detectChanges();
350
     * }
351
     * ```
352
     *
353
     * @memberOf {@link IgxButtonGroupComponent}
354
     */
355
    public selectButton(index: number) {
356
        if (index >= this.buttons.length || index < 0) {
62✔
357
            return;
2✔
358
        }
359

360
        this.updateSelected(index);
60✔
361

362
        const button = this.buttons[index];
60✔
363
        button.select();
60✔
364
    }
365

366
    /**
367
     * @hidden
368
     * @internal
369
     */
370
    public updateSelected(index: number) {
371
        const button = this.buttons[index];
113✔
372

373
        if (this.selectedIndexes.indexOf(index) === -1) {
113✔
374
            this.selectedIndexes.push(index);
113✔
375
        }
376

377
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'true');
113✔
378
        this._renderer.addClass(button.nativeElement, 'igx-button-group__item--selected');
113✔
379

380
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
113✔
381
        if (indexInViewButtons !== -1) {
113✔
382
            this.values[indexInViewButtons].selected = true;
29✔
383
        }
384

385
        // deselect other buttons if selectionMode is not multi
386
        if (this.selectionMode !== 'multi' && this.selectedIndexes.length > 1) {
113✔
387
            this.buttons.forEach((_, i) => {
20✔
388
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
54✔
389
                    this.deselectButton(i);
20✔
390
                }
391
            });
392
        }
393

394
    }
395

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

414
        const button = this.buttons[index];
140✔
415
        this.selectedIndexes.splice(this.selectedIndexes.indexOf(index), 1);
140✔
416

417
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'false');
140✔
418
        this._renderer.removeClass(button.nativeElement, 'igx-button-group__item--selected');
140✔
419
        button.deselect();
140✔
420

421
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
140✔
422
        if (indexInViewButtons !== -1) {
140✔
423
            this.values[indexInViewButtons].selected = false;
12✔
424
        }
425
    }
426

427
    /**
428
     * @hidden
429
     */
430
    public ngAfterContentInit() {
431
        this.templateButtons.forEach((button) => {
90✔
432
            if (!button.initialDensity) {
150✔
433
                button.displayDensity = this.displayDensity;
25✔
434
            }
435
        });
436
    }
437

438
    /**
439
     * @hidden
440
     */
441
    public ngAfterViewInit() {
442
        const initButtons = () => {
92✔
443
            // Cancel any existing buttonClick subscriptions
444
            this.buttonClickNotifier$.next();
92✔
445

446
            this.selectedIndexes.splice(0, this.selectedIndexes.length);
92✔
447

448
            // initial configuration
449
            this.buttons.forEach((button, index) => {
92✔
450
                const buttonElement = button.nativeElement;
208✔
451
                this._renderer.addClass(buttonElement, 'igx-button-group__item');
208✔
452

453
                if (this.disabled) {
208!
454
                    button.disabled = true;
×
455
                }
456

457
                if (button.selected) {
208✔
458
                    this.updateSelected(index);
53✔
459
                }
460

461
                button.buttonClick.pipe(takeUntil(this.buttonClickNotifier$)).subscribe((_) => this._clickHandler(index));
208✔
462
            });
463
        };
464

465
        this.mutationObserver = this.setMutationsObserver();
92✔
466

467
        this.viewButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
92✔
NEW
468
            this.mutationObserver.disconnect();
×
NEW
469
            initButtons();
×
NEW
470
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
471
        });
472
        this.templateButtons.changes.pipe(takeUntil(this.queryListNotifier$)).subscribe(() => {
92✔
NEW
473
            this.mutationObserver.disconnect();
×
NEW
474
            initButtons();
×
NEW
475
            this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
×
476
        });
477

478
        initButtons();
92✔
479
        this._cdr.detectChanges();
92✔
480
        this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
92✔
481
    }
482

483
    /**
484
     * @hidden
485
     */
486
    public ngOnDestroy() {
487
        this.buttonClickNotifier$.next();
90✔
488
        this.buttonClickNotifier$.complete();
90✔
489

490
        this.queryListNotifier$.next();
90✔
491
        this.queryListNotifier$.complete();
90✔
492

493
        this.mutationObserver?.disconnect();
90✔
494
    }
495

496
    /**
497
     * @hidden
498
     */
499
    public _clickHandler(index: number) {
500
        this.mutationObserver.disconnect();
38✔
501

502
        const button = this.buttons[index];
38✔
503
        const args: IButtonGroupEventArgs = { owner: this, button, index };
38✔
504

505
        if (this.selectionMode !== 'multi') {
38✔
506
            this.buttons.forEach((b, i) => {
33✔
507
                if (i !== index && this.selectedIndexes.indexOf(i) !== -1) {
70✔
508
                    this.deselected.emit({ owner: this, button: b, index: i });
8✔
509
                }
510
            });
511
        }
512

513
        if (this.selectedIndexes.indexOf(index) === -1) {
38✔
514
            this.selectButton(index);
25✔
515
            this.selected.emit(args);
25✔
516
        } else {
517
            if (this.selectionMode !== 'singleRequired') {
13✔
518
                this.deselectButton(index);
12✔
519
                this.deselected.emit(args);
12✔
520
            }
521
        }
522

523
        this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
38✔
524
    }
525

526
    private setMutationsObserver() {
527
        if (typeof MutationObserver !== 'undefined') {
92✔
528
            return new MutationObserver((records, observer) => {
92✔
529
                // Stop observing while handling changes
530
                observer.disconnect();
2✔
531

532
                const updatedButtons = this.getUpdatedButtons(records);
2✔
533

534
                if (updatedButtons.length > 0) {
2✔
535
                    updatedButtons.forEach((button) => {
2✔
536
                        const index = this.buttons.map((b) => b.nativeElement).indexOf(button);
18✔
537
                        const args: IButtonGroupEventArgs = { owner: this, button: this.buttons[index], index };
6✔
538

539
                        this.updateButtonSelectionState(index, args);
6✔
540
                    });
541
                }
542

543
                // Watch for changes again
544
                observer.observe(this._el.nativeElement, this.observerConfig);
2✔
545

546
                // Cleanup function
547
                this._renderer.listen(this._el.nativeElement, 'DOMNodeRemoved', () => {
2✔
548
                    observer.disconnect();
×
549
                });
550
            });
551
        }
552
    }
553

554
    private getUpdatedButtons(records: MutationRecord[]) {
555
        const updated: HTMLButtonElement[] = [];
2✔
556

557
        records
2✔
558
          .filter((x) => x.type === 'attributes')
6✔
559
          .reduce((prev, curr) => {
560
            prev.push(
6✔
561
              curr.target as HTMLButtonElement
562
            );
563
            return prev;
6✔
564
          }, updated);
565

566
        return updated;
2✔
567
    }
568

569
    private updateButtonSelectionState(index: number, args: IButtonGroupEventArgs) {
570
        if (this.selectedIndexes.indexOf(index) === -1) {
6✔
571
            this.selectButton(index);
5✔
572
            this.selected.emit(args);
5✔
573
        } else {
574
            this.deselectButton(index);
1✔
575
            this.deselected.emit(args);
1✔
576
        }
577
    }
578
}
579

580
export interface IButtonGroupEventArgs extends IBaseEventArgs {
581
    owner: IgxButtonGroupComponent;
582
    button: IgxButtonDirective;
583
    index: number;
584
}
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