Skip to content

Commit 81d63df

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 45399c6 commit 81d63df

File tree

3 files changed

+255
-219
lines changed

3 files changed

+255
-219
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: 51 additions & 32 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

@@ -90,7 +83,7 @@ describe('MatButtonToggle with forms', () => {
9083
let groupNgModel: NgModel;
9184
let buttonToggleLabels: HTMLElement[];
9285

93-
beforeEach(async(() => {
86+
beforeEach(fakeAsync(() => {
9487
fixture = TestBed.createComponent(ButtonToggleGroupWithNgModel);
9588
fixture.detectChanges();
9689
testComponent = fixture.debugElement.componentInstance;
@@ -144,7 +137,10 @@ describe('MatButtonToggle with forms', () => {
144137
for (let buttonToggle of buttonToggleInstances) {
145138
expect(buttonToggle.checked).toBe(groupInstance.value === buttonToggle.value);
146139
}
147-
expect(groupInstance.selected!.value).toBe(groupInstance.value);
140+
141+
const selected = groupInstance.selected as MatButtonToggle;
142+
143+
expect(selected.value).toBe(groupInstance.value);
148144
});
149145

150146
it('should have the correct FormControl state initially and after interaction',
@@ -208,7 +204,7 @@ describe('MatButtonToggle with forms', () => {
208204

209205
describe('MatButtonToggle without forms', () => {
210206

211-
beforeEach(async(() => {
207+
beforeEach(fakeAsync(() => {
212208
TestBed.configureTestingModule({
213209
imports: [MatButtonToggleModule],
214210
declarations: [
@@ -343,7 +339,6 @@ describe('MatButtonToggle without forms', () => {
343339
let changeSpy = jasmine.createSpy('button-toggle change listener');
344340
buttonToggleInstances[0].change.subscribe(changeSpy);
345341

346-
347342
buttonToggleLabelElements[0].click();
348343
fixture.detectChanges();
349344
tick();
@@ -417,14 +412,16 @@ describe('MatButtonToggle without forms', () => {
417412

418413
fixture.detectChanges();
419414

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

423420
groupInstance.value = 'green';
424421
fixture.detectChanges();
425422

423+
expect(!!testComponent.lastEvent).toBe(false);
426424
expect(groupInstance.value).toBe('green');
427-
expect(testComponent.lastEvent).toBeFalsy();
428425
});
429426

430427
});
@@ -436,20 +433,19 @@ describe('MatButtonToggle without forms', () => {
436433
let buttonToggleDebugElements: DebugElement[];
437434
let buttonToggleNativeElements: HTMLElement[];
438435
let buttonToggleLabelElements: HTMLLabelElement[];
439-
let groupInstance: MatButtonToggleGroupMultiple;
436+
let groupInstance: MatButtonToggleGroup;
440437
let buttonToggleInstances: MatButtonToggle[];
441438
let testComponent: ButtonTogglesInsideButtonToggleGroupMultiple;
442439

443-
beforeEach(async(() => {
440+
beforeEach(fakeAsync(() => {
444441
fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple);
445442
fixture.detectChanges();
446443

447444
testComponent = fixture.debugElement.componentInstance;
448445

449-
groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroupMultiple));
446+
groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup));
450447
groupNativeElement = groupDebugElement.nativeElement;
451-
groupInstance = groupDebugElement.injector.get<MatButtonToggleGroupMultiple>(
452-
MatButtonToggleGroupMultiple);
448+
groupInstance = groupDebugElement.injector.get<MatButtonToggleGroup>(MatButtonToggleGroup);
453449

454450
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle));
455451
buttonToggleNativeElements = buttonToggleDebugElements
@@ -473,16 +469,22 @@ describe('MatButtonToggle without forms', () => {
473469
let nativeCheckboxLabel = buttonToggleDebugElements[0].query(By.css('label')).nativeElement;
474470

475471
nativeCheckboxLabel.click();
472+
473+
expect(groupInstance.value).toEqual(['eggs']);
476474
expect(buttonToggleInstances[0].checked).toBe(true);
477475
});
478476

479477
it('should allow for multiple toggles to be selected', () => {
480478
buttonToggleInstances[0].checked = true;
481479
fixture.detectChanges();
480+
481+
expect(groupInstance.value).toEqual(['eggs']);
482482
expect(buttonToggleInstances[0].checked).toBe(true);
483483

484484
buttonToggleInstances[1].checked = true;
485485
fixture.detectChanges();
486+
487+
expect(groupInstance.value).toEqual(['eggs', 'flour']);
486488
expect(buttonToggleInstances[1].checked).toBe(true);
487489
expect(buttonToggleInstances[0].checked).toBe(true);
488490
});
@@ -493,6 +495,7 @@ describe('MatButtonToggle without forms', () => {
493495
nativeCheckboxInput.click();
494496
fixture.detectChanges();
495497

498+
expect(groupInstance.value).toEqual(['eggs']);
496499
expect(buttonToggleInstances[0].checked).toBe(true);
497500
});
498501

@@ -505,18 +508,23 @@ describe('MatButtonToggle without forms', () => {
505508
expect(groupNativeElement.classList).toContain('mat-button-toggle-vertical');
506509
});
507510

508-
it('should deselect a button toggle when selected twice', () => {
509-
buttonToggleNativeElements[0].click();
511+
it('should deselect a button toggle when selected twice', fakeAsync(() => {
512+
buttonToggleLabelElements[0].click();
510513
fixture.detectChanges();
514+
tick();
511515

512-
buttonToggleNativeElements[0].click();
516+
expect(buttonToggleInstances[0].checked).toBe(true);
517+
expect(groupInstance.value).toEqual(['eggs']);
518+
519+
buttonToggleLabelElements[0].click();
513520
fixture.detectChanges();
521+
tick();
514522

523+
expect(groupInstance.value).toEqual([]);
515524
expect(buttonToggleInstances[0].checked).toBe(false);
516-
});
525+
}));
517526

518527
it('should emit a change event for state changes', fakeAsync(() => {
519-
520528
expect(buttonToggleInstances[0].checked).toBe(false);
521529

522530
let changeSpy = jasmine.createSpy('button-toggle change listener');
@@ -526,17 +534,29 @@ describe('MatButtonToggle without forms', () => {
526534
fixture.detectChanges();
527535
tick();
528536
expect(changeSpy).toHaveBeenCalled();
537+
expect(groupInstance.value).toEqual(['eggs']);
529538

530539
buttonToggleLabelElements[0].click();
531540
fixture.detectChanges();
532541
tick();
542+
expect(groupInstance.value).toEqual([]);
533543

534544
// The default browser behavior is to emit an event, when the value was set
535545
// to false. That's because the current input type is set to `checkbox` when
536546
// using the multiple mode.
537547
expect(changeSpy).toHaveBeenCalledTimes(2);
538548
}));
539549

550+
it('should throw when attempting to assign a non-array value', () => {
551+
expect(() => {
552+
groupInstance.value = 'not-an-array';
553+
}).toThrowError(/Value must be an array/);
554+
});
555+
556+
it('should be able to query for the deprecated `MatButtonToggleGroupMultiple`', () => {
557+
expect(fixture.debugElement.query(By.directive(MatButtonToggleGroupMultiple))).toBeTruthy();
558+
});
559+
540560
});
541561

542562
describe('as standalone', () => {
@@ -547,19 +567,18 @@ describe('MatButtonToggle without forms', () => {
547567
let buttonToggleInstance: MatButtonToggle;
548568
let testComponent: StandaloneButtonToggle;
549569

550-
beforeEach(async(() => {
570+
beforeEach(fakeAsync(() => {
551571
fixture = TestBed.createComponent(StandaloneButtonToggle);
552572
fixture.detectChanges();
553573

554-
testComponent = fixture.debugElement.componentInstance;
555-
574+
testComponent = fixture.componentInstance;
556575
buttonToggleDebugElement = fixture.debugElement.query(By.directive(MatButtonToggle));
557576
buttonToggleNativeElement = buttonToggleDebugElement.nativeElement;
558577
buttonToggleLabelElement = fixture.debugElement.query(By.css('label')).nativeElement;
559578
buttonToggleInstance = buttonToggleDebugElement.componentInstance;
560579
}));
561580

562-
it('should toggle when clicked', () => {
581+
it('should toggle when clicked', fakeAsync(() => {
563582
buttonToggleLabelElement.click();
564583

565584
fixture.detectChanges();
@@ -570,7 +589,7 @@ describe('MatButtonToggle without forms', () => {
570589
fixture.detectChanges();
571590

572591
expect(buttonToggleInstance.checked).toBe(false);
573-
});
592+
}));
574593

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

0 commit comments

Comments
 (0)