Skip to content

Commit 6ff097d

Browse files
committed
feat(button-toggle): general component cleanup and support value input in multiple mode
* Reworks the `MatButtonToggleGroup` component to remove the need for the `MatButtonToggleGroupMultiple` component and to avoid having to implement features in two places. * Reworks the `MatButtonToggleGroup` to use the `SelectionModel` for managing its state. * Switches all of the `async` button toggle tests to run in `fakeAsync`. As a side-effect of the above-mentioned changes, the toggle group in multiple mode now supports the same inputs as the one in single-selection mode (`value` input/output, `name` input, `change` event etc.). Note: this is along the same lines as #2773, however #2773 got left behind for too long and it would need a lot more work to fit in our current setup. Fixes #9058.
1 parent 64ef3a8 commit 6ff097d

File tree

3 files changed

+254
-217
lines changed

3 files changed

+254
-217
lines changed

src/lib/button-toggle/button-toggle-module.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,14 @@
77
*/
88

99
import {NgModule} from '@angular/core';
10-
import {MatButtonToggleGroup, MatButtonToggleGroupMultiple, MatButtonToggle} from './button-toggle';
10+
import {MatButtonToggleGroup, MatButtonToggle} from './button-toggle';
1111
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
12-
import {UNIQUE_SELECTION_DISPATCHER_PROVIDER} from '@angular/cdk/collections';
1312
import {A11yModule} from '@angular/cdk/a11y';
1413

1514

1615
@NgModule({
1716
imports: [MatCommonModule, MatRippleModule, A11yModule],
18-
exports: [
19-
MatButtonToggleGroup,
20-
MatButtonToggleGroupMultiple,
21-
MatButtonToggle,
22-
MatCommonModule,
23-
],
24-
declarations: [MatButtonToggleGroup, MatButtonToggleGroupMultiple, MatButtonToggle],
25-
providers: [UNIQUE_SELECTION_DISPATCHER_PROVIDER]
17+
exports: [MatCommonModule, MatButtonToggleGroup, MatButtonToggle],
18+
declarations: [MatButtonToggleGroup, MatButtonToggle],
2619
})
2720
export class MatButtonToggleModule {}

