refactor(forms): Split up model.ts. (#45217)

model.ts is currently extremely large. This is the first step in an attempt to refactor it to be more easily navigable and reviewable. This commit breaks up `model.ts` into the following new files:

* `model/abstract_model.ts`: The remainder of the model, including the `AbstractControl` base class and helper functions which are used throughout.
* `model/form_control.ts`: `FormControl`, `FormControlOptions`, and helpers, plus the constructor and untyped friends.
* `model/form_array.ts`: `FormArray` and untyped friends.
* `model/form_group.ts`: `FormGroup` and untyped friends.

This first phase is a purely mechanical code move. There is no new code at all, and no interfaces have been separated.

PR Close #45217
This commit is contained in:
Dylan Hunn 2022-02-28 12:52:35 -08:00 committed by Jessica Janiuk
parent c5df474c7c
commit c8e4d62882
24 changed files with 2712 additions and 2667 deletions

View file

@ -138,25 +138,11 @@
"packages/core/testing/src/test_bed.ts"
],
[
"packages/forms/src/directives.ts",
"packages/forms/src/directives/ng_control_status.ts",
"packages/forms/src/directives/abstract_control_directive.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts"
],
[
"packages/forms/src/directives/abstract_control_directive.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/control_container.ts"
],
[
"packages/forms/src/directives/abstract_control_directive.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/control_container.ts",
"packages/forms/src/directives/form_interface.ts",
"packages/forms/src/directives/ng_control.ts"
"packages/forms/src/model/abstract_model.ts",
"packages/forms/src/forms.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
@ -165,10 +151,7 @@
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/control_container.ts",
"packages/forms/src/directives/form_interface.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts"
"packages/forms/src/directives/form_interface.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
@ -176,13 +159,59 @@
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts"
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/control_container.ts",
"packages/forms/src/directives/form_interface.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/ng_control.ts",
"packages/forms/src/directives/control_container.ts",
"packages/forms/src/directives/form_interface.ts",
"packages/forms/src/directives/ng_control.ts"
"packages/forms/src/directives/form_interface.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/control_container.ts",
"packages/forms/src/directives/form_interface.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/reactive_directives/form_group_directive.ts",
"packages/forms/src/directives/control_container.ts",
"packages/forms/src/directives/form_interface.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/reactive_directives/form_group_directive.ts",
"packages/forms/src/directives/form_interface.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/reactive_directives/form_group_directive.ts",
"packages/forms/src/directives/reactive_directives/form_control_name.ts"
],
[
"packages/forms/src/directives/abstract_form_group_directive.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/reactive_directives/form_group_directive.ts",
"packages/forms/src/directives/reactive_directives/form_control_name.ts",
"packages/forms/src/directives/control_container.ts",
"packages/forms/src/directives/form_interface.ts"
],
[
"packages/forms/src/directives/ng_form.ts",
@ -200,14 +229,6 @@
"packages/forms/src/directives/reactive_directives/form_group_directive.ts",
"packages/forms/src/directives/reactive_directives/form_control_name.ts"
],
[
"packages/forms/src/directives/reactive_directives/form_control_directive.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/reactive_directives/form_group_directive.ts",
"packages/forms/src/directives/reactive_directives/form_control_name.ts"
],
[
"packages/forms/src/directives/reactive_directives/form_control_name.ts",
"packages/forms/src/directives/reactive_directives/form_group_directive.ts"
@ -223,13 +244,6 @@
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/reactive_directives/form_group_directive.ts"
],
[
"packages/forms/src/directives/reactive_directives/form_control_name.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/reactive_directives/form_group_directive.ts"
],
[
"packages/forms/src/directives/reactive_directives/form_group_directive.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts"
@ -239,40 +253,23 @@
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts"
],
[
"packages/forms/src/directives/reactive_directives/form_group_directive.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts",
"packages/forms/src/directives/reactive_directives/form_group_name.ts"
],
[
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/directives/shared.ts"
],
[
"packages/forms/src/directives/reactive_directives/form_group_name.ts",
"packages/forms/src/model.ts",
"packages/forms/src/directives/shared.ts"
],
[
"packages/forms/src/directives/shared.ts",
"packages/forms/src/model.ts"
],
[
"packages/forms/src/directives/shared.ts",
"packages/forms/src/validators.ts",
"packages/forms/src/directives/validators.ts",
"packages/forms/src/model.ts"
],
[
"packages/forms/src/directives/shared.ts",
"packages/forms/src/validators.ts",
"packages/forms/src/model.ts"
"packages/forms/src/model/abstract_model.ts"
],
[
"packages/forms/src/directives/validators.ts",
"packages/forms/src/validators.ts"
],
[
"packages/forms/src/directives/validators.ts",
"packages/forms/src/validators.ts",
"packages/forms/src/model/abstract_model.ts"
],
[
"packages/router/src/directives/router_outlet.ts",
"packages/router/src/router_outlet_context.ts"

View file

@ -1115,9 +1115,6 @@
{
"name": "isFormControl"
},
{
"name": "isFormGroup"
},
{
"name": "isForwardRef"
},

View file

@ -1076,9 +1076,6 @@
{
"name": "isDirectiveHost"
},
{
"name": "isFormGroup"
},
{
"name": "isForwardRef"
},

View file

@ -8,7 +8,7 @@
import {Observable} from 'rxjs';
import {AbstractControl} from '../model';
import {AbstractControl} from '../model/abstract_model';
import {composeAsyncValidators, composeValidators} from '../validators';
import {AsyncValidator, AsyncValidatorFn, ValidationErrors, Validator, ValidatorFn} from './validators';

View file

@ -8,7 +8,7 @@
import {Directive, OnDestroy, OnInit} from '@angular/core';
import {FormGroup} from '../model';
import {FormGroup} from '../model/form_group';
import {ControlContainer} from './control_container';
import {Form} from './form_interface';

View file

@ -9,7 +9,6 @@
import {AbstractControlDirective} from './abstract_control_directive';
import {Form} from './form_interface';
/**
* @description
* A base class for directives that contain multiple registered instances of `NgControl`.

View file

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {FormControl, FormGroup} from '../model';
import {FormControl} from '../model/form_control';
import {FormGroup} from '../model/form_group';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
import {NgControl} from './ng_control';

View file

@ -8,7 +8,9 @@
import {AfterViewInit, Directive, EventEmitter, forwardRef, Inject, Input, Optional, Self} from '@angular/core';
import {AbstractControl, FormControl, FormGroup, FormHooks} from '../model';
import {AbstractControl, FormHooks} from '../model/abstract_model';
import {FormControl} from '../model/form_control';
import {FormGroup} from '../model/form_group';
import {composeAsyncValidators, composeValidators, NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {ControlContainer} from './control_container';

View file

@ -8,7 +8,8 @@
import {ChangeDetectorRef, Directive, EventEmitter, forwardRef, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, ɵcoerceToBoolean as coerceToBoolean} from '@angular/core';
import {FormControl, FormHooks} from '../model';
import {FormHooks} from '../model/abstract_model';
import {FormControl} from '../model/form_control';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';

View file

@ -8,7 +8,7 @@
import {Directive, EventEmitter, forwardRef, Inject, InjectionToken, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges} from '@angular/core';
import {FormControl} from '../../model';
import {FormControl} from '../../model/form_control';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor';
import {NgControl} from '../ng_control';

View file

@ -8,7 +8,7 @@
import {Directive, EventEmitter, forwardRef, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, SkipSelf} from '@angular/core';
import {FormControl} from '../../model';
import {FormControl} from '../../model/form_control';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {AbstractFormGroupDirective} from '../abstract_form_group_directive';
import {ControlContainer} from '../control_container';

View file

@ -8,12 +8,15 @@
import {Directive, EventEmitter, forwardRef, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges} from '@angular/core';
import {FormArray, FormControl, FormGroup, isFormControl} from '../../model';
import {FormArray} from '../../model/form_array';
import {FormControl, isFormControl} from '../../model/form_control';
import {FormGroup} from '../../model/form_group';
import {removeListItem} from '../../util';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {ControlContainer} from '../control_container';
import {Form} from '../form_interface';
import {missingFormException} from '../reactive_errors';
import {cleanUpControl, cleanUpFormContainer, cleanUpValidators, removeListItem, setUpControl, setUpFormContainer, setUpValidators, syncPendingControls} from '../shared';
import {cleanUpControl, cleanUpFormContainer, cleanUpValidators, setUpControl, setUpFormContainer, setUpValidators, syncPendingControls} from '../shared';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from '../validators';
import {FormControlName} from './form_control_name';

View file

@ -8,7 +8,7 @@
import {Directive, forwardRef, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf} from '@angular/core';
import {FormArray} from '../../model';
import {FormArray} from '../../model/form_array';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {AbstractFormGroupDirective} from '../abstract_form_group_directive';
import {ControlContainer} from '../control_container';

View file

@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbstractControl, FormArray, FormControl, FormGroup} from '../model';
import {AbstractControl} from '../model/abstract_model';
import {FormArray} from '../model/form_array';
import {FormControl} from '../model/form_control';
import {FormGroup} from '../model/form_group';
import {getControlAsyncValidators, getControlValidators, mergeValidators} from '../validators';
import {AbstractControlDirective} from './abstract_control_directive';
@ -347,11 +350,6 @@ export function selectValueAccessor(
return null;
}
export function removeListItem<T>(list: T[], el: T): void {
const index = list.indexOf(el);
if (index > -1) list.splice(index, 1);
}
// TODO(kara): remove after deprecation period
export function _ngModelWarning(
name: string, type: {_ngModelWarningSentOnce: boolean},

View file

@ -9,7 +9,7 @@
import {Directive, forwardRef, Input, OnChanges, SimpleChanges, StaticProvider, ɵcoerceToBoolean as coerceToBoolean} from '@angular/core';
import {Observable} from 'rxjs';
import {AbstractControl} from '../model';
import {AbstractControl} from '../model/abstract_model';
import {emailValidator, maxLengthValidator, maxValidator, minLengthValidator, minValidator, NG_VALIDATORS, nullValidator, patternValidator, requiredTrueValidator, requiredValidator} from '../validators';
/**

View file

@ -10,7 +10,10 @@ import {Injectable} from '@angular/core';
import {AsyncValidatorFn, ValidatorFn} from './directives/validators';
import {ReactiveFormsModule} from './form_providers';
import {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormGroup, FormHooks, isFormArray, isFormControl, isFormGroup} from './model';
import {AbstractControl, AbstractControlOptions, FormHooks} from './model/abstract_model';
import {FormArray, isFormArray} from './model/form_array';
import {FormControl, FormControlOptions, isFormControl} from './model/form_control';
import {FormGroup, isFormGroup} from './model/form_group';
function isAbstractControlOptions(options: AbstractControlOptions|
{[key: string]: any}): options is AbstractControlOptions {

View file

@ -42,7 +42,10 @@ export {NgSelectOption, SelectControlValueAccessor} from './directives/select_co
export {SelectMultipleControlValueAccessor, ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor';
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
export {FormBuilder} from './form_builder';
export {AbstractControl, AbstractControlOptions, FormArray, FormControl, FormControlOptions, FormControlStatus, FormGroup, UntypedFormArray, UntypedFormControl, UntypedFormGroup, ɵFormControlCtor} from './model';
export {AbstractControl, AbstractControlOptions, FormControlStatus} from './model/abstract_model';
export {FormArray, UntypedFormArray} from './model/form_array';
export {FormControl, FormControlOptions, UntypedFormControl, ɵFormControlCtor} from './model/form_control';
export {FormGroup, UntypedFormGroup} from './model/form_group';
export {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from './validators';
export {VERSION} from './version';

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,499 @@
/**
* @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
*/
import {AsyncValidatorFn, ValidatorFn} from '../directives/validators';
import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators} from './abstract_model';
/**
* Tracks the value and validity state of an array of `FormControl`,
* `FormGroup` or `FormArray` instances.
*
* A `FormArray` aggregates the values of each child `FormControl` into an array.
* It calculates its status by reducing the status values of its children. For example, if one of
* the controls in a `FormArray` is invalid, the entire array becomes invalid.
*
* `FormArray` is one of the three fundamental building blocks used to define forms in Angular,
* along with `FormControl` and `FormGroup`.
*
* @usageNotes
*
* ### Create an array of form controls
*
* ```
* const arr = new FormArray([
* new FormControl('Nancy', Validators.minLength(2)),
* new FormControl('Drew'),
* ]);
*
* console.log(arr.value); // ['Nancy', 'Drew']
* console.log(arr.status); // 'VALID'
* ```
*
* ### Create a form array with array-level validators
*
* You include array-level validators and async validators. These come in handy
* when you want to perform validation that considers the value of more than one child
* control.
*
* The two types of validators are passed in separately as the second and third arg
* respectively, or together as part of an options object.
*
* ```
* const arr = new FormArray([
* new FormControl('Nancy'),
* new FormControl('Drew')
* ], {validators: myValidator, asyncValidators: myAsyncValidator});
* ```
*
* ### Set the updateOn property for all controls in a form array
*
* 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
* array level, all child controls default to 'blur', unless the child
* has explicitly specified a different `updateOn` value.
*
* ```ts
* const arr = new FormArray([
* new FormControl()
* ], {updateOn: 'blur'});
* ```
*
* ### Adding or removing controls from a form array
*
* To change the controls in the array, use the `push`, `insert`, `removeAt` or `clear` methods
* in `FormArray` itself. These methods ensure the controls are properly tracked in the
* form's hierarchy. Do not modify the array of `AbstractControl`s used to instantiate
* the `FormArray` directly, as that result in strange and unexpected behavior such
* as broken change detection.
*
* @publicApi
*/
export class FormArray extends AbstractControl {
/**
* Creates a new `FormArray` instance.
*
* @param controls An array of child controls. Each child control is given an index
* where 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(
public controls: AbstractControl[],
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) {
super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
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: !!this.asyncValidator
});
}
/**
* Get the `AbstractControl` at the given `index` in the array.
*
* @param index Index in the array to retrieve the control. If `index` is negative, it will wrap
* around from the back, and if index is greatly negative (less than `-length`), the result is
* undefined. This behavior is the same as `Array.at(index)`.
*/
at(index: number): AbstractControl {
return this.controls[this._adjustIndex(index)];
}
/**
* Insert a new `AbstractControl` at the end of the array.
*
* @param control Form control to be inserted
* @param options Specifies whether this FormArray instance should emit events after a new
* control is added.
* * `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
* inserted. When false, no events are emitted.
*/
push(control: AbstractControl, options: {emitEvent?: boolean} = {}): void {
this.controls.push(control);
this._registerControl(control);
this.updateValueAndValidity({emitEvent: options.emitEvent});
this._onCollectionChange();
}
/**
* Insert a new `AbstractControl` at the given `index` in the array.
*
* @param index Index in the array to insert the control. If `index` is negative, wraps around
* from the back. If `index` is greatly negative (less than `-length`), prepends to the array.
* This behavior is the same as `Array.splice(index, 0, control)`.
* @param control Form control to be inserted
* @param options Specifies whether this FormArray instance should emit events after a new
* control is inserted.
* * `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
* inserted. When false, no events are emitted.
*/
insert(index: number, control: AbstractControl, options: {emitEvent?: boolean} = {}): void {
this.controls.splice(index, 0, control);
this._registerControl(control);
this.updateValueAndValidity({emitEvent: options.emitEvent});
}
/**
* Remove the control at the given `index` in the array.
*
* @param index Index in the array to remove the control. If `index` is negative, wraps around
* from the back. If `index` is greatly negative (less than `-length`), removes the first
* element. This behavior is the same as `Array.splice(index, 1)`.
* @param options Specifies whether this FormArray instance should emit events after a
* control is removed.
* * `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
* removed. When false, no events are emitted.
*/
removeAt(index: number, options: {emitEvent?: boolean} = {}): void {
// Adjust the index, then clamp it at no less than 0 to prevent undesired underflows.
let adjustedIndex = this._adjustIndex(index);
if (adjustedIndex < 0) adjustedIndex = 0;
if (this.controls[adjustedIndex])
this.controls[adjustedIndex]._registerOnCollectionChange(() => {});
this.controls.splice(adjustedIndex, 1);
this.updateValueAndValidity({emitEvent: options.emitEvent});
}
/**
* Replace an existing control.
*
* @param index Index in the array to replace the control. If `index` is negative, wraps around
* from the back. If `index` is greatly negative (less than `-length`), replaces the first
* element. This behavior is the same as `Array.splice(index, 1, control)`.
* @param control The `AbstractControl` control to replace the existing control
* @param options Specifies whether this FormArray instance should emit events after an
* existing control is replaced with a new one.
* * `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
* replaced with a new one. When false, no events are emitted.
*/
setControl(index: number, control: AbstractControl, options: {emitEvent?: boolean} = {}): void {
// Adjust the index, then clamp it at no less than 0 to prevent undesired underflows.
let adjustedIndex = this._adjustIndex(index);
if (adjustedIndex < 0) adjustedIndex = 0;
if (this.controls[adjustedIndex])
this.controls[adjustedIndex]._registerOnCollectionChange(() => {});
this.controls.splice(adjustedIndex, 1);
if (control) {
this.controls.splice(adjustedIndex, 0, control);
this._registerControl(control);
}
this.updateValueAndValidity({emitEvent: options.emitEvent});
this._onCollectionChange();
}
/**
* Length of the control array.
*/
get length(): number {
return this.controls.length;
}
/**
* Sets the value of the `FormArray`. It accepts an array that matches
* the structure of the control.
*
* This method performs strict checks, and throws 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.
*
* @usageNotes
* ### Set the values for the controls in the form array
*
* ```
* const arr = new FormArray([
* new FormControl(),
* new FormControl()
* ]);
* console.log(arr.value); // [null, null]
*
* arr.setValue(['Nancy', 'Drew']);
* console.log(arr.value); // ['Nancy', 'Drew']
* ```
*
* @param value Array of values for the controls
* @param options Configure options that determine how the control propagates changes and
* emits events after the value changes
*
* * `onlySelf`: When true, each change only affects this control, and not its parent. 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 value is updated.
* When false, no events are emitted.
* The configuration options are passed to the {@link AbstractControl#updateValueAndValidity
* updateValueAndValidity} method.
*/
override setValue(value: any[], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
assertAllValuesPresent(this, false, value);
value.forEach((newValue: any, index: number) => {
assertControlPresent(this, false, index);
this.at(index).setValue(newValue, {onlySelf: true, emitEvent: options.emitEvent});
});
this.updateValueAndValidity(options);
}
/**
* Patches the value of the `FormArray`. It accepts an array that matches the
* structure of the control, 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 array without throwing an error.
*
* @usageNotes
* ### Patch the values for controls in a form array
*
* ```
* const arr = new FormArray([
* new FormControl(),
* new FormControl()
* ]);
* console.log(arr.value); // [null, null]
*
* arr.patchValue(['Nancy']);
* console.log(arr.value); // ['Nancy', null]
* ```
*
* @param value Array of latest values for the controls
* @param options Configure options that determine how the control propagates changes and
* emits events after the value changes
*
* * `onlySelf`: When true, each change only affects this control, and not its parent. 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 value
* is updated. When false, no events are emitted. The configuration options are passed to
* the {@link AbstractControl#updateValueAndValidity updateValueAndValidity} method.
*/
override patchValue(value: any[], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
// Even though the `value` argument type doesn't allow `null` and `undefined` values, the
// `patchValue` can be called recursively and inner data structures might have these values, so
// we just ignore such cases when a field containing FormArray instance receives `null` or
// `undefined` as a value.
if (value == null /* both `null` and `undefined` */) return;
value.forEach((newValue: any, index: number) => {
if (this.at(index)) {
this.at(index).patchValue(newValue, {onlySelf: true, emitEvent: options.emitEvent});
}
});
this.updateValueAndValidity(options);
}
/**
* Resets the `FormArray` and all descendants are marked `pristine` and `untouched`, and the
* value of all descendants to null or null maps.
*
* You reset to a specific form state by passing in an array of states
* that matches the structure of the control. The state is a standalone value
* or a form state object with both a value and a disabled status.
*
* @usageNotes
* ### Reset the values in a form array
*
* ```ts
* const arr = new FormArray([
* new FormControl(),
* new FormControl()
* ]);
* arr.reset(['name', 'last name']);
*
* console.log(arr.value); // ['name', 'last name']
* ```
*
* ### Reset the values in a form array and the disabled status for the first control
*
* ```
* arr.reset([
* {value: 'name', disabled: true},
* 'last'
* ]);
*
* console.log(arr.value); // ['last']
* console.log(arr.at(0).status); // 'DISABLED'
* ```
*
* @param value Array of values for the controls
* @param options Configure options that determine how the control propagates changes and
* emits events after the value changes
*
* * `onlySelf`: When true, each change only affects this control, and not its parent. 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 reset.
* When false, no events are emitted.
* The configuration options are passed to the {@link AbstractControl#updateValueAndValidity
* updateValueAndValidity} method.
*/
override reset(value: any = [], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._forEachChild((control: AbstractControl, index: number) => {
control.reset(value[index], {onlySelf: true, emitEvent: options.emitEvent});
});
this._updatePristine(options);
this._updateTouched(options);
this.updateValueAndValidity(options);
}
/**
* The aggregate value of the array, including any disabled controls.
*
* Reports all values regardless of disabled status.
* For enabled controls only, the `value` property is the best way to get the value of the array.
*/
override getRawValue(): any[] {
return this.controls.map((control: AbstractControl) => control.getRawValue());
}
/**
* Remove all controls in the `FormArray`.
*
* @param options Specifies whether this FormArray instance should emit events after all
* controls are removed.
* * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
* `valueChanges` observables emit events with the latest status and value when all controls
* in this FormArray instance are removed. When false, no events are emitted.
*
* @usageNotes
* ### Remove all elements from a FormArray
*
* ```ts
* const arr = new FormArray([
* new FormControl(),
* new FormControl()
* ]);
* console.log(arr.length); // 2
*
* arr.clear();
* console.log(arr.length); // 0
* ```
*
* It's a simpler and more efficient alternative to removing all elements one by one:
*
* ```ts
* const arr = new FormArray([
* new FormControl(),
* new FormControl()
* ]);
*
* while (arr.length) {
* arr.removeAt(0);
* }
* ```
*/
clear(options: {emitEvent?: boolean} = {}): void {
if (this.controls.length < 1) return;
this._forEachChild((control: AbstractControl) => control._registerOnCollectionChange(() => {}));
this.controls.splice(0);
this.updateValueAndValidity({emitEvent: options.emitEvent});
}
/**
* Adjusts a negative index by summing it with the length of the array. For very negative indices,
* the result may remain negative.
* @internal
*/
private _adjustIndex(index: number): number {
return index < 0 ? index + this.length : index;
}
/** @internal */
override _syncPendingControls(): boolean {
let subtreeUpdated = this.controls.reduce((updated: boolean, child: AbstractControl) => {
return child._syncPendingControls() ? true : updated;
}, false);
if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true});
return subtreeUpdated;
}
/** @internal */
override _forEachChild(cb: (c: AbstractControl, index: number) => void): void {
this.controls.forEach((control: AbstractControl, index: number) => {
cb(control, index);
});
}
/** @internal */
override _updateValue(): void {
(this as {value: any}).value =
this.controls.filter((control) => control.enabled || this.disabled)
.map((control) => control.value);
}
/** @internal */
override _anyControls(condition: (c: AbstractControl) => boolean): boolean {
return this.controls.some((control: AbstractControl) => control.enabled && condition(control));
}
/** @internal */
_setUpControls(): void {
this._forEachChild((control: AbstractControl) => this._registerControl(control));
}
/** @internal */
override _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);
}
/** @internal */
override _find(name: string|number): AbstractControl|null {
return this.at(name as number) ?? null;
}
}
interface UntypedFormArrayCtor {
new(controls: AbstractControl[],
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): UntypedFormArray;
/**
* The presence of an explicit `prototype` property provides backwards-compatibility for apps that
* manually inspect the prototype chain.
*/
prototype: FormArray;
}
/**
* UntypedFormArray is a non-strongly-typed version of @see FormArray.
* Note: this is used for migration purposes only. Please avoid using it directly in your code and
* prefer `FormControl` instead, unless you have been migrated to it automatically.
*/
export type UntypedFormArray = FormArray;
export const UntypedFormArray: UntypedFormArrayCtor = FormArray;
export const isFormArray = (control: unknown): control is FormArray => control instanceof FormArray;

