UNPKG

forms-reactive

Version:

Reactive Form Web Component

1,444 lines 64.4 kB
/* 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