feat(forms): add options arg to abstract controls

FormControls, FormGroups, and FormArrays now optionally accept an options
object as their second argument. Validators and async validators can be
passed in as part of this options object (though they can still be passed
in as the second and third arg as before).

```ts
const c = new FormControl(, {
   validators: [Validators.required],
   asyncValidators: [myAsyncValidator]
});
```

This commit also adds support for passing arrays of validators and async
validators to FormGroups and FormArrays, which formerly only accepted
individual functions.

```ts
const g = new FormGroup({
   one: new FormControl()
}, [myPasswordValidator, myOtherValidator]);
```

This change paves the way for adding more options to AbstractControls,
such as more fine-grained control of validation timing.
This commit is contained in:
Kara Erickson 2017-07-25 15:01:04 -07:00 committed by Alex Rickabaugh
parent d71ae278ef
commit ebef5e697a
5 changed files with 353 additions and 46 deletions

View File

@ -55,16 +55,41 @@ function _find(control: AbstractControl, path: Array<string|number>| 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});

View File

@ -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()', () => {

View File

@ -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 !);

View File

@ -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});

View File

@ -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;