View file

@ -0,0 +1,478 @@
/**
* @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
*/
import {AsyncValidatorFn, ValidatorFn} from '../directives/validators';
import {removeListItem} from '../util';
import {AbstractControl, AbstractControlOptions, isOptionsObj, pickAsyncValidators, pickValidators} from './abstract_model';
/**
* Interface for options provided to a {@link FormControl}.
*
* This interface extends all options from {@link AbstractControlOptions}, plus some options
* unique to `FormControl`.
*
* @publicApi
*/
export interface FormControlOptions extends AbstractControlOptions {
/**
* @description
* Whether to use the initial value used to construct the {@link FormControl} as its default value
* as well. If this option is false or not provided, the default value of a FormControl is `null`.
* When a FormControl is reset without an explicit value, its value reverts to
* its default value.
*/
initialValueIsDefault?: boolean;
}
/**
* 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)
*
* @publicApi
*
* @overriddenImplementation ɵFormControlCtor
*
* @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 synchronous 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'
* ```
*/
export interface FormControl extends AbstractControl {
/**
* The default value of this FormControl, used whenever the control is reset without an explicit
* value. See {@link FormControlOptions#initialValueIsDefault} for more information on configuring
* a default value.
*/
readonly defaultValue: any;
/** @internal */
_onChange: Function[];
/**
* This field holds a pending value that has not yet been applied to the form's value.
* It is `any` because the value is untyped.
* @internal
*/
_pendingValue: any;
/** @internal */
_pendingChange: boolean;
/**
* 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.
*
* * `onlySelf`: When true, each change only affects this control, and not its parent. 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 value is updated.
* When false, no events are emitted.
* * `emitModelToViewChange`: When true or not supplied (the default), each change triggers an
* `onChange` event to
* update the view.
* * `emitViewToModelChange`: When true or not supplied (the default), each change triggers an
* `ngModelChange`
* event to update the model.
*
*/
setValue(value: any, options?: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
}): void;
/**
* 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: any, options?: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
}): void;
/**
* Resets the form control, marking it `pristine` and `untouched`, and resetting
* the value. The new value will be the provided value (if passed), `null`, or the initial value
* if `initialValueIsDefault` was set in the constructor via {@link FormControlOptions}.
*
* ```ts
* // By default, the control will reset to null.
* const dog = new FormControl('spot');
* dog.reset(); // dog.value is null
*
* // If this flag is set, the control will instead reset to the initial value.
* const cat = new FormControl('tabby', {initialValueIsDefault: true});
* cat.reset(); // cat.value is "tabby"
*
* // A value passed to reset always takes precedence.
* const fish = new FormControl('finn', {initialValueIsDefault: true});
* fish.reset('bubble'); // fish.value is "bubble"
* ```
*
* @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.
*
* * `onlySelf`: When true, each change only affects this control, and not its parent. 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 reset.
* When false, no events are emitted.
*
*/
reset(formState?: any, options?: {onlySelf?: boolean, emitEvent?: boolean}): void;
/**
* @internal
*/
_updateValue(): void;
/**
* @internal
*/
_anyControls(condition: (c: AbstractControl) => boolean): boolean;
/**
* @internal
*/
_allControlsDisabled(): boolean;
/**
* Register a listener for change events.
*
* @param fn The method that is called when the value changes
*/
registerOnChange(fn: Function): void;
/**
* Internal function to unregister a change events listener.
* @internal
*/
_unregisterOnChange(fn: (value?: any, emitModelEvent?: boolean) => void): void;
/**
* Register a listener for disabled events.
*
* @param fn The method that is called when the disabled status changes.
*/
registerOnDisabledChange(fn: (isDisabled: boolean) => void): void;
/**
* Internal function to unregister a disabled event listener.
* @internal
*/
_unregisterOnDisabledChange(fn: (isDisabled: boolean) => void): void;
/**
* @internal
*/
_forEachChild(cb: (c: AbstractControl) => void): void;
/** @internal */
_syncPendingControls(): boolean;
}
type FormControlInterface = FormControl;
/**
* Various available constructors for `FormControl`.
* Do not use this interface directly. Instead, use `FormControl`:
* ```
* const fc = new FormControl('foo');
* ```
* This symbol is prefixed with ɵ to make plain that it is an internal symbol.
*/
export interface ɵFormControlCtor {
/**
* Construct a FormControl with no initial value or validators.
*/
new(): FormControl;
/**
* 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 a `FormControlOptions` object that contains validation functions
* and a validation trigger.
*
* @param asyncValidator A single async validator or array of async validator functions.
*/
new(formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl;
/**
* The presence of an explicit `prototype` property provides backwards-compatibility for apps that
* manually inspect the prototype chain.
*/
prototype: FormControl;
}
export const FormControl: ɵFormControlCtor =
(class FormControl extends AbstractControl implements FormControlInterface {
/** @publicApi */
public readonly defaultValue: any = null;
/** @internal */
_onChange: Function[] = [];
/** @internal */
_pendingValue: any;
/** @internal */
_pendingChange: boolean = false;
constructor(
formState: any = null,
validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) {
super(
pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
this._applyFormState(formState);
this._setUpdateStrategy(validatorOrOpts);
this._initObservables();
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: !!this.asyncValidator
});
if (isOptionsObj(validatorOrOpts) && validatorOrOpts.initialValueIsDefault) {
if (this._isBoxedValue(formState)) {
(this.defaultValue as any) = formState.value;
} else {
(this.defaultValue as any) = formState;
}
}
}
override setValue(value: any, options: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
} = {}): void {
(this as {value: any}).value = this._pendingValue = value;
if (this._onChange.length && options.emitModelToViewChange !== false) {
this._onChange.forEach(
(changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
}
this.updateValueAndValidity(options);
}
override patchValue(value: any, options: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
} = {}): void {
this.setValue(value, options);
}
override reset(
formState: any = this.defaultValue,
options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._applyFormState(formState);
this.markAsPristine(options);
this.markAsUntouched(options);
this.setValue(this.value, options);
this._pendingChange = false;
}
/** @internal */
override _updateValue(): void {}
/** @internal */
override _anyControls(condition: (c: AbstractControl) => boolean): boolean {
return false;
}
/** @internal */
override _allControlsDisabled(): boolean {
return this.disabled;
}
registerOnChange(fn: Function): void {
this._onChange.push(fn);
}
/** @internal */
_unregisterOnChange(fn: (value?: any, emitModelEvent?: boolean) => void): void {
removeListItem(this._onChange, fn);
}
registerOnDisabledChange(fn: (isDisabled: boolean) => void): void {
this._onDisabledChange.push(fn);
}
/** @internal */
_unregisterOnDisabledChange(fn: (isDisabled: boolean) => void): void {
removeListItem(this._onDisabledChange, fn);
}
/** @internal */
override _forEachChild(cb: (c: AbstractControl) => void): void {}
/** @internal */
override _syncPendingControls(): boolean {
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;
}
private _applyFormState(formState: any) {
if (this._isBoxedValue(formState)) {
(this as {value: any}).value = this._pendingValue = formState.value;
formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) :
this.enable({onlySelf: true, emitEvent: false});
} else {
(this as {value: any}).value = this._pendingValue = formState;
}
}
});
interface UntypedFormControlCtor {
new(): UntypedFormControl;
new(formState?: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): UntypedFormControl;
/**
* The presence of an explicit `prototype` property provides backwards-compatibility for apps that
* manually inspect the prototype chain.
*/
prototype: FormControl;
}
/**
* UntypedFormControl is a non-strongly-typed version of @see FormControl.
* Note: this is used for migration purposes only. Please avoid using it directly in your code and
* prefer `FormControl` instead, unless you have been migrated to it automatically.
*/
export type UntypedFormControl = FormControl;
export const UntypedFormControl: UntypedFormControlCtor = FormControl;
export const isFormControl = (control: unknown): control is FormControl =>
control instanceof FormControl;

