Skip to content
Permalink
Browse files

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.
  • Loading branch information...
kara authored and alxhub committed Jul 25, 2017
1 parent d71ae27 commit ebef5e697a19d38d761f15a123739481ddc74622
@@ -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 {

This comment has been minimized.

Copy link
@chaosmonster
return validatorOrOpts != null && !Array.isArray(validatorOrOpts) &&
typeof validatorOrOpts === 'object';
}


/**
* @whatItDoes This is the base class for {@link FormControl}, {@link FormGroup}, and
* {@link FormArray}.
@@ -612,16 +637,28 @@ 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);
* console.log(ctrl.value); // ''
* 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});
@@ -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()', () => {
@@ -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 !);

Oops, something went wrong.

0 comments on commit ebef5e6

Please sign in to comment.
You can’t perform that action at this time.