Skip to content

Commit

Permalink
fix(slide-toggle): invalid required validator in template-driven forms (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
devversion authored and andrewseguin committed Jul 29, 2019
1 parent ac09e37 commit dc0c271
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/material-experimental/mdc-slide-toggle/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 12 additions & 2 deletions src/material-experimental/mdc-slide-toggle/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions src/material/slide-toggle/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
export * from './slide-toggle-module';
export * from './slide-toggle';
export * from './slide-toggle-config';
export * from './slide-toggle-required-validator';
22 changes: 20 additions & 2 deletions src/material/slide-toggle/slide-toggle-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
38 changes: 38 additions & 0 deletions src/material/slide-toggle/slide-toggle-required-validator.ts
Original file line number Diff line number Diff line change
@@ -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 {}
26 changes: 26 additions & 0 deletions src/material/slide-toggle/slide-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
8 changes: 8 additions & 0 deletions tools/public_api_guard/material/slide-toggle.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export declare class _MatSlideToggleRequiredValidatorModule {
}

export declare const MAT_SLIDE_TOGGLE_DEFAULT_OPTIONS: InjectionToken<MatSlideToggleDefaultOptions>;

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 {
Expand Down Expand Up @@ -51,3 +56,6 @@ export interface MatSlideToggleDefaultOptions {

export declare class MatSlideToggleModule {
}

export declare class MatSlideToggleRequiredValidator extends CheckboxRequiredValidator {
}

0 comments on commit dc0c271

Please sign in to comment.