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

IgniteUI / igniteui-angular / 8328783333

18 Mar 2024 02:57PM UTC coverage: 92.212% (-0.02%) from 92.231%
8328783333

push

github

web-flow
Merge pull request #13973 from IgniteUI/ikitanov/fix-13691-16.1.x

 Resolve buttons not selected if initialized after the group

15431 of 18146 branches covered (85.04%)

26997 of 29277 relevant lines covered (92.21%)

29659.72 hits per line

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

87.59
/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,
2✔
16
    ViewChildren,
17
    OnDestroy,
18
    ElementRef
19
} from '@angular/core';
2✔
20
import { Subject } from 'rxjs';
21
import { IgxButtonDirective } from '../directives/button/button.directive';
22
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
23

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

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

39
let NEXT_ID = 0;
40

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

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

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

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

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

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

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

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

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

205
            if (this.viewButtons && this.templateButtons) {
113✔
206
                this.buttons.forEach((b) => (b.disabled = this._disabled));
113✔
207
            }
113✔
208
        }
113✔
209
    }
29✔
210

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

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

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

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

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

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

303
    protected buttonClickNotifier$ = new Subject<boolean>();
304
    protected queryListNotifier$ = new Subject<boolean>();
305

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

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

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

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

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

5✔
359
        this.updateSelected(index);
360

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

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

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

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

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

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

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

412
        const button = this.buttons[index];
413
        this.selectedIndexes.splice(this.selectedIndexes.indexOf(index), 1);
414

415
        this._renderer.setAttribute(button.nativeElement, 'aria-pressed', 'false');
416
        this._renderer.removeClass(button.nativeElement, 'igx-button-group__item--selected');
417
        button.deselect();
418

419
        const indexInViewButtons = this.viewButtons.toArray().indexOf(button);
420
        if (indexInViewButtons !== -1) {
421
            this.values[indexInViewButtons].selected = false;
422
        }
423
    }
424

425
    /**
426
     * @hidden
427
     */
428
    public ngAfterContentInit() {
429
        this.templateButtons.forEach((button) => {
430
            if (!button.initialDensity) {
431
                button.displayDensity = this.displayDensity;
432
            }
433
        });
434
    }
435

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

444
            this.selectedIndexes.splice(0, this.selectedIndexes.length);
445

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

451
                if (this.disabled) {
452
                    button.disabled = true;
453
                }
454

455
                if (button.selected) {
456
                    this.updateSelected(index);
457
                }
458

459
                button.buttonClick.pipe(takeUntil(this.buttonClickNotifier$)).subscribe((_) => this._clickHandler(index));
460
            });
461
        };
462

463
        this.mutationObserver = this.setMutationsObserver();
464

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

476
        initButtons();
477
        this._cdr.detectChanges();
478
        this.mutationObserver?.observe(this._el.nativeElement, this.observerConfig);
479
    }
480

481
    /**
482
     * @hidden
483
     */
484
    public ngOnDestroy() {
485
        this.buttonClickNotifier$.next();
486
        this.buttonClickNotifier$.complete();
487

488
        this.queryListNotifier$.next();
489
        this.queryListNotifier$.complete();
490

491
        this.mutationObserver.disconnect();
492
    }
493

494
    /**
495
     * @hidden
496
     */
497
    public _clickHandler(index: number) {
498
        this.mutationObserver.disconnect();
499

500
        const button = this.buttons[index];
501
        const args: IButtonGroupEventArgs = { owner: this, button, index };
502

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

511
        if (this.selectedIndexes.indexOf(index) === -1) {
512
            this.selectButton(index);
513
            this.selected.emit(args);
514
        } else {
515
            if (this.selectionMode !== 'singleRequired') {
516
                this.deselectButton(index);
517
                this.deselected.emit(args);
518
            }
519
        }
520

521
        this.mutationObserver.observe(this._el.nativeElement, this.observerConfig);
522
    }
523

524
    private setMutationsObserver() {
525
        return new MutationObserver((records, observer) => {
526
            // Stop observing while handling changes
527
            observer.disconnect();
528

529
            const updatedButtons = this.getUpdatedButtons(records);
530

531
            if (updatedButtons.length > 0) {
532
                updatedButtons.forEach((button) => {
533
                    const index = this.buttons.map((b) => b.nativeElement).indexOf(button);
534
                    const args: IButtonGroupEventArgs = { owner: this, button: this.buttons[index], index };
535

536
                    this.updateButtonSelectionState(index, args);
537
                });
538
            }
539

540
            // Watch for changes again
541
            observer.observe(this._el.nativeElement, this.observerConfig);
542
        });
543
    }
544

545
    private getUpdatedButtons(records: MutationRecord[]) {
546
        const updated: HTMLButtonElement[] = [];
547

548
        records
549
          .filter((x) => x.type === 'attributes')
550
          .reduce((prev, curr) => {
551
            prev.push(
552
              curr.target as HTMLButtonElement
553
            );
554
            return prev;
555
          }, updated);
556

557
        return updated;
558
    }
559

560
    private updateButtonSelectionState(index: number, args: IButtonGroupEventArgs) {
561
        if (this.selectedIndexes.indexOf(index) === -1) {
562
            this.selectButton(index);
563
            this.selected.emit(args);
564
        } else {
565
            this.deselectButton(index);
566
            this.deselected.emit(args);
567
        }
568
    }
569
}
570

571
export interface IButtonGroupEventArgs extends IBaseEventArgs {
572
    owner: IgxButtonGroupComponent;
573
    button: IgxButtonDirective;
574
    index: number;
575
}
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