forms-reactive
Version:
Reactive Form Web Component
1,444 lines • 64.4 kB
JavaScript
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable no-underscore-dangle */
/* eslint-disable max-classes-per-file */
// tslint:disable: function-name variable-name
import { Subject } from "rxjs";
import { composeAsyncValidators, composeValidators } from "./directives/shared";
import { ReactiveFormStatus } from "./types";
import { normalizeValidator, toObservable } from "./validators";
/**
* @license
* Copyright Google LLC 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
*/
function _find(control, path, delimiter) {
let mPath = path;
if (mPath == null) {
return null;
}
if (!Array.isArray(mPath)) {
mPath = mPath.split(delimiter);
}
if (Array.isArray(mPath) && mPath.length === 0)
return null;
// Not using Array.reduce here due to a Chrome 80 bug
// https://bugs.chromium.org/p/chromium/issues/detail?id=1049982
let controlToFind = control;
mPath.forEach((name) => {
if (controlToFind instanceof FormGroup) {
controlToFind = Object.prototype.hasOwnProperty.call(controlToFind.controls, name)
? controlToFind.controls[name]
: null;
}
else if (controlToFind instanceof FormArray) {
controlToFind = controlToFind.at(name) || null;
}
else {
controlToFind = null;
}
});
return controlToFind;
}
/**
* Gets validators from either an options object or given validators.
*/
function pickValidators(validatorOrOpts) {
return ((isOptionsObj(validatorOrOpts)
? validatorOrOpts.validators
: validatorOrOpts) || null);
}
/**
* Creates validator function by combining provided validators.
*/
function coerceToValidator(validator) {
return Array.isArray(validator)
? composeValidators(validator)
: normalizeValidator(validator) || null;
}
/**
* Gets async validators from either an options object or given validators.
*/
function pickAsyncValidators(asyncValidator, validatorOrOpts) {
return ((isOptionsObj(validatorOrOpts)
? validatorOrOpts.asyncValidators
: asyncValidator) || null);
}
/**
* Creates async validator function by combining provided async validators.
*/
function coerceToAsyncValidator(asyncValidator) {
return Array.isArray(asyncValidator)
? composeAsyncValidators(asyncValidator)
: normalizeValidator(asyncValidator) || null;
}
function isOptionsObj(validatorOrOpts) {
return (validatorOrOpts != null
&& !Array.isArray(validatorOrOpts)
&& typeof validatorOrOpts === 'object');
}
/**
* This is the base class for `FormControl`, `FormGroup`, and `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.
*
* @see [Forms Guide](/guide/forms)
* @see [Reactive Forms Guide](/guide/reactive-forms)
* @see [Dynamic Forms Guide](/guide/dynamic-form)
*
* @publicApi
*/
export class AbstractControl {
/**
* Sets custom data so it can be written from validators and available in parent control.
* @param key the key of the data to save
* @param value the data to save
*/
setCustomData(key, value) {
if (value) {
this._customData[key] = value;
}
else {
delete this._customData[key];
}
}
/**
* Gets custom data saved on this control.
* @param key the key to read from custom data
* @returns the value previously saved or undefined if none available
*/
getCustomData(key) {
return this._customData[key];
}
/**
* Gets the DOM html element to which this control is bound.
*/
getHtmlElement() {
return this._htmlElement;
}
/**
* Sets the DOM html element to which this control is bound.
*/
setHtmlElement(htmlElement) {
this._htmlElement = htmlElement;
}
/**
* The function that is used to determine the validity of this control synchronously.
*/
get validator() {
return this._composedValidatorFn;
}
set validator(validatorFn) {
this._composedValidatorFn = validatorFn;
this._rawValidators = validatorFn;
}
/**
* The function that is used to determine the validity of this control asynchronously.
*/
get asyncValidator() {
return this._composedAsyncValidatorFn;
}
set asyncValidator(asyncValidatorFn) {
this._composedAsyncValidatorFn = asyncValidatorFn;
this._rawAsyncValidators = asyncValidatorFn;
}
/**
* The parent control.
*/
get parent() {
return this._parent;
}
/**
* A control is `valid` when its `status` is `VALID`.
*
* @see {@link AbstractControl.status}
*
* @returns True if the control has passed all of its validation tests,
* false otherwise.
*/
get valid() {
return this.status === ReactiveFormStatus.VALID;
}
/**
* A control is `invalid` when its `status` is `INVALID`.
*
* @see {@link AbstractControl.status}
*
* @returns True if this control has failed one or more of its validation checks,
* false otherwise.
*/
get invalid() {
return this.status === ReactiveFormStatus.INVALID;
}
/**
* A control is `pending` when its `status` is `PENDING`.
*
* @see {@link AbstractControl.status}
*
* @returns True if this control is in the process of conducting a validation check,
* false otherwise.
*/
get pending() {
return this.status === ReactiveFormStatus.PENDING;
}
/**
* A control is `disabled` when its `status` is `DISABLED`.
*
* Disabled controls are exempt from validation checks and
* are not included in the aggregate value of their ancestor
* controls.
*
* @see {@link AbstractControl.status}
*
* @returns True if the control is disabled, false otherwise.
*/
get disabled() {
return this.status === ReactiveFormStatus.DISABLED;
}
/**
* A control is `enabled` as long as its `status` is not `DISABLED`.
*
* @returns True if the control has any status other than 'DISABLED',
* false if the status is 'DISABLED'.
*
* @see {@link AbstractControl.status}
*
*/
get enabled() {
return this.status !== ReactiveFormStatus.DISABLED;
}
/**
* A control is `dirty` if the user has changed the value
* in the UI.
*
* @returns True if the user has changed the value of this control in the UI; compare `pristine`.
* Programmatic changes to a control's value do not mark it dirty.
*/
get dirty() {
return !this.pristine;
}
/**
* True if the control has not been marked as touched
*
* A control is `untouched` if the user has not yet triggered
* a `blur` event on it.
*/
get untouched() {
return !this.touched;
}
/**
* Reports the update strategy of the `AbstractControl` (meaning
* the event on which the control updates itself).
* Possible values: `'change'` | `'blur'` | `'submit'`
* Default value: `'change'`
*/
get updateOn() {
if (this._updateOn) {
return this._updateOn;
}
return this.parent
? this.parent.updateOn
: 'change';
}
/**
* Initialize the AbstractControl instance.
*
* @param validators The function or array of functions that is used to determine the validity of
* this control synchronously.
* @param asyncValidators The function or array of functions that is used to determine validity of
* this control asynchronously.
*/
constructor(validators, asyncValidators) {
/**
* Indicates that a control has its own pending asynchronous validation in progress.
*
* @internal
*/
this._hasOwnPendingAsyncValidator = false;
/**
* Custom data so it can be written from validators and available in parent control.
*/
this._customData = {};
/**
* A control is `pristine` if the user has not yet changed
* the value in the UI.
*
* @returns True if the user has not yet changed the value in the UI; compare `dirty`.
* Programmatic changes to a control's value do not mark it dirty.
*/
this.pristine = true;
/**
* True if the control is marked as `touched`.
*
* A control is marked `touched` once the user has triggered
* a `blur` event on it.
*/
this.touched = false;
/** @internal */
this._onCollectionChange = () => { };
/** @internal */
this._onDisabledChange = [];
this._rawValidators = validators;
this._rawAsyncValidators = asyncValidators;
this._composedValidatorFn = coerceToValidator(this._rawValidators);
this._composedAsyncValidatorFn = coerceToAsyncValidator(this._rawAsyncValidators);
}
/**
* Gets the synchronous validators that are active on this control.
* @returns Current validators.
*/
getValidators() {
return this._rawValidators;
}
/**
* Sets the synchronous validators that are active on this control. Calling
* this overwrites any existing sync validators.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
*/
setValidators(newValidator) {
this._rawValidators = newValidator;
this._composedValidatorFn = coerceToValidator(newValidator);
}
/**
* Gets the async validators that are active on this control.
* @returns Current validators.
*/
getAsyncValidators() {
return this._rawAsyncValidators;
}
/**
* Sets the async validators that are active on this control. Calling this
* overwrites any existing async validators.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
*/
setAsyncValidators(newValidator) {
this._rawAsyncValidators = newValidator;
this._composedAsyncValidatorFn = coerceToAsyncValidator(newValidator);
}
/**
* Empties out the sync validator list.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
*/
clearValidators() {
this.validator = null;
}
/**
* Empties out the async validator list.
*
* When you add or remove a validator at run time, you must call
* `updateValueAndValidity()` for the new validation to take effect.
*
*/
clearAsyncValidators() {
this.asyncValidator = null;
}
/**
* Marks the control as `touched`. A control is touched by focus and
* blur events that do not change the value.
*
* @see `markAsUntouched()`
* @see `markAsDirty()`
* @see `markAsPristine()`
*
* @param opts Configuration options that determine how the control propagates changes
* and emits events after marking is applied.
* * `onlySelf`: When true, mark only this control. When false or not supplied,
* marks all direct ancestors. Default is false.
*/
markAsTouched(opts = {}) {
this.touched = true;
if (this._parent && !opts.onlySelf) {
this._parent.markAsTouched(Object.assign(Object.assign({}, opts), { emitEvent: false }));
}
if (opts.emitEvent) {
this.statusChanges.next(this.status);
}
}
/**
* Marks the control and all its descendant controls as `touched`.
* @see `markAsTouched()`
*/
markAllAsTouched() {
this.markAsTouched({ onlySelf: true });
this._forEachChild((control) => control.markAllAsTouched());
}
/**
* Marks the control as `untouched`.
*
* If the control has any children, also marks all children as `untouched`
* and recalculates the `touched` status of all parent controls.
*
* @see `markAsTouched()`
* @see `markAsDirty()`
* @see `markAsPristine()`
*
* @param opts Configuration options that determine how the control propagates changes
* and emits events after the marking is applied.
* * `onlySelf`: When true, mark only this control. When false or not supplied,
* marks all direct ancestors. Default is false.
*/
markAsUntouched(opts = {}) {
this.touched = false;
this._pendingTouched = false;
this._forEachChild((control) => {
control.markAsUntouched({ onlySelf: true, emitEvent: false });
});
if (this._parent && !opts.onlySelf) {
this._parent._updateTouched(opts);
}
if (opts.emitEvent) {
this.statusChanges.next(this.status);
}
}
/**
* Marks the control as `dirty`. A control becomes dirty when
* the control's value is changed through the UI; compare `markAsTouched`.
*
* @see `markAsTouched()`
* @see `markAsUntouched()`
* @see `markAsPristine()`
*
* @param opts Configuration options that determine how the control propagates changes
* and emits events after marking is applied.
* * `onlySelf`: When true, mark only this control. When false or not supplied,
* marks all direct ancestors. Default is false.
*/
markAsDirty(opts = {}) {
this.pristine = false;
if (this._parent && !opts.onlySelf) {
this._parent.markAsDirty(opts);
}
}
/**
* Marks the control as `pristine`.
*
* If the control has any children, marks all children as `pristine`,
* and recalculates the `pristine` status of all parent
* controls.
*
* @see `markAsTouched()`
* @see `markAsUntouched()`
* @see `markAsDirty()`
*
* @param opts Configuration options that determine how the control emits events after
* marking is applied.
* * `onlySelf`: When true, mark only this control. When false or not supplied,
* marks all direct ancestors. Default is false.
*/
markAsPristine(opts = {}) {
this.pristine = true;
this._pendingDirty = false;
this._forEachChild((control) => {
control.markAsPristine({ onlySelf: true });
});
if (this._parent && !opts.onlySelf) {
this._parent._updatePristine(opts);
}
}
/**
* Marks the control as `pending`.
*
* A control is pending while the control performs async validation.
*
* @see {@link AbstractControl.status}
*
* @param opts Configuration options that determine how the control propagates changes and
* emits events after marking is applied.
*/
markAsPending(opts = {}) {
this.status = ReactiveFormStatus.PENDING;
if (opts.emitEvent) {
this.statusChanges.next(this.status);
}
if (this._parent && !opts.onlySelf) {
this._parent.markAsPending(opts);
}
}
/**
* Disables the control. This means the control is exempt from validation checks and
* excluded from the aggregate value of any parent. Its status is `DISABLED`.
*
* If the control has children, all children are also disabled.
*
* @see {@link AbstractControl.status}
*
* @param opts Configuration options that determine how the control propagates
* changes and emits events after the control is disabled.
*/
disable(opts = {}) {
// If parent has been marked artificially dirty we don't want to re-calculate the
// parent's dirtiness based on the children.
const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf);
this.status = ReactiveFormStatus.DISABLED;
this.errors = null;
this._forEachChild((control) => {
control.disable(Object.assign(Object.assign({}, opts), { onlySelf: true }));
});
this._updateValue();
// tslint:disable-next-line: no-boolean-literal-compare
if (opts.emitEvent !== false) {
this.valueChanges.next(this.value);
this.statusChanges.next(this.status);
}
this._updateAncestors(Object.assign(Object.assign({}, opts), { skipPristineCheck }));
this._onDisabledChange.forEach(changeFn => changeFn(true));
}
/**
* Enables the control. This means the control is included in validation checks and
* the aggregate value of its parent. Its status recalculates based on its value and
* its validators.
*
* By default, if the control has children, all children are enabled.
*
* @see {@link AbstractControl.status}
*
* @param opts Configure options that control how the control propagates changes and
* emits events when marked as untouched
* * `onlySelf`: When true, mark only this control. When false or not supplied,
* marks all direct ancestors. Default is false.
* * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
* `valueChanges`
* observables emit events with the latest status and value when the control is enabled.
* When false, no events are emitted.
*/
enable(opts = {}) {
// If parent has been marked artificially dirty we don't want to re-calculate the
// parent's dirtiness based on the children.
const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf);
this.status = ReactiveFormStatus.VALID;
this._forEachChild((control) => {
control.enable(Object.assign(Object.assign({}, opts), { onlySelf: true }));
});
this.updateValueAndValidity({ onlySelf: true, emitEvent: opts.emitEvent });
this._updateAncestors(Object.assign(Object.assign({}, opts), { skipPristineCheck }));
this._onDisabledChange.forEach(changeFn => changeFn(false));
}
_updateAncestors(opts) {
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts);
if (!opts.skipPristineCheck) {
this._parent._updatePristine();
}
this._parent._updateTouched();
}
}
/**
* @param parent Sets the parent of the control
*/
setParent(parent) {
this._parent = parent;
}
/**
* Recalculates the value and validation status of the control.
*
* By default, it also updates the value and validity of its ancestors.
*
* @param opts Configuration options determine how the control propagates changes and emits events
* after updates and validity checks are applied.
*/
updateValueAndValidity(opts = {}) {
this._setInitialStatus();
this._updateValue();
if (this.enabled) {
this._cancelExistingSubscription();
this.errors = this._runValidator();
this.status = this._calculateStatus();
if (this.status === ReactiveFormStatus.VALID || this.status === ReactiveFormStatus.PENDING) {
this._runAsyncValidator(opts.emitEvent);
}
}
if (opts.emitEvent !== false) {
this.valueChanges.next(this.value);
this.statusChanges.next(this.status);
}
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts);
}
}
/** @internal */
_updateTreeValidity(opts = { emitEvent: true }) {
this._forEachChild((ctrl) => ctrl._updateTreeValidity(opts));
this.updateValueAndValidity({ onlySelf: true, emitEvent: opts.emitEvent });
}
_setInitialStatus() {
this.status = this._allControlsDisabled()
? ReactiveFormStatus.DISABLED
: ReactiveFormStatus.VALID;
}
_runValidator() {
return this.validator ? this.validator(this) : null;
}
_runAsyncValidator(emitEvent) {
if (this.asyncValidator) {
this.status = ReactiveFormStatus.PENDING;
this._hasOwnPendingAsyncValidator = true;
const obs = toObservable(this.asyncValidator(this));
this._asyncValidationSubscription = obs.subscribe((errors) => {
this._hasOwnPendingAsyncValidator = false;
// This will trigger the recalculation of the validation status, which depends on
// the state of the asynchronous validation (whether it is in progress or not). So, it is
// necessary that we have updated the `_hasOwnPendingAsyncValidator` boolean flag first.
this.setErrors(errors, { emitEvent });
});
}
}
_cancelExistingSubscription() {
if (this._asyncValidationSubscription) {
this._asyncValidationSubscription.unsubscribe();
this._hasOwnPendingAsyncValidator = false;
}
}
/**
* Sets errors on a form control when running validations manually, rather than automatically.
*
* Calling `setErrors` also updates the validity of the parent control.
*
* @usageNotes
*
* ### Manually set the errors for a control
*
* ```
* const login = new FormControl('someLogin');
* login.setErrors({
* notUnique: true
* });
*
* expect(login.valid).toEqual(false);
* expect(login.errors).toEqual({ notUnique: true });
*
* login.setValue('someOtherLogin');
*
* expect(login.valid).toEqual(true);
* ```
*/
setErrors(errors, opts = {}) {
this.errors = errors;
this._updateControlsErrors(opts.emitEvent !== false);
}
/**
* Retrieves a child control given the control's name or path.
*
* @param path A dot-delimited string or array of string/number values that define the path to the
* control.
*
* @usageNotes
* ### Retrieve a nested control
*
* For example, to get a `name` control nested within a `person` sub-group:
*
* * `this.form.get('person.name');`
*
* -OR-
*
* * `this.form.get(['person', 'name']);`
*/
get(path) {
return _find(this, path, '.');
}
/**
* @description
* Reports error data for the control with the given path.
*
* @param errorCode The code of the error to check
* @param path A list of control names that designates how to move from the current control
* to the control that should be queried for errors.
*
* @usageNotes
* For example, for the following `FormGroup`:
*
* ```
* form = new FormGroup({
* address: new FormGroup({ street: new FormControl() })
* });
* ```
*
* The path to the 'street' control from the root form would be 'address' -> 'street'.
*
* It can be provided to this method in one of two formats:
*
* 1. An array of string control names, e.g. `['address', 'street']`
* 1. A period-delimited list of control names in one string, e.g. `'address.street'`
*
* @returns error data for that particular error. If the control or error is not present,
* null is returned.
*/
getError(errorCode, path) {
const control = path ? this.get(path) : this;
return control && control.errors ? control.errors[errorCode] : null;
}
/**
* @description
* Reports whether the control with the given path has the error specified.
*
* @param errorCode The code of the error to check
* @param path A list of control names that designates how to move from the current control
* to the control that should be queried for errors.
*
* @usageNotes
* For example, for the following `FormGroup`:
*
* ```
* form = new FormGroup({
* address: new FormGroup({ street: new FormControl() })
* });
* ```
*
* The path to the 'street' control from the root form would be 'address' -> 'street'.
*
* It can be provided to this method in one of two formats:
*
* 1. An array of string control names, e.g. `['address', 'street']`
* 1. A period-delimited list of control names in one string, e.g. `'address.street'`
*
* If no path is given, this method checks for the error on the current control.
*
* @returns whether the given error is present in the control at the given path.
*
* If the control is not present, false is returned.
*/
hasError(errorCode, path) {
return !!this.getError(errorCode, path);
}
/**
* Retrieves the top-level ancestor of this control.
*/
get root() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let x = this;
while (x._parent) {
x = x._parent;
}
return x;
}
/** @internal */
_updateControlsErrors(emitEvent) {
this.status = this._calculateStatus();
if (emitEvent) {
this.statusChanges.next(this.status);
}
if (this._parent) {
this._parent._updateControlsErrors(emitEvent);
}
}
/** @internal */
_initObservables() {
this.valueChanges = new Subject();
this.statusChanges = new Subject();
}
_calculateStatus() {
if (this._allControlsDisabled())
return ReactiveFormStatus.DISABLED;
if (this.errors)
return ReactiveFormStatus.INVALID;
if (this._hasOwnPendingAsyncValidator
|| this._anyControlsHaveStatus(ReactiveFormStatus.PENDING)) {
return ReactiveFormStatus.PENDING;
}
if (this._anyControlsHaveStatus(ReactiveFormStatus.INVALID))
return ReactiveFormStatus.INVALID;
return ReactiveFormStatus.VALID;
}
/** @internal */
_anyControlsHaveStatus(status) {
return this._anyControls((control) => control.status === status);
}
/** @internal */
_anyControlsDirty() {
return this._anyControls((control) => control.dirty);
}
/** @internal */
_anyControlsTouched() {
return this._anyControls((control) => control.touched);
}
/** @internal */
_updatePristine(opts = {}) {
this.pristine = !this._anyControlsDirty();
if (this._parent && !opts.onlySelf) {
this._parent._updatePristine(opts);
}
}
/** @internal */
_updateTouched(opts = {}) {
this.touched = this._anyControlsTouched();
if (this._parent && !opts.onlySelf) {
this._parent._updateTouched(opts);
}
}
/** @internal */
_isBoxedValue(formState) {
return (typeof formState === 'object'
&& formState !== null
&& Object.keys(formState).length === 2
&& 'value' in formState
&& 'disabled' in formState);
}
/** @internal */
_registerOnCollectionChange(fn) {
this._onCollectionChange = fn;
}
/** @internal */
_setUpdateStrategy(opts) {
if (isOptionsObj(opts) && opts.updateOn != null) {
this._updateOn = opts.updateOn;
}
}
/**
* Check to see if parent has been marked artificially dirty.
*
* @internal
*/
_parentMarkedDirty(onlySelf) {
const parentDirty = this._parent && this._parent.dirty;
return !onlySelf && parentDirty && !this._parent._anyControlsDirty();
}
}
/**
* Tracks the value and validation status of an individual form control.
*
* This is one of the three fundamental building blocks of Angular forms, along with
* `FormGroup` and `FormArray`. It extends the `AbstractControl` class that
* implements most of the base functionality for accessing the value, validation status,
* user interactions and events. See [usage examples below](#usage-notes).
*
* @see `AbstractControl`
* @see [Reactive Forms Guide](guide/reactive-forms)
* @see [Usage Notes](#usage-notes)
*
* @usageNotes
*
* ### Initializing Form Controls
*
* Instantiate a `FormControl`, with an initial value.
*
* ```ts
* const control = new FormControl('some value');
* console.log(control.value); // 'some value'
*```
*
* The following example initializes the control with a form state object. The `value`
* and `disabled` keys are required in this case.
*
* ```ts
* const control = new FormControl({ value: 'n/a', disabled: true });
* console.log(control.value); // 'n/a'
* console.log(control.status); // 'DISABLED'
* ```
*
* The following example initializes the control with a sync validator.
*
* ```ts
* const control = new FormControl('', Validators.required);
* console.log(control.value); // ''
* console.log(control.status); // 'INVALID'
* ```
*
* The following example initializes the control using an options object.
*
* ```ts
* const control = new FormControl('', {
* validators: Validators.required,
* asyncValidators: myAsyncValidator
* });
* ```
*
* ### Configure the control to update on a blur event
*
* Set the `updateOn` option to `'blur'` to update on the blur `event`.
*
* ```ts
* const control = new FormControl('', { updateOn: 'blur' });
* ```
*
* ### Configure the control to update on a submit event
*
* Set the `updateOn` option to `'submit'` to update on a submit `event`.
*
* ```ts
* const control = new FormControl('', { updateOn: 'submit' });
* ```
*
* ### Reset the control back to an initial value
*
* You 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).
*
* ```ts
* const control = new FormControl('Nancy');
*
* console.log(control.value); // 'Nancy'
*
* control.reset('Drew');
*
* console.log(control.value); // 'Drew'
* ```
*
* ### Reset the control back to an initial value and disabled
*
* ```
* const control = new FormControl('Nancy');
*
* console.log(control.value); // 'Nancy'
* console.log(control.status); // 'VALID'
*
* control.reset({ value: 'Drew', disabled: true });
*
* console.log(control.value); // 'Drew'
* console.log(control.status); // 'DISABLED'
* ```
*
* @publicApi
*/
export class FormControl extends AbstractControl {
/**
* Creates a new `FormControl` instance.
*
* @param formState Initializes the control with an initial value,
* or an object that defines the initial value and disabled state.
*
* @param validatorOrOpts A synchronous validator function, or an array of
* such functions, or an `AbstractControlOptions` object that contains validation functions
* and a validation trigger.
*
* @param asyncValidator A single async validator or array of async validator functions
*
*/
constructor(formState = null, validatorOrOpts = null, asyncValidator = null) {
super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
/** @internal */
this._onChange = [];
this._applyFormState(formState);
this._initObservables();
this._setUpdateStrategy(validatorOrOpts);
this.updateValueAndValidity({
onlySelf: true,
// If `asyncValidator` is present, it will trigger control status change from `PENDING` to
// `VALID` or `INVALID`.
// The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent`
// to `true` to allow that during the control creation process.
emitEvent: !!asyncValidator,
});
}
/**
* Sets a new value for the form control.
*
* @param value The new value for the control.
* @param options Configuration options that determine how the control propagates changes
* and emits events when the value changes.
* The configuration options are passed to the {@link AbstractControl#updateValueAndValidity
* updateValueAndValidity} method.
*
*/
setValue(value, options = {}) {
this._pendingValue = value;
this.value = value;
try {
const htmlElement = this.getHtmlElement();
htmlElement.value = value;
}
catch (_e) {
}
if (this._onChange.length && options.emitModelToViewChange) {
this._onChange.forEach(changeFn => changeFn(this.value, options.emitViewToModelChange));
}
this.updateValueAndValidity(options);
}
/**
* Patches the value of a control.
*
* This function is functionally the same as {@link FormControl#setValue setValue} at this level.
* It exists for symmetry with {@link FormGroup#patchValue patchValue} on `FormGroups` and
* `FormArrays`, where it does behave differently.
*
* @see `setValue` for options
*/
patchValue(value, options = {}) {
this.setValue(value, options);
}
/**
* Resets the form control, marking it `pristine` and `untouched`, and setting
* the value to null.
*
* @param formState Resets the control with an initial value,
* or an object that defines the initial value and disabled state.
*
* @param options Configuration options that determine how the control propagates changes
* and emits events after the value changes.
*
*/
reset(formState = null, options = {}) {
this._applyFormState(formState);
this.markAsPristine(options);
this.markAsUntouched(options);
this.setValue(this.value, options);
this._pendingChange = false;
}
/**
* @internal
*/
_updateValue() { }
/**
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_anyControls(_condition) {
return false;
}
/**
* @internal
*/
_allControlsDisabled() {
return this.disabled;
}
/**
* Register a listener for change events.
*
* @param fn The method that is called when the value changes
*/
registerOnChange(fn) {
this._onChange.push(fn);
}
/**
* @internal
*/
_clearChangeFns() {
this._onChange = [];
this._onDisabledChange = [];
this._onCollectionChange = () => { };
}
/**
* Register a listener for disabled events.
*
* @param fn The method that is called when the disabled status changes.
*/
registerOnDisabledChange(fn) {
this._onDisabledChange.push(fn);
}
/**
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_forEachChild(_cb) { }
/** @internal */
_syncPendingControls() {
if (this.updateOn === 'submit') {
if (this._pendingDirty)
this.markAsDirty();
if (this._pendingTouched)
this.markAsTouched();
if (this._pendingChange) {
this.setValue(this._pendingValue, {
onlySelf: true,
emitModelToViewChange: false,
});
return true;
}
}
return false;
}
_applyFormState(formState) {
if (this._isBoxedValue(formState)) {
this._pendingValue = formState.value;
this.value = formState.value;
if (formState.disabled) {
this.disable({ onlySelf: true, emitEvent: false });
}
else {
this.enable({ onlySelf: true, emitEvent: false });
}
}
else {
this._pendingValue = formState;
this.value = formState;
}
}
}
/**
* Tracks the value and validity state of a group of `FormControl` instances.
*
* A `FormGroup` aggregates the values of each child `FormControl` into one object,
* with each control name as the key. It calculates its status by reducing the status values
* of its children. For example, if one of the controls in a group is invalid, the entire
* group becomes invalid.
*
* `FormGroup` is one of the three fundamental building blocks used to define forms in Angular,
* along with `FormControl` and `FormArray`.
*
* When instantiating a `FormGroup`, pass in a collection of child controls as the first
* argument. The key for each child registers the name for the control.
*
* @usageNotes
*
* ### Create a form group with 2 controls
*
* ```
* 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'
* ```
*
* ### Create a form group with a group-level validator
*
* You 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.
*
* ```
* const form = new FormGroup({
* password: new FormControl('', Validators.minLength(2)),
* passwordConfirm: new FormControl('', Validators.minLength(2)),
* }, passwordMatchValidator);
*
*
* function passwordMatchValidator(g: FormGroup) {
* return g.get('password').value === g.get('passwordConfirm').value
* ? null : {'mismatch': true};
* }
* ```
*
* Like `FormControl` instances, you 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 });
* ```
*
* ### Set the updateOn property for all controls in a form group
*
* The options object is used to set a default value for each child
* control's `updateOn` property. If you set `updateOn` to `'blur'` at the
* group level, all child controls default to 'blur', unless the child
* has explicitly specified a different `updateOn` value.
*
* ```ts
* const c = new FormGroup({
* one: new FormControl()
* }, { updateOn: 'blur' });
* ```
*
* @publicApi
*/
export class FormGroup extends AbstractControl {
/**
* Creates a new `FormGroup` instance.
*
* @param controls A collection of child controls. The key for each child is the name
* under which it is registered.
*
* @param validatorOrOpts A synchronous validator function, or an array of
* such functions, or an `AbstractControlOptions` object that contains validation functions
* and a validation trigger.
*
* @param asyncValidator A single async validator or array of async validator functions
*
*/
constructor(controls, validatorOrOpts, asyncValidator) {
super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
this.controls = controls;
this._initObservables();
this._setUpdateStrategy(validatorOrOpts);
this._setUpControls();
this.updateValueAndValidity({
onlySelf: true,
// If `asyncValidator` is present, it will trigger control status change from `PENDING` to
// `VALID` or `INVALID`. The status should be broadcasted via the `statusChanges` observable,
// so we set `emitEvent` to `true` to allow that during the control creation process.
emitEvent: !!asyncValidator,
});
}
/**
* Registers a control with the group's list of controls.
*
* This method does not update the value or validity of the control.
* Use {@link FormGroup#addControl addControl} instead.
*
* @param name The control name to register in the collection
* @param control Provides the control for the given name
*/
registerControl(name, control) {
if (this.controls[name])
return this.controls[name];
this.controls[name] = control;
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
return control;
}
/**
* Add a control to this group.
*
* This method also updates the value and validity of the control.
*
* @param name The control name to add to the collection
* @param control Provides the control for the given name
*/
addControl(name, control) {
this.registerControl(name, control);
this.updateValueAndValidity();
this._onCollectionChange();
}
/**
* Remove a control from this group.
*
* @param name The control name to remove from the collection
*/
removeControl(name) {
if (this.controls[name]) {
this.controls[name]._registerOnCollectionChange(() => { });
}
delete this.controls[name];
this.updateValueAndValidity();
this._onCollectionChange();
}
/**
* Replace an existing control.
*
* @param name The control name to replace in the collection
* @param control Provides the control for the given name
*/
setControl(name, control) {
if (this.controls[name]) {
this.controls[name]._registerOnCollectionChange(() => { });
}
delete this.controls[name];
if (control)
this.registerControl(name, control);
this.updateValueAndValidity();
this._onCollectionChange();
}
/**
* Check whether there is an enabled control with the given name in the group.
*
* Reports false for disabled controls. If you'd like to check for existence in the group
* only, use {@link AbstractControl#get get} instead.
*
* @param controlName The control name to check for existence in the collection
*
* @returns false for disabled controls, true otherwise.
*/
contains(controlName) {
return (Object.prototype.hasOwnProperty.call(this.controls, controlName)
&& this.controls[controlName].enabled);
}
/**
* Sets the value of the `FormGroup`. It accepts an object that matches
* the structure of the group, with control names as keys.
*
* @usageNotes
* ### Set the complete value for the form group
*
* ```
* 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'}
* ```
*
* @throws When strict checks fail, such as setting the value of a control
* that doesn't exist or if you exclude a value of a control that does exist.
*
* @param value The new value for the control that matches the structure of the group.
* @param options Configuration options that determine how the control propagates changes
* and emits events after the value changes.
* The configuration options are passed to the {@link AbstractControl#updateValueAndValidity
* updateValueAndValidity} method.
*
*/
setValue(value, options = {}) {
this._checkAllValuesPresent(value);
Object.keys(value).forEach((name) => {
this._throwIfControlMissing(name);
this.controls[name].setValue(value[name], {
onlySelf: true,
emitEvent: options.emitEvent,
});
});
this.updateValueAndValidity(options);
}
/**
* Patches the value of the `FormGroup`. It accepts an object with control
* names as keys, and does 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.
*
* @usageNotes
* ### Patch the value for a form group
*
* ```
* 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}
* ```
*
* @param value The object that matches the structure of the group.
* @param options Configuration options that determine how the control propagates changes and
* emits events after the value is patched.
* The configuration options are passed to the {@link AbstractControl#updateValueAndValidity
* updateValueAndValidity} method.
*/
patchValue(value, options = {}) {
Object.keys(value).forEach((name) => {
if (this.controls[name]) {
this.controls[name].patchValue(value[name], {
onlySelf: true,
emitEvent: options.emitEvent,
});
}
});
this.updateValueAndValidity(options);
}
/**
* Resets the `FormGroup`, marks all descendants are marked `pristine` and `untouched`, and
* the value of all descendants to null.
*
* You 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
* is a standalone value or a form state object with both a value and a disabled
* status.
*
* @param value Resets the control with an initial value,
* or an object that defines the initial value and disabled state.
*
* @param options Configuration options that determine how the control propagates changes
* and emits events when the group is reset.
* The configuration options are passed to the {@link AbstractControl#updateValueAndValidity
* updateValueAndValidity} method.
*
* @usageNotes
*
* ### Reset the form group values
*
* ```ts
* const form = new FormGroup({
* first: new FormControl('first name'),
* last: new FormControl('last name')
* });
*
* console.log(form.value); // {first: 'first name', last: 'last name'}
*
* form.reset({ first: 'name', last: 'last name' });
*
* console.log(form.value); // {first: 'name', last: 'last name'}
* ```
*
* ### Reset the form group values and disabled status
*
* ```
* const form = new FormGroup({
* first: new FormControl('first name'),
* last: new FormControl('last name')
* });
*
* 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 = {}, options = {}) {
this._forEachChild((control, name) => {
control.reset(value[name], {
onlySelf: true,
emitEvent: options.emitEvent,
});
});
this._updatePristine(options);
this._updateTouched(options);
this.updateValueAndValidity(options);
}
/**
* The aggregate value of the `FormGroup`, including any disabled controls.
*
* Retrieves all values regardless of disabled status.
* The `value` property is the best way to get the value of the group, because
* it excludes disabled controls in the `FormGroup`.
*/
getRawValue() {
return this._reduceChildren({}, (acc, control, name) => {
acc[name] = control instanceof FormControl
? control.value
: control.getRawValue();
return acc;
});
}
/** @internal */
_syncPendingControls() {
const subtreeUpdated = this._reduceChildren(false, (updated, child) => (child._syncPendingControls() ? true : updated));
if (subtreeUpdated)
this.updateValueAndValidity({ onlySelf: true });
return subtreeUpdated;
}
/** @internal */
_throwIfControlMissing(name) {
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 */
_forEachChild(cb) {
Object.keys(this.controls).forEach(k => cb(this.controls[k], k));
}
/** @internal */
_setUpControls() {
this._forEachChild((control) => {
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
});
}
/** @internal */
_updateValue() {
this.value = this._reduceValue();
}
/** @internal */
_anyControls(condition) {
const controlNames = Object.keys(this.controls);
return controlNames.some((controlName) => {
const control = this.controls[controlName];
if (this.contains(controlName) && condition(control)) {
return true;
}
return false;
});
}
/** @internal */
_reduceValu