From dc0c271ed02bdba9eda5ab8e3081e2462936b20b Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 19 Jul 2019 16:28:58 +0200 Subject: [PATCH] fix(slide-toggle): invalid required validator in template-driven forms (#16547) 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. --- .../mdc-slide-toggle/BUILD.bazel | 1 + .../mdc-slide-toggle/module.ts | 14 ++++++- .../mdc-slide-toggle/slide-toggle.spec.ts | 26 +++++++++++++ src/material/slide-toggle/public-api.ts | 1 + .../slide-toggle/slide-toggle-module.ts | 22 ++++++++++- .../slide-toggle-required-validator.ts | 38 +++++++++++++++++++ .../slide-toggle/slide-toggle.spec.ts | 26 +++++++++++++ .../material/slide-toggle.d.ts | 8 ++++ 8 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 src/material/slide-toggle/slide-toggle-required-validator.ts diff --git a/src/material-experimental/mdc-slide-toggle/BUILD.bazel b/src/material-experimental/mdc-slide-toggle/BUILD.bazel index 6801c0608c8d..a87a5a0568f6 100644 --- a/src/material-experimental/mdc-slide-toggle/BUILD.bazel +++ b/src/material-experimental/mdc-slide-toggle/BUILD.bazel @@ -15,6 +15,7 @@ ng_module( deps = [ "//src/cdk/coercion", "//src/material/core", + "//src/material/slide-toggle", "@npm//@angular/animations", "@npm//@angular/common", "@npm//@angular/core", diff --git a/src/material-experimental/mdc-slide-toggle/module.ts b/src/material-experimental/mdc-slide-toggle/module.ts index b1e4ad5737c3..67b3500221a5 100644 --- a/src/material-experimental/mdc-slide-toggle/module.ts +++ b/src/material-experimental/mdc-slide-toggle/module.ts @@ -9,11 +9,21 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {MatCommonModule, MatRippleModule} from '@angular/material/core'; +import {_MatSlideToggleRequiredValidatorModule} from '@angular/material/slide-toggle'; import {MatSlideToggle} from './slide-toggle'; @NgModule({ - imports: [MatCommonModule, MatRippleModule, CommonModule], - exports: [MatSlideToggle, MatCommonModule], + imports: [ + _MatSlideToggleRequiredValidatorModule, + MatCommonModule, + MatRippleModule, + CommonModule + ], + exports: [ + _MatSlideToggleRequiredValidatorModule, + MatSlideToggle, + MatCommonModule + ], declarations: [MatSlideToggle], }) export class MatSlideToggleModule { diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts b/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts index b808438eae43..b09661a31c36 100644 --- a/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts +++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts @@ -650,6 +650,32 @@ describe('MatSlideToggle with forms', () => { expect(testComponent.isSubmitted).toBe(true); }); + + it('should have proper invalid state if unchecked', () => { + testComponent.isRequired = true; + fixture.detectChanges(); + + const slideToggleEl = fixture.nativeElement.querySelector('.mat-mdc-slide-toggle'); + + expect(slideToggleEl.classList).toContain('ng-invalid'); + expect(slideToggleEl.classList).not.toContain('ng-valid'); + + // The required slide-toggle will be checked and the form control + // should become valid. + inputElement.click(); + fixture.detectChanges(); + + expect(slideToggleEl.classList).not.toContain('ng-invalid'); + expect(slideToggleEl.classList).toContain('ng-valid'); + + // The required slide-toggle will be unchecked and the form control + // should become invalid. + inputElement.click(); + fixture.detectChanges(); + + expect(slideToggleEl.classList).toContain('ng-invalid'); + expect(slideToggleEl.classList).not.toContain('ng-valid'); + }); }); describe('with model and change event', () => { diff --git a/src/material/slide-toggle/public-api.ts b/src/material/slide-toggle/public-api.ts index 1598998e60f8..6f5078ec5988 100644 --- a/src/material/slide-toggle/public-api.ts +++ b/src/material/slide-toggle/public-api.ts @@ -9,3 +9,4 @@ export * from './slide-toggle-module'; export * from './slide-toggle'; export * from './slide-toggle-config'; +export * from './slide-toggle-required-validator'; diff --git a/src/material/slide-toggle/slide-toggle-module.ts b/src/material/slide-toggle/slide-toggle-module.ts index a8c47cd76b61..509751404156 100644 --- a/src/material/slide-toggle/slide-toggle-module.ts +++ b/src/material/slide-toggle/slide-toggle-module.ts @@ -11,11 +11,29 @@ import {NgModule} from '@angular/core'; import {GestureConfig, MatCommonModule, MatRippleModule} from '@angular/material/core'; import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser'; import {MatSlideToggle} from './slide-toggle'; +import {MatSlideToggleRequiredValidator} from './slide-toggle-required-validator'; +/** This module is used by both original and MDC-based slide-toggle implementations. */ +@NgModule({ + exports: [MatSlideToggleRequiredValidator], + declarations: [MatSlideToggleRequiredValidator], +}) +// tslint:disable-next-line:class-name +export class _MatSlideToggleRequiredValidatorModule { +} @NgModule({ - imports: [MatRippleModule, MatCommonModule, ObserversModule], - exports: [MatSlideToggle, MatCommonModule], + imports: [ + _MatSlideToggleRequiredValidatorModule, + MatRippleModule, + MatCommonModule, + ObserversModule, + ], + exports: [ + _MatSlideToggleRequiredValidatorModule, + MatSlideToggle, + MatCommonModule + ], declarations: [MatSlideToggle], providers: [ {provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig} diff --git a/src/material/slide-toggle/slide-toggle-required-validator.ts b/src/material/slide-toggle/slide-toggle-required-validator.ts new file mode 100644 index 000000000000..768881e7d7c7 --- /dev/null +++ b/src/material/slide-toggle/slide-toggle-required-validator.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + Directive, + forwardRef, + Provider, +} from '@angular/core'; +import { + CheckboxRequiredValidator, + NG_VALIDATORS, +} from '@angular/forms'; + +export const MAT_SLIDE_TOGGLE_REQUIRED_VALIDATOR: Provider = { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MatSlideToggleRequiredValidator), + multi: true +}; + +/** + * Validator for Material slide-toggle components with the required attribute in a + * template-driven form. The default validator for required form controls asserts + * that the control value is not undefined but that is not appropriate for a slide-toggle + * where the value is always defined. + * + * Required slide-toggle form controls are valid when checked. + */ +@Directive({ + selector: `mat-slide-toggle[required][formControlName], + mat-slide-toggle[required][formControl], mat-slide-toggle[required][ngModel]`, + providers: [MAT_SLIDE_TOGGLE_REQUIRED_VALIDATOR], +}) +export class MatSlideToggleRequiredValidator extends CheckboxRequiredValidator {} diff --git a/src/material/slide-toggle/slide-toggle.spec.ts b/src/material/slide-toggle/slide-toggle.spec.ts index 5038654b72b3..302297aadc1f 100644 --- a/src/material/slide-toggle/slide-toggle.spec.ts +++ b/src/material/slide-toggle/slide-toggle.spec.ts @@ -1041,6 +1041,32 @@ describe('MatSlideToggle with forms', () => { expect(testComponent.isSubmitted).toBe(true); }); + + it('should have proper invalid state if unchecked', () => { + testComponent.isRequired = true; + fixture.detectChanges(); + + const slideToggleEl = fixture.nativeElement.querySelector('.mat-slide-toggle'); + + expect(slideToggleEl.classList).toContain('ng-invalid'); + expect(slideToggleEl.classList).not.toContain('ng-valid'); + + // The required slide-toggle will be checked and the form control + // should become valid. + inputElement.click(); + fixture.detectChanges(); + + expect(slideToggleEl.classList).not.toContain('ng-invalid'); + expect(slideToggleEl.classList).toContain('ng-valid'); + + // The required slide-toggle will be unchecked and the form control + // should become invalid. + inputElement.click(); + fixture.detectChanges(); + + expect(slideToggleEl.classList).toContain('ng-invalid'); + expect(slideToggleEl.classList).not.toContain('ng-valid'); + }); }); describe('with model and change event', () => { diff --git a/tools/public_api_guard/material/slide-toggle.d.ts b/tools/public_api_guard/material/slide-toggle.d.ts index 60b1fba3f057..487f72a1eed5 100644 --- a/tools/public_api_guard/material/slide-toggle.d.ts +++ b/tools/public_api_guard/material/slide-toggle.d.ts @@ -1,5 +1,10 @@ +export declare class _MatSlideToggleRequiredValidatorModule { +} + export declare const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS: InjectionToken; +export declare const MAT_SLIDE_TOGGLE_REQUIRED_VALIDATOR: Provider; + export declare const MAT_SLIDE_TOGGLE_VALUE_ACCESSOR: any; export declare class MatSlideToggle extends _MatSlideToggleMixinBase implements OnDestroy, AfterContentInit, ControlValueAccessor, CanDisable, CanColor, HasTabIndex, CanDisableRipple { @@ -51,3 +56,6 @@ export interface MatSlideToggleDefaultOptions { export declare class MatSlideToggleModule { } + +export declare class MatSlideToggleRequiredValidator extends CheckboxRequiredValidator { +}