src/lib/button-toggle/button-toggle.spec.ts

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
1-
import {
2-
async,
3-
fakeAsync,
4-
tick,
5-
ComponentFixture,
6-
TestBed,
7-
} from '@angular/core/testing';
1+
import {fakeAsync, tick, ComponentFixture, TestBed} from '@angular/core/testing';
82
import {dispatchMouseEvent} from '@angular/cdk/testing';
93
import {NgModel, FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms';
104
import {Component, DebugElement} from '@angular/core';
115
import {By} from '@angular/platform-browser';
126
import {
137
MatButtonToggleGroup,
14-
MatButtonToggle,
158
MatButtonToggleGroupMultiple,
9+
MatButtonToggle,
1610
MatButtonToggleChange,
1711
MatButtonToggleModule,
1812
} from './index';
1913

20-
2114
describe('MatButtonToggle with forms', () => {
2215

23-
beforeEach(async(() => {
16+
beforeEach(fakeAsync(() => {
2417
TestBed.configureTestingModule({
2518
imports: [MatButtonToggleModule, FormsModule, ReactiveFormsModule],
2619
declarations: [
@@ -38,7 +31,7 @@ describe('MatButtonToggle with forms', () => {
3831
let groupInstance: MatButtonToggleGroup;
3932
let testComponent: ButtonToggleGroupWithFormControl;
4033

41-
beforeEach(async(() => {
34+
beforeEach(fakeAsync(() => {
4235
fixture = TestBed.createComponent(ButtonToggleGroupWithFormControl);
4336
fixture.detectChanges();
4437

@@ -88,7 +81,7 @@ describe('MatButtonToggle with forms', () => {
8881
let groupNgModel: NgModel;
8982
let buttonToggleLabels: HTMLElement[];
9083

91-
beforeEach(async(() => {
84+
beforeEach(fakeAsync(() => {
9285
fixture = TestBed.createComponent(ButtonToggleGroupWithNgModel);
9386
fixture.detectChanges();
9487
testComponent = fixture.debugElement.componentInstance;
@@ -139,7 +132,10 @@ describe('MatButtonToggle with forms', () => {
139132
for (let buttonToggle of buttonToggleInstances) {
140133
expect(buttonToggle.checked).toBe(groupInstance.value === buttonToggle.value);
141134
}
142-
expect(groupInstance.selected!.value).toBe(groupInstance.value);
135+
136+
const selected = groupInstance.selected as MatButtonToggle;
137+
138+
expect(selected.value).toBe(groupInstance.value);
143139
});
144140

145141
it('should have the correct FormControl state initially and after interaction',
@@ -203,7 +199,7 @@ describe('MatButtonToggle with forms', () => {
203199

204200
describe('MatButtonToggle without forms', () => {
205201

206-
beforeEach(async(() => {
202+
beforeEach(fakeAsync(() => {
207203
TestBed.configureTestingModule({
208204
imports: [MatButtonToggleModule],
209205
declarations: [
@@ -338,7 +334,6 @@ describe('MatButtonToggle without forms', () => {
338334
let changeSpy = jasmine.createSpy('button-toggle change listener');
339335
buttonToggleInstances[0].change.subscribe(changeSpy);
340336

341-
342337
buttonToggleLabelElements[0].click();
343338
fixture.detectChanges();
344339
tick();
@@ -412,14 +407,16 @@ describe('MatButtonToggle without forms', () => {
412407

413408
fixture.detectChanges();
414409

410+
// Note that we cast to a boolean, because the event has some circular references
411+
// which will crash the runner when Jasmine attempts to stringify them.
412+
expect(!!testComponent.lastEvent).toBe(false);
415413
expect(groupInstance.value).toBe('red');
416-
expect(testComponent.lastEvent).toBeFalsy();
417414

418415
groupInstance.value = 'green';
419416
fixture.detectChanges();
420417

418+
expect(!!testComponent.lastEvent).toBe(false);
421419
expect(groupInstance.value).toBe('green');
422-
expect(testComponent.lastEvent).toBeFalsy();
423420
});
424421

425422
});
@@ -431,20 +428,19 @@ describe('MatButtonToggle without forms', () => {
431428
let buttonToggleDebugElements: DebugElement[];
432429
let buttonToggleNativeElements: HTMLElement[];
433430
let buttonToggleLabelElements: HTMLLabelElement[];
434-
let groupInstance: MatButtonToggleGroupMultiple;
431+
let groupInstance: MatButtonToggleGroup;
435432
let buttonToggleInstances: MatButtonToggle[];
436433
let testComponent: ButtonTogglesInsideButtonToggleGroupMultiple;
437434

438-
beforeEach(async(() => {
435+
beforeEach(fakeAsync(() => {
439436
fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple);
440437
fixture.detectChanges();
441438

442439
testComponent = fixture.debugElement.componentInstance;
443440

444-
groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroupMultiple));
441+
groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup));
445442
groupNativeElement = groupDebugElement.nativeElement;
446-
groupInstance = groupDebugElement.injector.get<MatButtonToggleGroupMultiple>(
447-
MatButtonToggleGroupMultiple);
443+
groupInstance = groupDebugElement.injector.get<MatButtonToggleGroup>(MatButtonToggleGroup);
448444

449445
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle));
450446
buttonToggleNativeElements = buttonToggleDebugElements
@@ -468,16 +464,22 @@ describe('MatButtonToggle without forms', () => {
468464
let nativeCheckboxLabel = buttonToggleDebugElements[0].query(By.css('label')).nativeElement;
469465

470466
nativeCheckboxLabel.click();
467+
468+
expect(groupInstance.value).toEqual(['eggs']);
471469
expect(buttonToggleInstances[0].checked).toBe(true);
472470
});
473471

474472
it('should allow for multiple toggles to be selected', () => {
475473
buttonToggleInstances[0].checked = true;
476474
fixture.detectChanges();
475+
476+
expect(groupInstance.value).toEqual(['eggs']);
477477
expect(buttonToggleInstances[0].checked).toBe(true);
478478

479479
buttonToggleInstances[1].checked = true;
480480
fixture.detectChanges();
481+
482+
expect(groupInstance.value).toEqual(['eggs', 'flour']);
481483
expect(buttonToggleInstances[1].checked).toBe(true);
482484
expect(buttonToggleInstances[0].checked).toBe(true);
483485
});
@@ -488,6 +490,7 @@ describe('MatButtonToggle without forms', () => {
488490
nativeCheckboxInput.click();
489491
fixture.detectChanges();
490492

493+
expect(groupInstance.value).toEqual(['eggs']);
491494
expect(buttonToggleInstances[0].checked).toBe(true);
492495
});
493496

@@ -500,18 +503,23 @@ describe('MatButtonToggle without forms', () => {
500503
expect(groupNativeElement.classList).toContain('mat-button-toggle-vertical');
501504
});
502505

503-
it('should deselect a button toggle when selected twice', () => {
504-
buttonToggleNativeElements[0].click();
506+
it('should deselect a button toggle when selected twice', fakeAsync(() => {
507+
buttonToggleLabelElements[0].click();
505508
fixture.detectChanges();
509+
tick();
506510

507-
buttonToggleNativeElements[0].click();
511+
expect(buttonToggleInstances[0].checked).toBe(true);
512+
expect(groupInstance.value).toEqual(['eggs']);
513+
514+
buttonToggleLabelElements[0].click();
508515
fixture.detectChanges();
516+
tick();
509517

518+
expect(groupInstance.value).toEqual([]);
510519
expect(buttonToggleInstances[0].checked).toBe(false);
511-
});
520+
}));
512521

513522
it('should emit a change event for state changes', fakeAsync(() => {
514-
515523
expect(buttonToggleInstances[0].checked).toBe(false);
516524

517525
let changeSpy = jasmine.createSpy('button-toggle change listener');
@@ -521,17 +529,29 @@ describe('MatButtonToggle without forms', () => {
521529
fixture.detectChanges();
522530
tick();
523531
expect(changeSpy).toHaveBeenCalled();
532+
expect(groupInstance.value).toEqual(['eggs']);
524533

525534
buttonToggleLabelElements[0].click();
526535
fixture.detectChanges();
527536
tick();
537+
expect(groupInstance.value).toEqual([]);
528538

529539
// The default browser behavior is to emit an event, when the value was set
530540
// to false. That's because the current input type is set to `checkbox` when
531541
// using the multiple mode.
532542
expect(changeSpy).toHaveBeenCalledTimes(2);
533543
}));
534544

545+
it('should throw when attempting to assign a non-array value', () => {
546+
expect(() => {
547+
groupInstance.value = 'not-an-array';
548+
}).toThrowError(/Value must be an array/);
549+
});
550+
551+
it('should be able to query for the deprecated `MatButtonToggleGroupMultiple`', () => {
552+
expect(fixture.debugElement.query(By.directive(MatButtonToggleGroupMultiple))).toBeTruthy();
553+
});
554+
535555
});
536556

537557
describe('as standalone', () => {
@@ -541,7 +561,7 @@ describe('MatButtonToggle without forms', () => {
541561
let buttonToggleLabelElement: HTMLLabelElement;
542562
let buttonToggleInstance: MatButtonToggle;
543563

544-
beforeEach(async(() => {
564+
beforeEach(fakeAsync(() => {
545565
fixture = TestBed.createComponent(StandaloneButtonToggle);
546566
fixture.detectChanges();
547567

@@ -551,7 +571,7 @@ describe('MatButtonToggle without forms', () => {
551571
buttonToggleInstance = buttonToggleDebugElement.componentInstance;
552572
}));
553573

554-
it('should toggle when clicked', () => {
574+
it('should toggle when clicked', fakeAsync(() => {
555575
buttonToggleLabelElement.click();
556576

557577
fixture.detectChanges();
@@ -562,7 +582,7 @@ describe('MatButtonToggle without forms', () => {
562582
fixture.detectChanges();
563583

564584
expect(buttonToggleInstance.checked).toBe(false);
565-
});
585+
}));
566586

567587
it('should emit a change event for state changes', fakeAsync(() => {
568588

0 commit comments

Comments
 (0)