diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index 0f215ffbbb..1613e356b4 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -55,16 +55,41 @@ function _find(control: AbstractControl, path: Array| string, del }, control); } -function coerceToValidator(validator?: ValidatorFn | ValidatorFn[] | null): ValidatorFn|null { +function coerceToValidator( + validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): ValidatorFn| + null { + const validator = + (isOptionsObj(validatorOrOpts) ? (validatorOrOpts as AbstractControlOptions).validators : + validatorOrOpts) as ValidatorFn | + ValidatorFn[] | null; + return Array.isArray(validator) ? composeValidators(validator) : validator || null; } -function coerceToAsyncValidator(asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): - AsyncValidatorFn|null { - return Array.isArray(asyncValidator) ? composeAsyncValidators(asyncValidator) : - asyncValidator || null; +function coerceToAsyncValidator( + asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, validatorOrOpts?: ValidatorFn | + ValidatorFn[] | AbstractControlOptions | null): AsyncValidatorFn|null { + const origAsyncValidator = + (isOptionsObj(validatorOrOpts) ? (validatorOrOpts as AbstractControlOptions).asyncValidators : + asyncValidator) as AsyncValidatorFn | + AsyncValidatorFn | null; + + return Array.isArray(origAsyncValidator) ? composeAsyncValidators(origAsyncValidator) : + origAsyncValidator || null; } +export interface AbstractControlOptions { + validators?: ValidatorFn|ValidatorFn[]|null; + asyncValidators?: AsyncValidatorFn|AsyncValidatorFn[]|null; +} + +function isOptionsObj( + validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): boolean { + return validatorOrOpts != null && !Array.isArray(validatorOrOpts) && + typeof validatorOrOpts === 'object'; +} + + /** * @whatItDoes This is the base class for {@link FormControl}, {@link FormGroup}, and * {@link FormArray}. @@ -612,9 +637,12 @@ export abstract class AbstractControl { * console.log(ctrl.status); // 'DISABLED' * ``` * - * To include a sync validator (or an array of sync validators) with the control, - * pass it in as the second argument. Async validators are also supported, but - * have to be passed in separately as the third arg. + * The second {@link FormControl} argument can accept one of three things: + * * a sync validator function + * * an array of sync validator functions + * * an options object containing validator and/or async validator functions + * + * Example of a single sync validator function: * * ```ts * const ctrl = new FormControl('', Validators.required); @@ -622,6 +650,15 @@ export abstract class AbstractControl { * console.log(ctrl.status); // 'INVALID' * ``` * + * Example using options object: + * + * ```ts + * const ctrl = new FormControl('', { + * validators: Validators.required, + * asyncValidators: myAsyncValidator + * }); + * ``` + * * See its superclass, {@link AbstractControl}, for more properties and methods. * * * **npm package**: `@angular/forms` @@ -633,9 +670,12 @@ export class FormControl extends AbstractControl { _onChange: Function[] = []; constructor( - formState: any = null, validator?: ValidatorFn|ValidatorFn[]|null, + formState: any = null, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { - super(coerceToValidator(validator), coerceToAsyncValidator(asyncValidator)); + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._applyFormState(formState); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); this._initObservables(); @@ -823,15 +863,28 @@ export class FormControl extends AbstractControl { * } * ``` * + * Like {@link FormControl} instances, you can alternatively choose to pass in + * validators and async validators as part of an options object. + * + * ``` + * const form = new FormGroup({ + * password: new FormControl('') + * passwordConfirm: new FormControl('') + * }, {validators: passwordMatchValidator, asyncValidators: otherValidator}); + * ``` + * * * **npm package**: `@angular/forms` * * @stable */ export class FormGroup extends AbstractControl { constructor( - public controls: {[key: string]: AbstractControl}, validator?: ValidatorFn|null, - asyncValidator?: AsyncValidatorFn|null) { - super(validator || null, asyncValidator || null); + public controls: {[key: string]: AbstractControl}, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._initObservables(); this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); @@ -1114,9 +1167,19 @@ export class FormGroup extends AbstractControl { * console.log(arr.status); // 'VALID' * ``` * - * You can also include array-level validators as the second arg, or array-level async - * validators as the third arg. These come in handy when you want to perform validation - * that considers the value of more than one child control. + * You can also include array-level validators and async validators. These come in handy + * when you want to perform validation that considers the value of more than one child + * control. + * + * The two types of validators can be passed in separately as the second and third arg + * respectively, or together as part of an options object. + * + * ``` + * const arr = new FormArray([ + * new FormControl('Nancy'), + * new FormControl('Drew') + * ], {validators: myValidator, asyncValidators: myAsyncValidator}); + * ``` * * ### Adding or removing controls * @@ -1132,9 +1195,12 @@ export class FormGroup extends AbstractControl { */ export class FormArray extends AbstractControl { constructor( - public controls: AbstractControl[], validator?: ValidatorFn|null, - asyncValidator?: AsyncValidatorFn|null) { - super(validator || null, asyncValidator || null); + public controls: AbstractControl[], + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { + super( + coerceToValidator(validatorOrOpts), + coerceToAsyncValidator(asyncValidator, validatorOrOpts)); this._initObservables(); this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); diff --git a/packages/forms/test/form_array_spec.ts b/packages/forms/test/form_array_spec.ts index 2b94b80ec7..11b0605ae3 100644 --- a/packages/forms/test/form_array_spec.ts +++ b/packages/forms/test/form_array_spec.ts @@ -8,8 +8,8 @@ import {fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; -import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms'; - +import {AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors} from '@angular/forms'; +import {of } from 'rxjs/observable/of'; import {Validators} from '../src/validators'; export function main() { @@ -725,18 +725,113 @@ export function main() { }); }); + describe('validator', () => { + function simpleValidator(c: AbstractControl): ValidationErrors|null { + return c.get([0]) !.value === 'correct' ? null : {'broken': true}; + } + + function arrayRequiredValidator(c: AbstractControl): ValidationErrors|null { + return Validators.required(c.get([0]) as AbstractControl); + } + + it('should set a single validator', () => { + const a = new FormArray([new FormControl()], simpleValidator); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set a single validator from options obj', () => { + const a = new FormArray([new FormControl()], {validators: simpleValidator}); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set multiple validators from an array', () => { + const a = new FormArray([new FormControl()], [simpleValidator, arrayRequiredValidator]); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'required': true, 'broken': true}); + + a.setValue(['c']); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + + it('should set multiple validators from options obj', () => { + const a = new FormArray( + [new FormControl()], {validators: [simpleValidator, arrayRequiredValidator]}); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'required': true, 'broken': true}); + + a.setValue(['c']); + expect(a.valid).toBe(false); + expect(a.errors).toEqual({'broken': true}); + + a.setValue(['correct']); + expect(a.valid).toBe(true); + }); + }); + describe('asyncValidator', () => { + function otherObservableValidator() { return of ({'other': true}); } + it('should run the async validator', fakeAsync(() => { const c = new FormControl('value'); const g = new FormArray([c], null !, asyncValidator('expected')); expect(g.pending).toEqual(true); - tick(1); + tick(); expect(g.errors).toEqual({'async': true}); expect(g.pending).toEqual(false); })); + + it('should set a single async validator from options obj', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], {asyncValidators: asyncValidator('expected')}); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from an array', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], null !, + [asyncValidator('expected'), otherObservableValidator]); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from options obj', fakeAsync(() => { + const g = new FormArray( + [new FormControl('value')], + {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); + + expect(g.pending).toEqual(true); + + tick(); + + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); }); describe('disable() & enable()', () => { diff --git a/packages/forms/test/form_control_spec.ts b/packages/forms/test/form_control_spec.ts index 4fa4d9f3a2..5199aaac5b 100644 --- a/packages/forms/test/form_control_spec.ts +++ b/packages/forms/test/form_control_spec.ts @@ -97,6 +97,39 @@ export function main() { expect(c.valid).toEqual(true); }); + it('should support single validator from options obj', () => { + const c = new FormControl(null, {validators: Validators.required}); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({required: true}); + + c.setValue('value'); + expect(c.valid).toEqual(true); + }); + + it('should support multiple validators from options obj', () => { + const c = + new FormControl(null, {validators: [Validators.required, Validators.minLength(3)]}); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({required: true}); + + c.setValue('aa'); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({minlength: {requiredLength: 3, actualLength: 2}}); + + c.setValue('aaa'); + expect(c.valid).toEqual(true); + }); + + it('should support a null validators value', () => { + const c = new FormControl(null, {validators: null}); + expect(c.valid).toEqual(true); + }); + + it('should support an empty options obj', () => { + const c = new FormControl(null, {}); + expect(c.valid).toEqual(true); + }); + it('should return errors', () => { const c = new FormControl(null, Validators.required); expect(c.errors).toEqual({'required': true}); @@ -222,6 +255,40 @@ export function main() { expect(c.errors).toEqual({'async': true, 'other': true}); })); + + it('should support a single async validator from options obj', fakeAsync(() => { + const c = new FormControl('value', {asyncValidators: asyncValidator('expected')}); + expect(c.pending).toEqual(true); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + + it('should support multiple async validators from options obj', fakeAsync(() => { + const c = new FormControl( + 'value', {asyncValidators: [asyncValidator('expected'), otherAsyncValidator]}); + expect(c.pending).toEqual(true); + tick(); + + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true, 'other': true}); + })); + + it('should support a mix of validators from options obj', fakeAsync(() => { + const c = new FormControl( + '', {validators: Validators.required, asyncValidators: asyncValidator('expected')}); + tick(); + expect(c.errors).toEqual({required: true}); + + c.setValue('value'); + expect(c.pending).toBe(true); + + tick(); + expect(c.valid).toEqual(false); + expect(c.errors).toEqual({'async': true}); + })); + it('should add single async validator', fakeAsync(() => { const c = new FormControl('value', null !); diff --git a/packages/forms/test/form_group_spec.ts b/packages/forms/test/form_group_spec.ts index aa62b78c9c..b9a1fbbf03 100644 --- a/packages/forms/test/form_group_spec.ts +++ b/packages/forms/test/form_group_spec.ts @@ -9,10 +9,15 @@ import {EventEmitter} from '@angular/core'; import {async, fakeAsync, tick} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, describe, inject, it} from '@angular/core/testing/src/testing_internal'; -import {AbstractControl, FormArray, FormControl, FormGroup, Validators} from '@angular/forms'; +import {AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators} from '@angular/forms'; +import {of } from 'rxjs/observable/of'; export function main() { + function simpleValidator(c: AbstractControl): ValidationErrors|null { + return c.get('one') !.value === 'correct' ? null : {'broken': true}; + } + function asyncValidator(expected: string, timeouts = {}) { return (c: AbstractControl) => { let resolve: (result: any) => void = undefined !; @@ -36,6 +41,8 @@ export function main() { return e; } + function otherObservableValidator() { return of ({'other': true}) } + describe('FormGroup', () => { describe('value', () => { it('should be the reduced value of the child controls', () => { @@ -104,26 +111,6 @@ export function main() { }); }); - describe('errors', () => { - it('should run the validator when the value changes', () => { - const simpleValidator = (c: FormGroup) => - c.controls['one'].value != 'correct' ? {'broken': true} : null; - - const c = new FormControl(null); - const g = new FormGroup({'one': c}, simpleValidator); - - c.setValue('correct'); - - expect(g.valid).toEqual(true); - expect(g.errors).toEqual(null); - - c.setValue('incorrect'); - - expect(g.valid).toEqual(false); - expect(g.errors).toEqual({'broken': true}); - }); - }); - describe('dirty', () => { let c: FormControl, g: FormGroup; @@ -687,6 +674,66 @@ export function main() { }); }); + describe('validator', () => { + + function containsValidator(c: AbstractControl): ValidationErrors|null { + return c.get('one') !.value && c.get('one') !.value.indexOf('c') !== -1 ? null : + {'missing': true}; + } + + it('should run a single validator when the value changes', () => { + const c = new FormControl(null); + const g = new FormGroup({'one': c}, simpleValidator); + + c.setValue('correct'); + + expect(g.valid).toEqual(true); + expect(g.errors).toEqual(null); + + c.setValue('incorrect'); + + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({'broken': true}); + }); + + it('should support multiple validators from array', () => { + const g = new FormGroup({one: new FormControl()}, [simpleValidator, containsValidator]); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({missing: true, broken: true}); + + g.setValue({one: 'c'}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + it('should set single validator from options obj', () => { + const g = new FormGroup({one: new FormControl()}, {validators: simpleValidator}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + it('should set multiple validators from options obj', () => { + const g = new FormGroup( + {one: new FormControl()}, {validators: [simpleValidator, containsValidator]}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({missing: true, broken: true}); + + g.setValue({one: 'c'}); + expect(g.valid).toEqual(false); + expect(g.errors).toEqual({broken: true}); + + g.setValue({one: 'correct'}); + expect(g.valid).toEqual(true); + }); + + }); + describe('asyncValidator', () => { it('should run the async validator', fakeAsync(() => { const c = new FormControl('value'); @@ -700,6 +747,38 @@ export function main() { expect(g.pending).toEqual(false); })); + it('should set multiple async validators from array', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, null !, + [asyncValidator('expected'), otherObservableValidator]); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + + it('should set single async validator from options obj', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, {asyncValidators: asyncValidator('expected')}); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true}); + expect(g.pending).toEqual(false); + })); + + it('should set multiple async validators from options obj', fakeAsync(() => { + const g = new FormGroup( + {'one': new FormControl('value')}, + {asyncValidators: [asyncValidator('expected'), otherObservableValidator]}); + expect(g.pending).toEqual(true); + + tick(); + expect(g.errors).toEqual({'async': true, 'other': true}); + expect(g.pending).toEqual(false); + })); + it('should set the parent group\'s status to pending', fakeAsync(() => { const c = new FormControl('value', null !, asyncValidator('expected')); const g = new FormGroup({'one': c}); diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index fcd570bc0a..85ebcb1b0d 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -175,7 +175,7 @@ export interface Form { export declare class FormArray extends AbstractControl { controls: AbstractControl[]; readonly length: number; - constructor(controls: AbstractControl[], validator?: ValidatorFn | null, asyncValidator?: AsyncValidatorFn | null); + constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); at(index: number): AbstractControl; getRawValue(): any[]; insert(index: number, control: AbstractControl): void; @@ -222,7 +222,7 @@ export declare class FormBuilder { /** @stable */ export declare class FormControl extends AbstractControl { - constructor(formState?: any, validator?: ValidatorFn | ValidatorFn[] | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); + constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); patchValue(value: any, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -283,7 +283,7 @@ export declare class FormGroup extends AbstractControl { }; constructor(controls: { [key: string]: AbstractControl; - }, validator?: ValidatorFn | null, asyncValidator?: AsyncValidatorFn | null); + }, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null); addControl(name: string, control: AbstractControl): void; contains(controlName: string): boolean; getRawValue(): any;