Skip to content

Commit f6a5fd9

Browse files
committed
fix(slide-toggle): invalid required validator in template-driven forms
Currently using the `slide-toggle` in a form using template-driven forms causes the slide-toggle to retrieve a wrong validator if the `required` attribute is set. This is because by default `@angular/forms` uses an input validator that ensures that the value is just defined. This is always the case for a slide-toggle since the value is always `true` or `false`. The solution to this problem is that we need to provide the checkbox validator for required slide-toggle components. The checkbox validator from the forms package ensures that the control is only valid if the slide-toggle is checked.
1 parent 5fb0dc6 commit f6a5fd9

File tree

6 files changed

+124
-4
lines changed

6 files changed

+124
-4
lines changed

src/material-experimental/mdc-slide-toggle/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ng_module(
1515
deps = [
1616
"//src/cdk/coercion",
1717
"//src/material/core",
18+
"//src/material/slide-toggle",
1819
"@npm//@angular/animations",
1920
"@npm//@angular/common",
2021
"@npm//@angular/core",

src/material-experimental/mdc-slide-toggle/module.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,21 @@
99
import {CommonModule} from '@angular/common';
1010
import {NgModule} from '@angular/core';
1111
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
12+
import {_MatSlideToggleRequiredValidatorModule} from '@angular/material/slide-toggle';
1213
import {MatSlideToggle} from './slide-toggle';
1314

1415
@NgModule({
15-
imports: [MatCommonModule, MatRippleModule, CommonModule],
16-
exports: [MatSlideToggle, MatCommonModule],
16+
imports: [
17+
_MatSlideToggleRequiredValidatorModule,
18+
MatCommonModule,
19+
MatRippleModule,
20+
CommonModule
21+
],
22+
exports: [
23+
_MatSlideToggleRequiredValidatorModule,
24+
MatSlideToggle,
25+
MatCommonModule
26+
],
1727
declarations: [MatSlideToggle],
1828
})
1929
export class MatSlideToggleModule {

src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,32 @@ describe('MatSlideToggle with forms', () => {
650650

651651
expect(testComponent.isSubmitted).toBe(true);
652652
});
653+
654+
it('should have proper invalid state if unchecked', () => {
655+
testComponent.isRequired = true;
656+
fixture.detectChanges();
657+
658+
const slideToggleEl = fixture.nativeElement.querySelector('.mat-mdc-slide-toggle');
659+
660+
expect(slideToggleEl.classList).toContain('ng-invalid');
661+
expect(slideToggleEl.classList).not.toContain('ng-valid');
662+
663+
// The required slide-toggle will be checked and the form control
664+
// should become valid.
665+
inputElement.click();
666+
fixture.detectChanges();
667+
668+
expect(slideToggleEl.classList).not.toContain('ng-invalid');
669+
expect(slideToggleEl.classList).toContain('ng-valid');
670+
671+
// The required slide-toggle will be unchecked and the form control
672+
// should become invalid.
673+
inputElement.click();
674+
fixture.detectChanges();
675+
676+
expect(slideToggleEl.classList).toContain('ng-invalid');
677+
expect(slideToggleEl.classList).not.toContain('ng-valid');
678+
});
653679
});
654680

655681
describe('with model and change event', () => {

src/material/slide-toggle/slide-toggle-module.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,29 @@ import {NgModule} from '@angular/core';
1111
import {GestureConfig, MatCommonModule, MatRippleModule} from '@angular/material/core';
1212
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
1313
import {MatSlideToggle} from './slide-toggle';
14+
import {MatSlideToggleRequiredValidator} from './slide-toggle-required-validator';
1415

16+
/** This module is used by both original and MDC-based slide-toggle implementations. */
17+
@NgModule({
18+
exports: [MatSlideToggleRequiredValidator],
19+
declarations: [MatSlideToggleRequiredValidator],
20+
})
21+
// tslint:disable-next-line:class-name
22+
export class _MatSlideToggleRequiredValidatorModule {
23+
}
1524

1625
@NgModule({
17-
imports: [MatRippleModule, MatCommonModule, ObserversModule],
18-
exports: [MatSlideToggle, MatCommonModule],
26+
imports: [
27+
_MatSlideToggleRequiredValidatorModule,
28+
MatRippleModule,
29+
MatCommonModule,
30+
ObserversModule,
31+
],
32+
exports: [
33+
_MatSlideToggleRequiredValidatorModule,
34+
MatSlideToggle,
35+
MatCommonModule
36+
],
1937
declarations: [MatSlideToggle],
2038
providers: [
2139
{provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
Directive,
11+
forwardRef,
12+
Provider,
13+
} from '@angular/core';
14+
import {
15+
CheckboxRequiredValidator,
16+
NG_VALIDATORS,
17+
} from '@angular/forms';
18+
19+
export const MAT_SLIDE_TOGGLE_REQUIRED_VALIDATOR: Provider = {
20+
provide: NG_VALIDATORS,
21+
useExisting: forwardRef(() => MatSlideToggleRequiredValidator),
22+
multi: true
23+
};
24+
25+
/**
26+
* Validator for Material slide-toggle components with the required attribute in a
27+
* template-driven form. The default validator for required form controls asserts
28+
* that the control value is not undefined but that is not appropriate for a slide-toggle
29+
* where the value is always defined.
30+
*
31+
* Required slide-toggle form controls are valid when checked.
32+
*/
33+
@Directive({
34+
selector: `mat-slide-toggle[required][formControlName],
35+
mat-slide-toggle[required][formControl], mat-slide-toggle[required][ngModel]`,
36+
providers: [MAT_SLIDE_TOGGLE_REQUIRED_VALIDATOR],
37+
host: {'[attr.required]': 'required ? "" : null'}
38+
})
39+
export class MatSlideToggleRequiredValidator extends CheckboxRequiredValidator {}

src/material/slide-toggle/slide-toggle.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,32 @@ describe('MatSlideToggle with forms', () => {
10411041

10421042
expect(testComponent.isSubmitted).toBe(true);
10431043
});
1044+
1045+
it('should have proper invalid state if unchecked', () => {
1046+
testComponent.isRequired = true;
1047+
fixture.detectChanges();
1048+
1049+
const slideToggleEl = fixture.nativeElement.querySelector('.mat-slide-toggle');
1050+
1051+
expect(slideToggleEl.classList).toContain('ng-invalid');
1052+
expect(slideToggleEl.classList).not.toContain('ng-valid');
1053+
1054+
// The required slide-toggle will be checked and the form control
1055+
// should become valid.
1056+
inputElement.click();
1057+
fixture.detectChanges();
1058+
1059+
expect(slideToggleEl.classList).not.toContain('ng-invalid');
1060+
expect(slideToggleEl.classList).toContain('ng-valid');
1061+
1062+
// The required slide-toggle will be unchecked and the form control
1063+
// should become invalid.
1064+
inputElement.click();
1065+
fixture.detectChanges();
1066+
1067+
expect(slideToggleEl.classList).toContain('ng-invalid');
1068+
expect(slideToggleEl.classList).not.toContain('ng-valid');
1069+
});
10441070
});
10451071

10461072
describe('with model and change event', () => {

0 commit comments

Comments
 (0)