View file

@ -0,0 +1,482 @@
/**
* @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
*/
import {AsyncValidatorFn, ValidatorFn} from '../directives/validators';
import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators} from './abstract_model';
/**
* 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(
public controls: {[key: string]: AbstractControl},
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) {
super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts));
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: !!this.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: string, control: AbstractControl): AbstractControl {
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.
*
* If a control with a given name already exists, it would *not* be replaced with a new one.
* If you want to replace an existing control, use the {@link FormGroup#setControl setControl}
* method instead. 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
* @param options Specifies whether this FormGroup instance should emit events after a new
* control is added.
* * `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
* added. When false, no events are emitted.
*/
addControl(name: string, control: AbstractControl, options: {emitEvent?: boolean} = {}): void {
this.registerControl(name, control);
this.updateValueAndValidity({emitEvent: options.emitEvent});
this._onCollectionChange();
}
/**
* Remove a control from this group.
*
* This method also updates the value and validity of the control.
*
* @param name The control name to remove from the collection
* @param options Specifies whether this FormGroup instance should emit events after a
* control is removed.
* * `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
* removed. When false, no events are emitted.
*/
removeControl(name: string, options: {emitEvent?: boolean} = {}): void {
if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {});
delete (this.controls[name]);
this.updateValueAndValidity({emitEvent: options.emitEvent});
this._onCollectionChange();
}
/**
* Replace an existing control.
*
* If a control with a given name does not exist in this `FormGroup`, it will be added.
*
* @param name The control name to replace in the collection
* @param control Provides the control for the given name
* @param options Specifies whether this FormGroup instance should emit events after an
* existing control is replaced.
* * `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
* replaced with a new one. When false, no events are emitted.
*/
setControl(name: string, control: AbstractControl, options: {emitEvent?: boolean} = {}): void {
if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {});
delete (this.controls[name]);
if (control) this.registerControl(name, control);
this.updateValueAndValidity({emitEvent: options.emitEvent});
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: string): boolean {
return this.controls.hasOwnProperty(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.
*
* * `onlySelf`: When true, each change only affects this control, and not its parent. 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 value is updated.
* When false, no events are emitted.
*/
override setValue(
value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
assertAllValuesPresent(this, true, value);
Object.keys(value).forEach(name => {
assertControlPresent(this, true, 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.
* * `onlySelf`: When true, each change only affects this control and not its parent. Default is
* true.
* * `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 value
* is updated. When false, no events are emitted. The configuration options are passed to
* the {@link AbstractControl#updateValueAndValidity updateValueAndValidity} method.
*/
override patchValue(
value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
// Even though the `value` argument type doesn't allow `null` and `undefined` values, the
// `patchValue` can be called recursively and inner data structures might have these values, so
// we just ignore such cases when a field containing FormGroup instance receives `null` or
// `undefined` as a value.
if (value == null /* both `null` and `undefined` */) return;
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 `pristine` and `untouched` and sets
* 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.
* * `onlySelf`: When true, each change only affects this control, and not its parent. 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 reset.
* When false, no events are emitted.
* 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(form.value); // {last: 'last'}
* console.log(form.get('first').status); // 'DISABLED'
* ```
*/
override reset(value: any = {}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._forEachChild((control: AbstractControl, name: string) => {
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`.
*/
override getRawValue(): any {
return this._reduceChildren(
{}, (acc: {[k: string]: AbstractControl}, control: AbstractControl, name: string) => {
acc[name] = control.getRawValue();
return acc;
});
}
/** @internal */
override _syncPendingControls(): boolean {
let subtreeUpdated = this._reduceChildren(false, (updated: boolean, child: AbstractControl) => {
return child._syncPendingControls() ? true : updated;
});
if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true});
return subtreeUpdated;
}
/** @internal */
override _forEachChild(cb: (v: any, k: string) => void): void {
Object.keys(this.controls).forEach(key => {
// The list of controls can change (for ex. controls might be removed) while the loop
// is running (as a result of invoking Forms API in `valueChanges` subscription), so we
// have to null check before invoking the callback.
const control = this.controls[key];
control && cb(control, key);
});
}
/** @internal */
_setUpControls(): void {
this._forEachChild((control: AbstractControl) => {
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
});
}
/** @internal */
override _updateValue(): void {
(this as {value: any}).value = this._reduceValue();
}
/** @internal */
override _anyControls(condition: (c: AbstractControl) => boolean): boolean {
for (const controlName of Object.keys(this.controls)) {
const control = this.controls[controlName];
if (this.contains(controlName) && condition(control)) {
return true;
}
}
return false;
}
/** @internal */
_reduceValue() {
return this._reduceChildren(
{}, (acc: {[k: string]: any}, control: AbstractControl, name: string): any => {
if (control.enabled || this.disabled) {
acc[name] = control.value;
}
return acc;
});
}
/** @internal */
_reduceChildren<T>(initValue: T, fn: (acc: T, control: AbstractControl, name: string) => T): T {
let res = initValue;
this._forEachChild((control: AbstractControl, name: string) => {
res = fn(res, control, name);
});
return res;
}
/** @internal */
override _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;
}
/** @internal */
override _find(name: string|number): AbstractControl|null {
return this.controls.hasOwnProperty(name as string) ? this.controls[name as string] : null;
}
}
interface UntypedFormGroupCtor {
new(controls: {[key: string]: AbstractControl},
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): UntypedFormGroup;
/**
* The presence of an explicit `prototype` property provides backwards-compatibility for apps that
* manually inspect the prototype chain.
*/
prototype: FormGroup;
}
/**
* UntypedFormGroup is a non-strongly-typed version of @see FormGroup.
* Note: this is used for migration purposes only. Please avoid using it directly in your code and
* prefer `FormControl` instead, unless you have been migrated to it automatically.
*/
export type UntypedFormGroup = FormGroup;
export const UntypedFormGroup: UntypedFormGroupCtor = FormGroup;
export const isFormGroup = (control: unknown): control is FormGroup => control instanceof FormGroup;

View file

@ -0,0 +1,12 @@
/**
* @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
*/
export function removeListItem<T>(list: T[], el: T): void {
const index = list.indexOf(el);
if (index > -1) list.splice(index, 1);
}

View file

@ -11,7 +11,7 @@ import {forkJoin, from, Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {AsyncValidator, AsyncValidatorFn, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
import {AbstractControl} from './model';
import {AbstractControl} from './model/abstract_model';
function isEmptyInputValue(value: any): boolean {
/**