angular-cn/modules/@angular/forms/src/model.ts

1386 lines
42 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google Inc. 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 {ɵisObservable as isObservable, ɵisPromise as isPromise} from '@angular/core';
import {fromPromise} from 'rxjs/observable/fromPromise';
import {composeAsyncValidators, composeValidators} from './directives/shared';
import {AsyncValidatorFn, ValidatorFn} from './directives/validators';
import {EventEmitter, Observable} from './facade/async';
2016-06-08 18:36:24 -04:00
/**
* Indicates that a FormControl is valid, i.e. that no errors exist in the input value.
2016-06-08 18:36:24 -04:00
*/
export const VALID = 'VALID';
2016-06-08 18:36:24 -04:00
/**
* Indicates that a FormControl is invalid, i.e. that an error exists in the input value.
2016-06-08 18:36:24 -04:00
*/
export const INVALID = 'INVALID';
2016-06-08 18:36:24 -04:00
/**
* Indicates that a FormControl is pending, i.e. that async validation is occurring and
2016-06-08 18:36:24 -04:00
* errors are not yet available for the input value.
*/
export const PENDING = 'PENDING';
2016-06-08 18:36:24 -04:00
/**
* Indicates that a FormControl is disabled, i.e. that the control is exempt from ancestor
* calculations of validity or value.
*/
export const DISABLED = 'DISABLED';
function _find(control: AbstractControl, path: Array<string|number>| string, delimiter: string) {
2016-10-21 18:14:44 -04:00
if (path == null) return null;
2016-06-08 18:36:24 -04:00
if (!(path instanceof Array)) {
path = (<string>path).split(delimiter);
2016-06-08 18:36:24 -04:00
}
if (path instanceof Array && (path.length === 0)) return null;
2016-06-08 18:36:24 -04:00
return (<Array<string|number>>path).reduce((v, name) => {
if (v instanceof FormGroup) {
return v.controls[name] || null;
}
if (v instanceof FormArray) {
return v.at(<number>name) || null;
}
return null;
}, control);
2016-06-08 18:36:24 -04:00
}
function toObservable(r: any): Observable<any> {
return isPromise(r) ? fromPromise(r) : r;
2016-06-08 18:36:24 -04:00
}
function coerceToValidator(validator: ValidatorFn | ValidatorFn[]): ValidatorFn {
return Array.isArray(validator) ? composeValidators(validator) : validator;
}
function coerceToAsyncValidator(asyncValidator: AsyncValidatorFn | AsyncValidatorFn[]):
AsyncValidatorFn {
return Array.isArray(asyncValidator) ? composeAsyncValidators(asyncValidator) : asyncValidator;
}
2016-06-08 18:36:24 -04:00
/**
* @whatItDoes This is the base class for {@link FormControl}, {@link FormGroup}, and
* {@link FormArray}.
*
* It provides some of the shared behavior that all controls and groups of controls have, like
* running validators, calculating status, and resetting state. It also defines the properties
* that are shared between all sub-classes, like `value`, `valid`, and `dirty`. It shouldn't be
* instantiated directly.
*
* @stable
2016-06-08 18:36:24 -04:00
*/
export abstract class AbstractControl {
/** @internal */
_value: any;
/** @internal */
_onCollectionChange = () => {};
2016-06-08 18:36:24 -04:00
private _valueChanges: EventEmitter<any>;
private _statusChanges: EventEmitter<any>;
private _status: string;
private _errors: {[key: string]: any};
private _pristine: boolean = true;
private _touched: boolean = false;
private _parent: FormGroup|FormArray;
2016-06-08 18:36:24 -04:00
private _asyncValidationSubscription: any;
constructor(public validator: ValidatorFn, public asyncValidator: AsyncValidatorFn) {}
/**
* The value of the control.
*/
2016-06-08 18:36:24 -04:00
get value(): any { return this._value; }
/**
* The parent control.
*/
get parent(): FormGroup|FormArray { return this._parent; }
/**
* The validation status of the control. There are four possible
* validation statuses:
*
* * **VALID**: control has passed all validation checks
* * **INVALID**: control has failed at least one validation check
* * **PENDING**: control is in the midst of conducting a validation check
* * **DISABLED**: control is exempt from validation checks
*
* These statuses are mutually exclusive, so a control cannot be
* both valid AND invalid or invalid AND disabled.
*/
2016-06-08 18:36:24 -04:00
get status(): string { return this._status; }
/**
* A control is `valid` when its `status === VALID`.
*
* In order to have this status, the control must have passed all its
* validation checks.
*/
2016-06-08 18:36:24 -04:00
get valid(): boolean { return this._status === VALID; }
/**
* A control is `invalid` when its `status === INVALID`.
*
* In order to have this status, the control must have failed
* at least one of its validation checks.
*/
get invalid(): boolean { return this._status === INVALID; }
2016-06-08 18:36:24 -04:00
/**
* A control is `pending` when its `status === PENDING`.
*
* In order to have this status, the control must be in the
* middle of conducting a validation check.
*/
get pending(): boolean { return this._status == PENDING; }
/**
* A control is `disabled` when its `status === DISABLED`.
*
* Disabled controls are exempt from validation checks and
* are not included in the aggregate value of their ancestor
* controls.
*/
get disabled(): boolean { return this._status === DISABLED; }
/**
* A control is `enabled` as long as its `status !== DISABLED`.
*
* In other words, it has a status of `VALID`, `INVALID`, or
* `PENDING`.
*/
get enabled(): boolean { return this._status !== DISABLED; }
/**
* Returns any errors generated by failing validation. If there
* are no errors, it will return null.
2016-06-08 18:36:24 -04:00
*/
get errors(): {[key: string]: any} { return this._errors; }
/**
* A control is `pristine` if the user has not yet changed
* the value in the UI.
*
* Note that programmatic changes to a control's value will
* *not* mark it dirty.
*/
2016-06-08 18:36:24 -04:00
get pristine(): boolean { return this._pristine; }
/**
* A control is `dirty` if the user has changed the value
* in the UI.
*
* Note that programmatic changes to a control's value will
* *not* mark it dirty.
*/
2016-06-08 18:36:24 -04:00
get dirty(): boolean { return !this.pristine; }
/**
* A control is marked `touched` once the user has triggered
* a `blur` event on it.
*/
2016-06-08 18:36:24 -04:00
get touched(): boolean { return this._touched; }
/**
* A control is `untouched` if the user has not yet triggered
* a `blur` event on it.
*/
2016-06-08 18:36:24 -04:00
get untouched(): boolean { return !this._touched; }
/**
* Emits an event every time the value of the control changes, in
* the UI or programmatically.
*/
2016-06-08 18:36:24 -04:00
get valueChanges(): Observable<any> { return this._valueChanges; }
/**
* Emits an event every time the validation status of the control
* is re-calculated.
*/
2016-06-08 18:36:24 -04:00
get statusChanges(): Observable<any> { return this._statusChanges; }
/**
* Sets the synchronous validators that are active on this control. Calling
* this will overwrite any existing sync validators.
*/
setValidators(newValidator: ValidatorFn|ValidatorFn[]): void {
this.validator = coerceToValidator(newValidator);
}
/**
* Sets the async validators that are active on this control. Calling this
* will overwrite any existing async validators.
*/
setAsyncValidators(newValidator: AsyncValidatorFn|AsyncValidatorFn[]): void {
this.asyncValidator = coerceToAsyncValidator(newValidator);
}
/**
* Empties out the sync validator list.
*/
clearValidators(): void { this.validator = null; }
/**
* Empties out the async validator list.
*/
clearAsyncValidators(): void { this.asyncValidator = null; }
/**
* Marks the control as `touched`.
*
* This will also mark all direct ancestors as `touched` to maintain
* the model.
*/
markAsTouched({onlySelf}: {onlySelf?: boolean} = {}): void {
this._touched = true;
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent.markAsTouched({onlySelf});
}
}
2016-06-08 18:36:24 -04:00
/**
* Marks the control as `untouched`.
*
* If the control has any children, it will also mark all children as `untouched`
* to maintain the model, and re-calculate the `touched` status of all parent
* controls.
*/
markAsUntouched({onlySelf}: {onlySelf?: boolean} = {}): void {
this._touched = false;
this._forEachChild(
(control: AbstractControl) => { control.markAsUntouched({onlySelf: true}); });
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent._updateTouched({onlySelf});
}
}
/**
* Marks the control as `dirty`.
*
* This will also mark all direct ancestors as `dirty` to maintain
* the model.
*/
2016-06-08 18:36:24 -04:00
markAsDirty({onlySelf}: {onlySelf?: boolean} = {}): void {
this._pristine = false;
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent.markAsDirty({onlySelf});
2016-06-08 18:36:24 -04:00
}
}
/**
* Marks the control as `pristine`.
*
* If the control has any children, it will also mark all children as `pristine`
* to maintain the model, and re-calculate the `pristine` status of all parent
* controls.
*/
markAsPristine({onlySelf}: {onlySelf?: boolean} = {}): void {
this._pristine = true;
this._forEachChild((control: AbstractControl) => { control.markAsPristine({onlySelf: true}); });
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent._updatePristine({onlySelf});
}
}
/**
* Marks the control as `pending`.
*/
2016-06-08 18:36:24 -04:00
markAsPending({onlySelf}: {onlySelf?: boolean} = {}): void {
this._status = PENDING;
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent.markAsPending({onlySelf});
2016-06-08 18:36:24 -04:00
}
}
/**
* Disables the control. This means the control will be exempt from validation checks and
* excluded from the aggregate value of any parent. Its status is `DISABLED`.
*
* If the control has children, all children will be disabled to maintain the model.
*/
disable({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._status = DISABLED;
this._errors = null;
this._forEachChild((control: AbstractControl) => { control.disable({onlySelf: true}); });
this._updateValue();
2016-10-21 18:14:44 -04:00
if (emitEvent !== false) {
this._valueChanges.emit(this._value);
this._statusChanges.emit(this._status);
}
this._updateAncestors(onlySelf);
this._onDisabledChange.forEach((changeFn) => changeFn(true));
}
/**
* Enables the control. This means the control will be included in validation checks and
* the aggregate value of its parent. Its status is re-calculated based on its value and
* its validators.
*
* If the control has children, all children will be enabled.
*/
enable({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._status = VALID;
this._forEachChild((control: AbstractControl) => { control.enable({onlySelf: true}); });
this.updateValueAndValidity({onlySelf: true, emitEvent});
this._updateAncestors(onlySelf);
this._onDisabledChange.forEach((changeFn) => changeFn(false));
}
private _updateAncestors(onlySelf: boolean) {
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent.updateValueAndValidity();
this._parent._updatePristine();
this._parent._updateTouched();
}
}
setParent(parent: FormGroup|FormArray): void { this._parent = parent; }
2016-06-08 18:36:24 -04:00
/**
* Sets the value of the control. Abstract method (implemented in sub-classes).
*/
abstract setValue(value: any, options?: Object): void;
/**
* Patches the value of the control. Abstract method (implemented in sub-classes).
*/
abstract patchValue(value: any, options?: Object): void;
/**
* Resets the control. Abstract method (implemented in sub-classes).
*/
abstract reset(value?: any, options?: Object): void;
/**
* Re-calculates the value and validation status of the control.
*
* By default, it will also update the value and validity of its ancestors.
*/
updateValueAndValidity({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
this._setInitialStatus();
2016-06-08 18:36:24 -04:00
this._updateValue();
if (this.enabled) {
this._cancelExistingSubscription();
this._errors = this._runValidator();
this._status = this._calculateStatus();
2016-06-08 18:36:24 -04:00
if (this._status === VALID || this._status === PENDING) {
this._runAsyncValidator(emitEvent);
}
}
2016-10-21 18:14:44 -04:00
if (emitEvent !== false) {
this._valueChanges.emit(this._value);
this._statusChanges.emit(this._status);
2016-06-08 18:36:24 -04:00
}
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent.updateValueAndValidity({onlySelf, emitEvent});
2016-06-08 18:36:24 -04:00
}
}
/** @internal */
_updateTreeValidity({emitEvent}: {emitEvent?: boolean} = {emitEvent: true}) {
this._forEachChild((ctrl: AbstractControl) => ctrl._updateTreeValidity({emitEvent}));
this.updateValueAndValidity({onlySelf: true, emitEvent});
}
private _setInitialStatus() { this._status = this._allControlsDisabled() ? DISABLED : VALID; }
2016-06-08 18:36:24 -04:00
private _runValidator(): {[key: string]: any} {
2016-10-21 18:14:44 -04:00
return this.validator ? this.validator(this) : null;
2016-06-08 18:36:24 -04:00
}
private _runAsyncValidator(emitEvent: boolean): void {
2016-10-21 18:14:44 -04:00
if (this.asyncValidator) {
2016-06-08 18:36:24 -04:00
this._status = PENDING;
2016-10-21 18:14:44 -04:00
const obs = toObservable(this.asyncValidator(this));
if (!(isObservable(obs))) {
throw new Error(
`expected the following validator to return Promise or Observable: ${this.asyncValidator}. If you are using FormBuilder; did you forget to brace your validators in an array?`);
}
this._asyncValidationSubscription =
obs.subscribe({next: (res: {[key: string]: any}) => this.setErrors(res, {emitEvent})});
2016-06-08 18:36:24 -04:00
}
}
private _cancelExistingSubscription(): void {
2016-10-21 18:14:44 -04:00
if (this._asyncValidationSubscription) {
this._asyncValidationSubscription.unsubscribe();
2016-06-08 18:36:24 -04:00
}
}
/**
* Sets errors on a form control.
2016-06-08 18:36:24 -04:00
*
* This is used when validations are run manually by the user, rather than automatically.
2016-06-08 18:36:24 -04:00
*
* Calling `setErrors` will also update the validity of the parent control.
*
* ### Example
2016-06-08 18:36:24 -04:00
*
* ```
* const login = new FormControl("someLogin");
2016-06-08 18:36:24 -04:00
* login.setErrors({
* "notUnique": true
* });
*
* expect(login.valid).toEqual(false);
* expect(login.errors).toEqual({"notUnique": true});
*
* login.setValue("someOtherLogin");
2016-06-08 18:36:24 -04:00
*
* expect(login.valid).toEqual(true);
* ```
*/
setErrors(errors: {[key: string]: any}, {emitEvent}: {emitEvent?: boolean} = {}): void {
this._errors = errors;
2016-10-21 18:14:44 -04:00
this._updateControlsErrors(emitEvent !== false);
2016-06-08 18:36:24 -04:00
}
/**
* Retrieves a child control given the control's name or path.
*
* Paths can be passed in as an array or a string delimited by a dot.
*
* To get a control nested within a `person` sub-group:
*
* * `this.form.get('person.name');`
*
* -OR-
*
* * `this.form.get(['person', 'name']);`
*/
get(path: Array<string|number>|string): AbstractControl { return _find(this, path, '.'); }
2016-06-08 18:36:24 -04:00
/**
* Returns true if the control with the given path has the error specified. Otherwise
* returns null or undefined.
*
* If no path is given, it checks for the error on the present control.
*/
2016-06-08 18:36:24 -04:00
getError(errorCode: string, path: string[] = null): any {
2016-10-21 18:14:44 -04:00
const control = path ? this.get(path) : this;
return control && control._errors ? control._errors[errorCode] : null;
2016-06-08 18:36:24 -04:00
}
/**
* Returns true if the control with the given path has the error specified. Otherwise
* returns false.
*
* If no path is given, it checks for the error on the present control.
*/
2016-06-08 18:36:24 -04:00
hasError(errorCode: string, path: string[] = null): boolean {
2016-10-21 18:14:44 -04:00
return !!this.getError(errorCode, path);
2016-06-08 18:36:24 -04:00
}
/**
* Retrieves the top-level ancestor of this control.
*/
2016-06-08 18:36:24 -04:00
get root(): AbstractControl {
let x: AbstractControl = this;
2016-10-21 18:14:44 -04:00
while (x._parent) {
2016-06-08 18:36:24 -04:00
x = x._parent;
}
return x;
}
/** @internal */
_updateControlsErrors(emitEvent: boolean): void {
2016-06-08 18:36:24 -04:00
this._status = this._calculateStatus();
if (emitEvent) {
this._statusChanges.emit(this._status);
}
2016-10-21 18:14:44 -04:00
if (this._parent) {
this._parent._updateControlsErrors(emitEvent);
2016-06-08 18:36:24 -04:00
}
}
/** @internal */
_initObservables() {
this._valueChanges = new EventEmitter();
this._statusChanges = new EventEmitter();
}
private _calculateStatus(): string {
if (this._allControlsDisabled()) return DISABLED;
2016-10-21 18:14:44 -04:00
if (this._errors) return INVALID;
2016-06-08 18:36:24 -04:00
if (this._anyControlsHaveStatus(PENDING)) return PENDING;
if (this._anyControlsHaveStatus(INVALID)) return INVALID;
return VALID;
}
/** @internal */
abstract _updateValue(): void;
/** @internal */
abstract _forEachChild(cb: Function): void;
/** @internal */
abstract _anyControls(condition: Function): boolean;
/** @internal */
abstract _allControlsDisabled(): boolean;
/** @internal */
_anyControlsHaveStatus(status: string): boolean {
return this._anyControls((control: AbstractControl) => control.status === status);
}
/** @internal */
_anyControlsDirty(): boolean {
return this._anyControls((control: AbstractControl) => control.dirty);
}
/** @internal */
_anyControlsTouched(): boolean {
return this._anyControls((control: AbstractControl) => control.touched);
}
/** @internal */
_updatePristine({onlySelf}: {onlySelf?: boolean} = {}): void {
this._pristine = !this._anyControlsDirty();
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent._updatePristine({onlySelf});
}
}
/** @internal */
_updateTouched({onlySelf}: {onlySelf?: boolean} = {}): void {
this._touched = this._anyControlsTouched();
2016-10-21 18:14:44 -04:00
if (this._parent && !onlySelf) {
this._parent._updateTouched({onlySelf});
}
}
/** @internal */
_onDisabledChange: Function[] = [];
/** @internal */
_isBoxedValue(formState: any): boolean {
2016-10-19 16:42:39 -04:00
return typeof formState === 'object' && formState !== null &&
Object.keys(formState).length === 2 && 'value' in formState && 'disabled' in formState;
}
/** @internal */
_registerOnCollectionChange(fn: () => void): void { this._onCollectionChange = fn; }
2016-06-08 18:36:24 -04:00
}
/**
2016-09-13 03:14:07 -04:00
* @whatItDoes Tracks the value and validation status of an individual form control.
2016-06-08 18:36:24 -04:00
*
2016-09-13 03:14:07 -04:00
* It is one of the three fundamental building blocks of Angular forms, along with
* {@link FormGroup} and {@link FormArray}.
2016-06-08 18:36:24 -04:00
*
2016-09-13 03:14:07 -04:00
* @howToUse
2016-06-08 18:36:24 -04:00
*
2016-09-13 03:14:07 -04:00
* When instantiating a {@link FormControl}, you can pass in an initial value as the
* first argument. Example:
*
* ```ts
* const ctrl = new FormControl('some value');
* console.log(ctrl.value); // 'some value'
*```
*
* You can also initialize the control with a form state object on instantiation,
* which includes both the value and whether or not the control is disabled.
* You can't use the value key without the disabled key; both are required
* to use this way of initialization.
2016-09-13 03:14:07 -04:00
*
* ```ts
* const ctrl = new FormControl({value: 'n/a', disabled: true});
* console.log(ctrl.value); // 'n/a'
* 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.
*
* ```ts
* const ctrl = new FormControl('', Validators.required);
* console.log(ctrl.value); // ''
* console.log(ctrl.status); // 'INVALID'
* ```
*
* See its superclass, {@link AbstractControl}, for more properties and methods.
*
* * **npm package**: `@angular/forms`
2016-06-08 18:36:24 -04:00
*
* @stable
2016-06-08 18:36:24 -04:00
*/
export class FormControl extends AbstractControl {
2016-06-08 18:36:24 -04:00
/** @internal */
_onChange: Function[] = [];
2016-06-08 18:36:24 -04:00
constructor(
formState: any = null, validator: ValidatorFn|ValidatorFn[] = null,
asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null) {
super(coerceToValidator(validator), coerceToAsyncValidator(asyncValidator));
this._applyFormState(formState);
2016-06-08 18:36:24 -04:00
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
this._initObservables();
}
/**
* Set the value of the form control to `value`.
2016-06-08 18:36:24 -04:00
*
* If `onlySelf` is `true`, this change will only affect the validation of this `FormControl`
2016-09-13 14:43:08 -04:00
* and not its parent component. This defaults to false.
*
* If `emitEvent` is `true`, this
2016-09-13 03:14:07 -04:00
* change will cause a `valueChanges` event on the `FormControl` to be emitted. This defaults
* to true (as it falls through to `updateValueAndValidity`).
2016-06-08 18:36:24 -04:00
*
* If `emitModelToViewChange` is `true`, the view will be notified about the new value
* via an `onChange` event. This is the default behavior if `emitModelToViewChange` is not
* specified.
*
* If `emitViewToModelChange` is `true`, an ngModelChange event will be fired to update the
* model. This is the default behavior if `emitViewToModelChange` is not specified.
2016-06-08 18:36:24 -04:00
*/
setValue(value: any, {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}: {
2016-06-08 18:36:24 -04:00
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
2016-06-08 18:36:24 -04:00
} = {}): void {
this._value = value;
2016-10-21 18:14:44 -04:00
if (this._onChange.length && emitModelToViewChange !== false) {
this._onChange.forEach((changeFn) => changeFn(this._value, emitViewToModelChange !== false));
}
this.updateValueAndValidity({onlySelf, emitEvent});
2016-06-08 18:36:24 -04:00
}
/**
2016-09-13 03:14:07 -04:00
* Patches the value of a control.
*
* This function is functionally the same as {@link FormControl.setValue} at this level.
* It exists for symmetry with {@link FormGroup.patchValue} on `FormGroups` and `FormArrays`,
* where it does behave differently.
*/
patchValue(value: any, options: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
} = {}): void {
this.setValue(value, options);
}
2016-09-13 03:14:07 -04:00
/**
* Resets the form control. This means by default:
*
* * it is marked as `pristine`
* * it is marked as `untouched`
* * value is set to null
*
* You can also reset to a specific form state by passing through a standalone
* value or a form state object that contains both a value and a disabled state
* (these are the only two properties that cannot be calculated).
*
* Ex:
*
* ```ts
* this.control.reset('Nancy');
*
* console.log(this.control.value); // 'Nancy'
* ```
*
* OR
*
* ```
* this.control.reset({value: 'Nancy', disabled: true});
2016-09-13 14:43:08 -04:00
*
2016-09-13 03:14:07 -04:00
* console.log(this.control.value); // 'Nancy'
* console.log(this.control.status); // 'DISABLED'
* ```
*/
reset(formState: any = null, {onlySelf, emitEvent}: {onlySelf?: boolean,
emitEvent?: boolean} = {}): void {
this._applyFormState(formState);
this.markAsPristine({onlySelf});
this.markAsUntouched({onlySelf});
this.setValue(this._value, {onlySelf, emitEvent});
}
2016-06-08 18:36:24 -04:00
/**
* @internal
*/
_updateValue() {}
/**
* @internal
*/
_anyControls(condition: Function): boolean { return false; }
2016-06-08 18:36:24 -04:00
/**
* @internal
*/
_allControlsDisabled(): boolean { return this.disabled; }
2016-06-08 18:36:24 -04:00
/**
* Register a listener for change events.
*/
registerOnChange(fn: Function): void { this._onChange.push(fn); }
/**
* @internal
*/
_clearChangeFns(): void {
this._onChange = [];
this._onDisabledChange = [];
this._onCollectionChange = () => {};
}
/**
* Register a listener for disabled events.
*/
registerOnDisabledChange(fn: (isDisabled: boolean) => void): void {
this._onDisabledChange.push(fn);
}
/**
* @internal
*/
_forEachChild(cb: Function): void {}
private _applyFormState(formState: any) {
if (this._isBoxedValue(formState)) {
this._value = formState.value;
formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) :
this.enable({onlySelf: true, emitEvent: false});
} else {
this._value = formState;
}
}
2016-06-08 18:36:24 -04:00
}
/**
2016-09-13 14:43:08 -04:00
* @whatItDoes Tracks the value and validity state of a group of {@link FormControl}
* instances.
2016-06-08 18:36:24 -04:00
*
2016-09-13 14:43:08 -04:00
* A `FormGroup` aggregates the values of each child {@link FormControl} into one object,
* with each control name as the key. It calculates its status by reducing the statuses
* of its children. For example, if one of the controls in a group is invalid, the entire
* group becomes invalid.
2016-06-08 18:36:24 -04:00
*
* `FormGroup` is one of the three fundamental building blocks used to define forms in Angular,
2016-09-13 14:43:08 -04:00
* along with {@link FormControl} and {@link FormArray}.
*
* @howToUse
*
* When instantiating a {@link FormGroup}, pass in a collection of child controls as the first
* argument. The key for each child will be the name under which it is registered.
*
* ### Example
*
* ```
* const form = new FormGroup({
* first: new FormControl('Nancy', Validators.minLength(2)),
* last: new FormControl('Drew'),
* });
*
* console.log(form.value); // {first: 'Nancy', last; 'Drew'}
* console.log(form.status); // 'VALID'
* ```
*
* You can also include group-level validators as the second arg, or group-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.
*
* ### Example
*
* ```
* const form = new FormGroup({
* password: new FormControl('', Validators.minLength(2)),
* passwordConfirm: new FormControl('', Validators.minLength(2)),
* }, passwordMatchValidator);
2016-06-08 18:36:24 -04:00
*
*
2016-09-13 14:43:08 -04:00
* function passwordMatchValidator(g: FormGroup) {
* return g.get('password').value === g.get('passwordConfirm').value
* ? null : {'mismatch': true};
* }
* ```
*
* * **npm package**: `@angular/forms`
*
* @stable
2016-06-08 18:36:24 -04:00
*/
export class FormGroup extends AbstractControl {
constructor(
public controls: {[key: string]: AbstractControl}, validator: ValidatorFn = null,
asyncValidator: AsyncValidatorFn = null) {
2016-06-08 18:36:24 -04:00
super(validator, asyncValidator);
this._initObservables();
this._setUpControls();
2016-06-08 18:36:24 -04:00
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}
/**
2016-09-13 14:43:08 -04:00
* Registers a control with the group's list of controls.
*
* This method does not update value or validity of the control, so for
* most cases you'll want to use {@link FormGroup.addControl} instead.
2016-06-08 18:36:24 -04:00
*/
registerControl(name: string, control: AbstractControl): AbstractControl {
if (this.controls[name]) return this.controls[name];
2016-06-08 18:36:24 -04:00
this.controls[name] = control;
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
return control;
2016-06-08 18:36:24 -04:00
}
/**
* Add a control to this group.
*/
addControl(name: string, control: AbstractControl): void {
this.registerControl(name, control);
this.updateValueAndValidity();
this._onCollectionChange();
2016-06-08 18:36:24 -04:00
}
/**
* Remove a control from this group.
*/
removeControl(name: string): void {
if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {});
2016-09-19 20:15:57 -04:00
delete (this.controls[name]);
this.updateValueAndValidity();
this._onCollectionChange();
}
/**
* Replace an existing control.
*/
setControl(name: string, control: AbstractControl): void {
if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {});
2016-09-19 20:15:57 -04:00
delete (this.controls[name]);
if (control) this.registerControl(name, control);
2016-06-08 18:36:24 -04:00
this.updateValueAndValidity();
this._onCollectionChange();
2016-06-08 18:36:24 -04:00
}
/**
2016-09-13 14:43:08 -04:00
* Check whether there is an enabled control with the given name in the group.
*
* It will return false for disabled controls. If you'd like to check for
* existence in the group only, use {@link AbstractControl.get} instead.
2016-06-08 18:36:24 -04:00
*/
contains(controlName: string): boolean {
return this.controls.hasOwnProperty(controlName) && this.controls[controlName].enabled;
2016-06-08 18:36:24 -04:00
}
2016-09-13 14:43:08 -04:00
/**
* Sets the value of the {@link FormGroup}. It accepts an object that matches
* the structure of the group, with control names as keys.
*
* This method performs strict checks, so it will throw an error if you try
* to set the value of a control that doesn't exist or if you exclude the
* value of a control.
*
* ### Example
*
* ```
* const form = new FormGroup({
* first: new FormControl(),
* last: new FormControl()
* });
* console.log(form.value); // {first: null, last: null}
*
* form.setValue({first: 'Nancy', last: 'Drew'});
* console.log(form.value); // {first: 'Nancy', last: 'Drew'}
*
* ```
*/
setValue(
value: {[key: string]: any},
{onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._checkAllValuesPresent(value);
Object.keys(value).forEach(name => {
this._throwIfControlMissing(name);
this.controls[name].setValue(value[name], {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf, emitEvent});
}
2016-09-13 14:43:08 -04:00
/**
* Patches the value of the {@link FormGroup}. It accepts an object with control
* names as keys, and will do its best to match the values to the correct controls
* in the group.
*
* It accepts both super-sets and sub-sets of the group without throwing an error.
*
* ### Example
*
* ```
* const form = new FormGroup({
* first: new FormControl(),
* last: new FormControl()
* });
* console.log(form.value); // {first: null, last: null}
*
* form.patchValue({first: 'Nancy'});
* console.log(form.value); // {first: 'Nancy', last: null}
*
* ```
*/
patchValue(
value: {[key: string]: any},
{onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
Object.keys(value).forEach(name => {
if (this.controls[name]) {
this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
}
});
this.updateValueAndValidity({onlySelf, emitEvent});
}
2016-09-13 14:43:08 -04:00
/**
* Resets the {@link FormGroup}. This means by default:
*
* * The group and all descendants are marked `pristine`
* * The group and all descendants are marked `untouched`
* * The value of all descendants will be null or null maps
*
* You can also reset to a specific form state by passing in a map of states
* that matches the structure of your form, with control names as keys. The state
* can be a standalone value or a form state object with both a value and a disabled
* status.
*
* ### Example
*
* ```ts
* this.form.reset({first: 'name', last: 'last name'});
2016-09-13 14:43:08 -04:00
*
* console.log(this.form.value); // {first: 'name', last: 'last name'}
* ```
*
* - OR -
*
* ```
* this.form.reset({
* first: {value: 'name', disabled: true},
* last: 'last'
* });
*
* console.log(this.form.value); // {first: 'name', last: 'last name'}
* console.log(this.form.get('first').status); // 'DISABLED'
* ```
*/
reset(value: any = {}, {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
this._forEachChild((control: AbstractControl, name: string) => {
control.reset(value[name], {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf, emitEvent});
this._updatePristine({onlySelf});
this._updateTouched({onlySelf});
}
2016-09-13 14:43:08 -04:00
/**
2016-09-13 16:07:40 -04:00
* The aggregate value of the {@link FormGroup}, including any disabled controls.
2016-09-13 14:43:08 -04:00
*
* If you'd like to include all values regardless of disabled status, use this method.
2016-09-13 16:07:40 -04:00
* Otherwise, the `value` property is the best way to get the value of the group.
2016-09-13 14:43:08 -04:00
*/
getRawValue(): any {
return this._reduceChildren(
{}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => {
acc[name] = control instanceof FormControl ? control.value : (<any>control).getRawValue();
return acc;
});
}
/** @internal */
_throwIfControlMissing(name: string): void {
if (!Object.keys(this.controls).length) {
throw new Error(`
There are no form controls registered with this group yet. If you're using ngModel,
you may want to check next tick (e.g. use setTimeout).
`);
}
if (!this.controls[name]) {
throw new Error(`Cannot find form control with name: ${name}.`);
}
}
/** @internal */
2016-07-15 19:26:19 -04:00
_forEachChild(cb: (v: any, k: string) => void): void {
Object.keys(this.controls).forEach(k => cb(this.controls[k], k));
2016-07-15 19:26:19 -04:00
}
2016-06-08 18:36:24 -04:00
/** @internal */
_setUpControls(): void {
this._forEachChild((control: AbstractControl) => {
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
});
2016-06-08 18:36:24 -04:00
}
/** @internal */
_updateValue(): void { this._value = this._reduceValue(); }
2016-06-08 18:36:24 -04:00
/** @internal */
_anyControls(condition: Function): boolean {
let res = false;
this._forEachChild((control: AbstractControl, name: string) => {
res = res || (this.contains(name) && condition(control));
2016-06-08 18:36:24 -04:00
});
return res;
}
/** @internal */
_reduceValue() {
return this._reduceChildren(
{}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => {
if (control.enabled || this.disabled) {
acc[name] = control.value;
}
2016-06-08 18:36:24 -04:00
return acc;
});
}
/** @internal */
_reduceChildren(initValue: any, fn: Function) {
let res = initValue;
this._forEachChild(
(control: AbstractControl, name: string) => { res = fn(res, control, name); });
2016-06-08 18:36:24 -04:00
return res;
}
/** @internal */
_allControlsDisabled(): boolean {
for (const controlName of Object.keys(this.controls)) {
if (this.controls[controlName].enabled) {
return false;
}
}
return Object.keys(this.controls).length > 0 || this.disabled;
2016-06-08 18:36:24 -04:00
}
/** @internal */
_checkAllValuesPresent(value: any): void {
this._forEachChild((control: AbstractControl, name: string) => {
if (value[name] === undefined) {
throw new Error(`Must supply a value for form control with name: '${name}'.`);
}
});
}
2016-06-08 18:36:24 -04:00
}
/**
* @whatItDoes Tracks the value and validity state of an array of {@link FormControl},
* {@link FormGroup} or {@link FormArray} instances.
2016-06-08 18:36:24 -04:00
*
2016-09-13 16:07:40 -04:00
* A `FormArray` aggregates the values of each child {@link FormControl} into an array.
* It calculates its status by reducing the statuses of its children. For example, if one of
* the controls in a `FormArray` is invalid, the entire array becomes invalid.
2016-06-08 18:36:24 -04:00
*
* `FormArray` is one of the three fundamental building blocks used to define forms in Angular,
2016-09-13 16:07:40 -04:00
* along with {@link FormControl} and {@link FormGroup}.
*
* @howToUse
*
* When instantiating a {@link FormArray}, pass in an array of child controls as the first
* argument.
*
* ### Example
*
* ```
* const arr = new FormArray([
* new FormControl('Nancy', Validators.minLength(2)),
* new FormControl('Drew'),
* ]);
*
* console.log(arr.value); // ['Nancy', 'Drew']
* 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.
2016-06-08 18:36:24 -04:00
*
2016-09-13 16:07:40 -04:00
* ### Adding or removing controls
2016-06-08 18:36:24 -04:00
*
* To change the controls in the array, use the `push`, `insert`, or `removeAt` methods
* in `FormArray` itself. These methods ensure the controls are properly tracked in the
2016-06-08 18:36:24 -04:00
* form's hierarchy. Do not modify the array of `AbstractControl`s used to instantiate
* the `FormArray` directly, as that will result in strange and unexpected behavior such
2016-06-08 18:36:24 -04:00
* as broken change detection.
*
2016-09-13 16:07:40 -04:00
* * **npm package**: `@angular/forms`
2016-06-08 18:36:24 -04:00
*
* @stable
2016-06-08 18:36:24 -04:00
*/
export class FormArray extends AbstractControl {
constructor(
public controls: AbstractControl[], validator: ValidatorFn = null,
asyncValidator: AsyncValidatorFn = null) {
2016-06-08 18:36:24 -04:00
super(validator, asyncValidator);
this._initObservables();
this._setUpControls();
2016-06-08 18:36:24 -04:00
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}
/**
* Get the {@link AbstractControl} at the given `index` in the array.
*/
at(index: number): AbstractControl { return this.controls[index]; }
/**
* Insert a new {@link AbstractControl} at the end of the array.
*/
push(control: AbstractControl): void {
this.controls.push(control);
this._registerControl(control);
2016-06-08 18:36:24 -04:00
this.updateValueAndValidity();
this._onCollectionChange();
2016-06-08 18:36:24 -04:00
}
/**
* Insert a new {@link AbstractControl} at the given `index` in the array.
*/
insert(index: number, control: AbstractControl): void {
this.controls.splice(index, 0, control);
this._registerControl(control);
2016-06-08 18:36:24 -04:00
this.updateValueAndValidity();
this._onCollectionChange();
2016-06-08 18:36:24 -04:00
}
/**
* Remove the control at the given `index` in the array.
*/
removeAt(index: number): void {
if (this.controls[index]) this.controls[index]._registerOnCollectionChange(() => {});
this.controls.splice(index, 1);
2016-06-08 18:36:24 -04:00
this.updateValueAndValidity();
this._onCollectionChange();
}
/**
* Replace an existing control.
*/
setControl(index: number, control: AbstractControl): void {
if (this.controls[index]) this.controls[index]._registerOnCollectionChange(() => {});
this.controls.splice(index, 1);
if (control) {
this.controls.splice(index, 0, control);
this._registerControl(control);
}
this.updateValueAndValidity();
this._onCollectionChange();
2016-06-08 18:36:24 -04:00
}
/**
* Length of the control array.
*/
get length(): number { return this.controls.length; }
2016-09-13 16:07:40 -04:00
/**
* Sets the value of the {@link FormArray}. It accepts an array that matches
* the structure of the control.
*
* This method performs strict checks, so it will throw an error if you try
* to set the value of a control that doesn't exist or if you exclude the
* value of a control.
*
* ### Example
*
* ```
* const arr = new FormArray([
* new FormControl(),
* new FormControl()
* ]);
* console.log(arr.value); // [null, null]
*
* arr.setValue(['Nancy', 'Drew']);
* console.log(arr.value); // ['Nancy', 'Drew']
* ```
*/
setValue(value: any[], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
this._checkAllValuesPresent(value);
value.forEach((newValue: any, index: number) => {
this._throwIfControlMissing(index);
this.at(index).setValue(newValue, {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf, emitEvent});
}
2016-09-13 16:07:40 -04:00
/**
* Patches the value of the {@link FormArray}. It accepts an array that matches the
* structure of the control, and will do its best to match the values to the correct
* controls in the group.
*
* It accepts both super-sets and sub-sets of the array without throwing an error.
*
* ### Example
*
* ```
* const arr = new FormArray([
* new FormControl(),
* new FormControl()
* ]);
* console.log(arr.value); // [null, null]
*
* arr.patchValue(['Nancy']);
* console.log(arr.value); // ['Nancy', null]
* ```
*/
patchValue(value: any[], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
value.forEach((newValue: any, index: number) => {
if (this.at(index)) {
this.at(index).patchValue(newValue, {onlySelf: true, emitEvent});
}
});
this.updateValueAndValidity({onlySelf, emitEvent});
}
2016-09-13 16:07:40 -04:00
/**
* Resets the {@link FormArray}. This means by default:
*
* * The array and all descendants are marked `pristine`
* * The array and all descendants are marked `untouched`
* * The value of all descendants will be null or null maps
*
* You can also reset to a specific form state by passing in an array of states
* that matches the structure of the control. The state can be a standalone value
* or a form state object with both a value and a disabled status.
*
* ### Example
*
* ```ts
* this.arr.reset(['name', 'last name']);
*
* console.log(this.arr.value); // ['name', 'last name']
* ```
*
* - OR -
*
* ```
* this.arr.reset([
* {value: 'name', disabled: true},
* 'last'
* ]);
*
* console.log(this.arr.value); // ['name', 'last name']
* console.log(this.arr.get(0).status); // 'DISABLED'
* ```
*/
reset(value: any = [], {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
this._forEachChild((control: AbstractControl, index: number) => {
control.reset(value[index], {onlySelf: true, emitEvent});
});
this.updateValueAndValidity({onlySelf, emitEvent});
this._updatePristine({onlySelf});
this._updateTouched({onlySelf});
}
2016-09-13 16:07:40 -04:00
/**
* The aggregate value of the array, including any disabled controls.
*
* If you'd like to include all values regardless of disabled status, use this method.
* Otherwise, the `value` property is the best way to get the value of the array.
*/
getRawValue(): any[] {
return this.controls.map((control: AbstractControl) => {
return control instanceof FormControl ? control.value : (<any>control).getRawValue();
});
}
/** @internal */
_throwIfControlMissing(index: number): void {
if (!this.controls.length) {
throw new Error(`
There are no form controls registered with this array yet. If you're using ngModel,
you may want to check next tick (e.g. use setTimeout).
`);
}
if (!this.at(index)) {
throw new Error(`Cannot find form control at index ${index}`);
}
}
/** @internal */
_forEachChild(cb: Function): void {
this.controls.forEach((control: AbstractControl, index: number) => { cb(control, index); });
}
2016-06-08 18:36:24 -04:00
/** @internal */
_updateValue(): void {
this._value = this.controls.filter((control) => control.enabled || this.disabled)
.map((control) => control.value);
}
2016-06-08 18:36:24 -04:00
/** @internal */
_anyControls(condition: Function): boolean {
return this.controls.some((control: AbstractControl) => control.enabled && condition(control));
2016-06-08 18:36:24 -04:00
}
/** @internal */
_setUpControls(): void {
this._forEachChild((control: AbstractControl) => this._registerControl(control));
2016-06-08 18:36:24 -04:00
}
/** @internal */
_checkAllValuesPresent(value: any): void {
this._forEachChild((control: AbstractControl, i: number) => {
if (value[i] === undefined) {
throw new Error(`Must supply a value for form control at index: ${i}.`);
}
});
}
/** @internal */
_allControlsDisabled(): boolean {
for (const control of this.controls) {
if (control.enabled) return false;
}
return this.controls.length > 0 || this.disabled;
}
private _registerControl(control: AbstractControl) {
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
}
2016-06-08 18:36:24 -04:00
}