From 2bf1bbc071f8280e56751184be7444d3e573a148 Mon Sep 17 00:00:00 2001 From: Dzmitry Shylovich Date: Sat, 10 Dec 2016 13:44:04 +0300 Subject: [PATCH] fix(forms): introduce checkbox required validator Closes #11459 Closes #13364 --- modules/@angular/forms/src/directives.ts | 24 +++++-- .../forms/src/directives/validators.ts | 37 +++++++++- modules/@angular/forms/src/forms.ts | 2 +- modules/@angular/forms/src/validators.ts | 7 ++ .../forms/test/reactive_integration_spec.ts | 29 +++++++- .../forms/test/template_integration_spec.ts | 71 +++++++++++++++++-- .../@angular/forms/test/validators_spec.ts | 8 +++ tools/public_api_guard/forms/index.d.ts | 10 +++ 8 files changed, 172 insertions(+), 16 deletions(-) diff --git a/modules/@angular/forms/src/directives.ts b/modules/@angular/forms/src/directives.ts index f6e3719d4a..723c650e68 100644 --- a/modules/@angular/forms/src/directives.ts +++ b/modules/@angular/forms/src/directives.ts @@ -24,7 +24,7 @@ import {FormGroupDirective} from './directives/reactive_directives/form_group_di import {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name'; import {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor'; import {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor'; -import {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators'; +import {CheckboxRequiredValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators'; export {CheckboxControlValueAccessor} from './directives/checkbox_value_accessor'; export {ControlValueAccessor} from './directives/control_value_accessor'; @@ -43,13 +43,25 @@ export {FormGroupDirective} from './directives/reactive_directives/form_group_di export {FormArrayName, FormGroupName} from './directives/reactive_directives/form_group_name'; export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor'; export {NgSelectMultipleOption, SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor'; -export {MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator} from './directives/validators'; export const SHARED_FORM_DIRECTIVES: Type[] = [ - NgSelectOption, NgSelectMultipleOption, DefaultValueAccessor, NumberValueAccessor, - RangeValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, - SelectMultipleControlValueAccessor, RadioControlValueAccessor, NgControlStatus, NgNovalidate, - NgControlStatusGroup, RequiredValidator, MinLengthValidator, MaxLengthValidator, PatternValidator + NgNovalidate, + NgSelectOption, + NgSelectMultipleOption, + DefaultValueAccessor, + NumberValueAccessor, + RangeValueAccessor, + CheckboxControlValueAccessor, + SelectControlValueAccessor, + SelectMultipleControlValueAccessor, + RadioControlValueAccessor, + NgControlStatus, + NgControlStatusGroup, + RequiredValidator, + MinLengthValidator, + MaxLengthValidator, + PatternValidator, + CheckboxRequiredValidator, ]; export const TEMPLATE_DRIVEN_DIRECTIVES: Type[] = [NgModel, NgModelGroup, NgForm]; diff --git a/modules/@angular/forms/src/directives/validators.ts b/modules/@angular/forms/src/directives/validators.ts index dc6996481e..fd8c5f3baa 100644 --- a/modules/@angular/forms/src/directives/validators.ts +++ b/modules/@angular/forms/src/directives/validators.ts @@ -5,7 +5,7 @@ * 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, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core'; +import {Directive, Input, OnChanges, Provider, SimpleChanges, forwardRef} from '@angular/core'; import {AbstractControl} from '../model'; import {NG_VALIDATORS, Validators} from '../validators'; @@ -33,12 +33,18 @@ export interface Validator { registerOnValidatorChange?(fn: () => void): void; } -export const REQUIRED_VALIDATOR: any = { +export const REQUIRED_VALIDATOR: Provider = { provide: NG_VALIDATORS, useExisting: forwardRef(() => RequiredValidator), multi: true }; +export const CHECKBOX_REQUIRED_VALIDATOR: Provider = { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CheckboxRequiredValidator), + multi: true +}; + /** * A Directive that adds the `required` validator to any controls marked with the * `required` attribute, via the {@link NG_VALIDATORS} binding. @@ -52,7 +58,8 @@ export const REQUIRED_VALIDATOR: any = { * @stable */ @Directive({ - selector: '[required][formControlName],[required][formControl],[required][ngModel]', + selector: + ':not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]', providers: [REQUIRED_VALIDATOR], host: {'[attr.required]': 'required ? "" : null'} }) @@ -75,6 +82,30 @@ export class RequiredValidator implements Validator { registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } } +/** + * A Directive that adds the `required` validator to checkbox controls marked with the + * `required` attribute, via the {@link NG_VALIDATORS} binding. + * + * ### Example + * + * ``` + * + * ``` + * + * @experimental + */ +@Directive({ + selector: + 'input[type=checkbox][required][formControlName],input[type=checkbox][required][formControl],input[type=checkbox][required][ngModel]', + providers: [CHECKBOX_REQUIRED_VALIDATOR], + host: {'[attr.required]': 'required ? "" : null'} +}) +export class CheckboxRequiredValidator extends RequiredValidator { + validate(c: AbstractControl): {[key: string]: any} { + return this.required ? Validators.requiredTrue(c) : null; + } +} + /** * @stable */ diff --git a/modules/@angular/forms/src/forms.ts b/modules/@angular/forms/src/forms.ts index 16fa548f88..b776d120ce 100644 --- a/modules/@angular/forms/src/forms.ts +++ b/modules/@angular/forms/src/forms.ts @@ -38,7 +38,7 @@ export {FormArrayName} from './directives/reactive_directives/form_group_name'; export {FormGroupName} from './directives/reactive_directives/form_group_name'; export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor'; export {SelectMultipleControlValueAccessor} from './directives/select_multiple_control_value_accessor'; -export {AsyncValidatorFn, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, Validator, ValidatorFn} from './directives/validators'; +export {AsyncValidatorFn, CheckboxRequiredValidator, MaxLengthValidator, MinLengthValidator, PatternValidator, RequiredValidator, Validator, ValidatorFn} from './directives/validators'; export {FormBuilder} from './form_builder'; export {AbstractControl, FormArray, FormControl, FormGroup} from './model'; export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators'; diff --git a/modules/@angular/forms/src/validators.ts b/modules/@angular/forms/src/validators.ts index 460bc8fba5..ba57904789 100644 --- a/modules/@angular/forms/src/validators.ts +++ b/modules/@angular/forms/src/validators.ts @@ -64,6 +64,13 @@ export class Validators { return isEmptyInputValue(control.value) ? {'required': true} : null; } + /** + * Validator that requires control value to be true. + */ + static requiredTrue(control: AbstractControl): {[key: string]: boolean} { + return control.value === true ? null : {'required': true}; + } + /** * Validator that requires controls to have a value of a minimum length. */ diff --git a/modules/@angular/forms/test/reactive_integration_spec.ts b/modules/@angular/forms/test/reactive_integration_spec.ts index 54b1ccab8c..a40217f634 100644 --- a/modules/@angular/forms/test/reactive_integration_spec.ts +++ b/modules/@angular/forms/test/reactive_integration_spec.ts @@ -39,7 +39,8 @@ export function main() { ValidationBindingsForm, UniqLoginValidator, UniqLoginWrapper, - NestedFormGroupComp + NestedFormGroupComp, + FormControlCheckboxRequiredValidator, ] }); }); @@ -1320,6 +1321,24 @@ export function main() { }); describe('validations', () => { + it('required validator should validate checkbox', () => { + const fixture = TestBed.createComponent(FormControlCheckboxRequiredValidator); + const control = new FormControl(false, Validators.requiredTrue); + fixture.componentInstance.control = control; + fixture.detectChanges(); + + const checkbox = fixture.debugElement.query(By.css('input')); + expect(checkbox.nativeElement.checked).toBe(false); + expect(control.hasError('required')).toEqual(true); + + checkbox.nativeElement.checked = true; + dispatchEvent(checkbox.nativeElement, 'change'); + fixture.detectChanges(); + + expect(checkbox.nativeElement.checked).toBe(true); + expect(control.hasError('required')).toEqual(false); + }); + it('should use sync validators defined in html', () => { const fixture = TestBed.createComponent(LoginIsEmptyWrapper); const form = new FormGroup({ @@ -2061,6 +2080,14 @@ class ValidationBindingsForm { pattern: string; } +@Component({ + selector: 'form-control-checkbox-validator', + template: `` +}) +class FormControlCheckboxRequiredValidator { + control: FormControl; +} + @Component({ selector: 'uniq-login-wrapper', template: ` diff --git a/modules/@angular/forms/test/template_integration_spec.ts b/modules/@angular/forms/test/template_integration_spec.ts index 0a6565131a..ebfa75848f 100644 --- a/modules/@angular/forms/test/template_integration_spec.ts +++ b/modules/@angular/forms/test/template_integration_spec.ts @@ -19,11 +19,26 @@ export function main() { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ - StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm, - NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, - NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper, - NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator, - NgModelAsyncValidation, NgModelSelectMultipleForm, NgModelSelectWithNullForm + StandaloneNgModel, + NgModelForm, + NgModelGroupForm, + NgModelValidBinding, + NgModelNgIfForm, + NgModelRadioForm, + NgModelRangeForm, + NgModelSelectForm, + NgNoFormComp, + InvalidNgModelNoName, + NgModelOptionsStandalone, + NgModelCustomComp, + NgModelCustomWrapper, + NgModelValidationBindings, + NgModelMultipleValidators, + NgAsyncValidator, + NgModelAsyncValidation, + NgModelSelectMultipleForm, + NgModelSelectWithNullForm, + NgModelCheckboxRequiredValidator, ], imports: [FormsModule] }); @@ -825,6 +840,42 @@ export function main() { describe('validation directives', () => { + it('required validator should validate checkbox', fakeAsync(() => { + const fixture = TestBed.createComponent(NgModelCheckboxRequiredValidator); + fixture.detectChanges(); + tick(); + + const control = + fixture.debugElement.children[0].injector.get(NgForm).control.get('checkbox'); + + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.checked).toBe(false); + expect(control.hasError('required')).toBe(false); + + fixture.componentInstance.required = true; + fixture.detectChanges(); + tick(); + + expect(input.nativeElement.checked).toBe(false); + expect(control.hasError('required')).toBe(true); + + input.nativeElement.checked = true; + dispatchEvent(input.nativeElement, 'change'); + fixture.detectChanges(); + tick(); + + expect(input.nativeElement.checked).toBe(true); + expect(control.hasError('required')).toBe(false); + + input.nativeElement.checked = false; + dispatchEvent(input.nativeElement, 'change'); + fixture.detectChanges(); + tick(); + + expect(input.nativeElement.checked).toBe(false); + expect(control.hasError('required')).toBe(true); + })); + it('should support dir validators using bindings', fakeAsync(() => { const fixture = TestBed.createComponent(NgModelValidationBindings); fixture.componentInstance.required = true; @@ -1291,6 +1342,16 @@ class NgModelMultipleValidators { pattern: string|RegExp; } +@Component({ + selector: 'ng-model-checkbox-validator', + template: + `
` +}) +class NgModelCheckboxRequiredValidator { + accepted: boolean = false; + required: boolean = false; +} + @Directive({ selector: '[ng-async-validator]', providers: [ diff --git a/modules/@angular/forms/test/validators_spec.ts b/modules/@angular/forms/test/validators_spec.ts index 0cf4abbb71..af44cf3efc 100644 --- a/modules/@angular/forms/test/validators_spec.ts +++ b/modules/@angular/forms/test/validators_spec.ts @@ -50,6 +50,14 @@ export function main() { () => { expect(Validators.required(new FormControl(0))).toBeNull(); }); }); + describe('requiredTrue', () => { + it('should error on false', + () => expect(Validators.requiredTrue(new FormControl(false))).toEqual({'required': true})); + + it('should not error on true', + () => expect(Validators.requiredTrue(new FormControl(true))).toBeNull()); + }); + describe('minLength', () => { it('should not error on an empty string', () => { expect(Validators.minLength(2)(new FormControl(''))).toBeNull(); }); diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index f76e1b57f8..a257a499d5 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -117,6 +117,13 @@ export declare class CheckboxControlValueAccessor implements ControlValueAccesso writeValue(value: any): void; } +/** @experimental */ +export declare class CheckboxRequiredValidator extends RequiredValidator { + validate(c: AbstractControl): { + [key: string]: any; + }; +} + /** @stable */ export declare class ControlContainer extends AbstractControlDirective { formDirective: Form; @@ -531,6 +538,9 @@ export declare class Validators { static required(control: AbstractControl): { [key: string]: boolean; }; + static requiredTrue(control: AbstractControl): { + [key: string]: boolean; + }; } /** @stable */