diff --git a/goldens/circular-deps/packages.json b/goldens/circular-deps/packages.json index b191d1edcd2..c5fd350bf98 100644 --- a/goldens/circular-deps/packages.json +++ b/goldens/circular-deps/packages.json @@ -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" diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index b9e2fb26ca9..f3d94e378d4 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -1115,9 +1115,6 @@ { "name": "isFormControl" }, - { - "name": "isFormGroup" - }, { "name": "isForwardRef" }, diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index 5eac6303230..56fb12a5fa2 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -1076,9 +1076,6 @@ { "name": "isDirectiveHost" }, - { - "name": "isFormGroup" - }, { "name": "isForwardRef" }, diff --git a/packages/forms/src/directives/abstract_control_directive.ts b/packages/forms/src/directives/abstract_control_directive.ts index e1fb2c6e02d..611fd41b758 100644 --- a/packages/forms/src/directives/abstract_control_directive.ts +++ b/packages/forms/src/directives/abstract_control_directive.ts @@ -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'; diff --git a/packages/forms/src/directives/abstract_form_group_directive.ts b/packages/forms/src/directives/abstract_form_group_directive.ts index 4c48a7a3bd2..1b405c7ba3e 100644 --- a/packages/forms/src/directives/abstract_form_group_directive.ts +++ b/packages/forms/src/directives/abstract_form_group_directive.ts @@ -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'; diff --git a/packages/forms/src/directives/control_container.ts b/packages/forms/src/directives/control_container.ts index 9c88665dd4a..4c47e42b90d 100644 --- a/packages/forms/src/directives/control_container.ts +++ b/packages/forms/src/directives/control_container.ts @@ -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`. diff --git a/packages/forms/src/directives/form_interface.ts b/packages/forms/src/directives/form_interface.ts index 88fea76293c..99624acc806 100644 --- a/packages/forms/src/directives/form_interface.ts +++ b/packages/forms/src/directives/form_interface.ts @@ -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'; diff --git a/packages/forms/src/directives/ng_form.ts b/packages/forms/src/directives/ng_form.ts index 71579b1cbde..244e8f891fe 100644 --- a/packages/forms/src/directives/ng_form.ts +++ b/packages/forms/src/directives/ng_form.ts @@ -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'; diff --git a/packages/forms/src/directives/ng_model.ts b/packages/forms/src/directives/ng_model.ts index 664f737ceb6..8bf9c3477c5 100644 --- a/packages/forms/src/directives/ng_model.ts +++ b/packages/forms/src/directives/ng_model.ts @@ -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'; diff --git a/packages/forms/src/directives/reactive_directives/form_control_directive.ts b/packages/forms/src/directives/reactive_directives/form_control_directive.ts index cada6a32d6e..cfd79754930 100644 --- a/packages/forms/src/directives/reactive_directives/form_control_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_control_directive.ts @@ -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'; diff --git a/packages/forms/src/directives/reactive_directives/form_control_name.ts b/packages/forms/src/directives/reactive_directives/form_control_name.ts index 8d900857b95..ca173614c5e 100644 --- a/packages/forms/src/directives/reactive_directives/form_control_name.ts +++ b/packages/forms/src/directives/reactive_directives/form_control_name.ts @@ -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'; diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index 50de9479385..c6670260939 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -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'; diff --git a/packages/forms/src/directives/reactive_directives/form_group_name.ts b/packages/forms/src/directives/reactive_directives/form_group_name.ts index b713a73fc64..48f0174691b 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_name.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_name.ts @@ -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'; diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index 18fa1460104..377965f492a 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -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(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}, diff --git a/packages/forms/src/directives/validators.ts b/packages/forms/src/directives/validators.ts index 154c560ec63..8a736a5a3b0 100644 --- a/packages/forms/src/directives/validators.ts +++ b/packages/forms/src/directives/validators.ts @@ -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'; /** diff --git a/packages/forms/src/form_builder.ts b/packages/forms/src/form_builder.ts index 0ac96ac290b..f33b6b39bfb 100644 --- a/packages/forms/src/form_builder.ts +++ b/packages/forms/src/form_builder.ts @@ -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 { diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index 17c10d873ba..626b7656eac 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -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'; diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts deleted file mode 100644 index 550e2abb33b..00000000000 --- a/packages/forms/src/model.ts +++ /dev/null @@ -1,2576 +0,0 @@ -/** - * @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 {EventEmitter, ɵRuntimeError as RuntimeError} from '@angular/core'; -import {Observable} from 'rxjs'; - -import {missingControlError, missingControlValueError, noControlsError} from './directives/reactive_errors'; -import {removeListItem} from './directives/shared'; -import {AsyncValidatorFn, ValidationErrors, ValidatorFn} from './directives/validators'; -import {RuntimeErrorCode} from './errors'; -import {addValidators, composeAsyncValidators, composeValidators, hasValidator, removeValidators, toObservable} from './validators'; - -const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode; - -/** - * Reports that a FormControl is valid, meaning that no errors exist in the input value. - * - * @see `status` - */ -export const VALID = 'VALID'; - -/** - * Reports that a FormControl is invalid, meaning that an error exists in the input value. - * - * @see `status` - */ -export const INVALID = 'INVALID'; - -/** - * Reports that a FormControl is pending, meaning that that async validation is occurring and - * errors are not yet available for the input value. - * - * @see `markAsPending` - * @see `status` - */ -export const PENDING = 'PENDING'; - -/** - * Reports that a FormControl is disabled, meaning that the control is exempt from ancestor - * calculations of validity or value. - * - * @see `markAsDisabled` - * @see `status` - */ -export const DISABLED = 'DISABLED'; - -/** - * A form can have several different statuses. Each - * possible status is returned as a string literal. - * - * * **VALID**: Reports that a FormControl is valid, meaning that no errors exist in the input - * value. - * * **INVALID**: Reports that a FormControl is invalid, meaning that an error exists in the input - * value. - * * **PENDING**: Reports that a FormControl is pending, meaning that that async validation is - * occurring and errors are not yet available for the input value. - * * **DISABLED**: Reports that a FormControl is - * disabled, meaning that the control is exempt from ancestor calculations of validity or value. - * - * @publicApi - */ -export type FormControlStatus = 'VALID'|'INVALID'|'PENDING'|'DISABLED'; - -/** - * Gets validators from either an options object or given validators. - */ -function pickValidators(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions| - null): ValidatorFn|ValidatorFn[]|null { - return (isOptionsObj(validatorOrOpts) ? validatorOrOpts.validators : validatorOrOpts) || null; -} - -/** - * Creates validator function by combining provided validators. - */ -function coerceToValidator(validator: ValidatorFn|ValidatorFn[]|null): ValidatorFn|null { - return Array.isArray(validator) ? composeValidators(validator) : validator || null; -} - -/** - * Gets async validators from either an options object or given validators. - */ -function pickAsyncValidators( - asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null, - validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): AsyncValidatorFn| - AsyncValidatorFn[]|null { - return (isOptionsObj(validatorOrOpts) ? validatorOrOpts.asyncValidators : asyncValidator) || null; -} - -/** - * Creates async validator function by combining provided async validators. - */ -function coerceToAsyncValidator(asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]| - null): AsyncValidatorFn|null { - return Array.isArray(asyncValidator) ? composeAsyncValidators(asyncValidator) : - asyncValidator || null; -} - -export type FormHooks = 'change'|'blur'|'submit'; - -/** - * Interface for options provided to an `AbstractControl`. - * - * @publicApi - */ -export interface AbstractControlOptions { - /** - * @description - * The list of validators applied to a control. - */ - validators?: ValidatorFn|ValidatorFn[]|null; - /** - * @description - * The list of async validators applied to control. - */ - asyncValidators?: AsyncValidatorFn|AsyncValidatorFn[]|null; - /** - * @description - * The event name for control to update upon. - */ - updateOn?: 'change'|'blur'|'submit'; -} - -/** - * 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; -} - -function isOptionsObj(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions| - null): validatorOrOpts is AbstractControlOptions { - return validatorOrOpts != null && !Array.isArray(validatorOrOpts) && - typeof validatorOrOpts === 'object'; -} - -export const isFormControl = (control: unknown): control is FormControl => - control instanceof FormControl; - -export const isFormGroup = (control: unknown): control is FormGroup => control instanceof FormGroup; - -export const isFormArray = (control: unknown): control is FormArray => control instanceof FormArray; - -function assertControlPresent(parent: FormGroup|FormArray, key: string|number): void { - const isGroup = isFormGroup(parent); - const controls = parent.controls as {[key: string|number]: unknown}; - const collection = isGroup ? Object.keys(controls) : controls; - if (!collection.length) { - throw new RuntimeError( - RuntimeErrorCode.NO_CONTROLS, NG_DEV_MODE ? noControlsError(isGroup) : ''); - } - if (!controls[key]) { - throw new RuntimeError( - RuntimeErrorCode.MISSING_CONTROL, NG_DEV_MODE ? missingControlError(isGroup, key) : ''); - } -} - -function assertAllValuesPresent(control: FormGroup|FormArray, value: any): void { - const isGroup = isFormGroup(control); - control._forEachChild((_: unknown, key: string|number) => { - if (value[key] === undefined) { - throw new RuntimeError( - RuntimeErrorCode.MISSING_CONTROL_VALUE, - NG_DEV_MODE ? missingControlValueError(isGroup, key) : ''); - } - }); -} - -/** - * 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 abstract class AbstractControl { - /** @internal */ - _pendingDirty = false; - - /** - * Indicates that a control has its own pending asynchronous validation in progress. - * - * @internal - */ - _hasOwnPendingAsyncValidator = false; - - /** @internal */ - _pendingTouched = false; - - /** @internal */ - _onCollectionChange = () => {}; - - /** @internal */ - _updateOn?: FormHooks; - - private _parent: FormGroup|FormArray|null = null; - private _asyncValidationSubscription: any; - - /** - * Contains the result of merging synchronous validators into a single validator function - * (combined using `Validators.compose`). - * - * @internal - */ - private _composedValidatorFn: ValidatorFn|null; - - /** - * Contains the result of merging asynchronous validators into a single validator function - * (combined using `Validators.composeAsync`). - * - * @internal - */ - private _composedAsyncValidatorFn: AsyncValidatorFn|null; - - /** - * Synchronous validators as they were provided: - * - in `AbstractControl` constructor - * - as an argument while calling `setValidators` function - * - while calling the setter on the `validator` field (e.g. `control.validator = validatorFn`) - * - * @internal - */ - private _rawValidators: ValidatorFn|ValidatorFn[]|null; - - /** - * Asynchronous validators as they were provided: - * - in `AbstractControl` constructor - * - as an argument while calling `setAsyncValidators` function - * - while calling the setter on the `asyncValidator` field (e.g. `control.asyncValidator = - * asyncValidatorFn`) - * - * @internal - */ - private _rawAsyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null; - - /** - * The current value of the control. - * - * * For a `FormControl`, the current value. - * * For an enabled `FormGroup`, the values of enabled controls as an object - * with a key-value pair for each member of the group. - * * For a disabled `FormGroup`, the values of all controls as an object - * with a key-value pair for each member of the group. - * * For a `FormArray`, the values of enabled controls as an array. - * - */ - public readonly value: any; - - /** - * 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: ValidatorFn|ValidatorFn[]|null, - asyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null) { - this._rawValidators = validators; - this._rawAsyncValidators = asyncValidators; - this._composedValidatorFn = coerceToValidator(this._rawValidators); - this._composedAsyncValidatorFn = coerceToAsyncValidator(this._rawAsyncValidators); - } - - /** - * Returns the function that is used to determine the validity of this control synchronously. - * If multiple validators have been added, this will be a single composed function. - * See `Validators.compose()` for additional information. - */ - get validator(): ValidatorFn|null { - return this._composedValidatorFn; - } - set validator(validatorFn: ValidatorFn|null) { - this._rawValidators = this._composedValidatorFn = validatorFn; - } - - /** - * Returns the function that is used to determine the validity of this control asynchronously. - * If multiple validators have been added, this will be a single composed function. - * See `Validators.compose()` for additional information. - */ - get asyncValidator(): AsyncValidatorFn|null { - return this._composedAsyncValidatorFn; - } - set asyncValidator(asyncValidatorFn: AsyncValidatorFn|null) { - this._rawAsyncValidators = this._composedAsyncValidatorFn = asyncValidatorFn; - } - - /** - * The parent control. - */ - get parent(): FormGroup|FormArray|null { - return this._parent; - } - - /** - * The validation status of the control. - * - * @see `FormControlStatus` - * - * These status values are mutually exclusive, so a control cannot be - * both valid AND invalid or invalid AND disabled. - */ - public readonly status!: FormControlStatus; - - /** - * 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(): boolean { - return this.status === 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(): boolean { - return this.status === 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(): boolean { - return this.status == 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(): boolean { - return this.status === 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(): boolean { - return this.status !== DISABLED; - } - - /** - * An object containing any errors generated by failing validation, - * or null if there are no errors. - */ - public readonly errors!: ValidationErrors|null; - - /** - * 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. - */ - public readonly pristine: boolean = true; - - /** - * 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(): boolean { - return !this.pristine; - } - - /** - * True if the control is marked as `touched`. - * - * A control is marked `touched` once the user has triggered - * a `blur` event on it. - */ - public readonly touched: boolean = false; - - /** - * 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(): boolean { - return !this.touched; - } - - /** - * A multicasting observable that emits an event every time the value of the control changes, in - * the UI or programmatically. It also emits an event each time you call enable() or disable() - * without passing along {emitEvent: false} as a function argument. - */ - public readonly valueChanges!: Observable; - - /** - * A multicasting observable that emits an event every time the validation `status` of the control - * recalculates. - * - * @see `FormControlStatus` - * @see {@link AbstractControl.status} - * - */ - public readonly statusChanges!: Observable; - - /** - * 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(): FormHooks { - return this._updateOn ? this._updateOn : (this.parent ? this.parent.updateOn : 'change'); - } - - /** - * Sets the synchronous validators that are active on this control. Calling - * this overwrites any existing synchronous validators. - * - * When you add or remove a validator at run time, you must call - * `updateValueAndValidity()` for the new validation to take effect. - * - * If you want to add a new validator without affecting existing ones, consider - * using `addValidators()` method instead. - */ - setValidators(validators: ValidatorFn|ValidatorFn[]|null): void { - this._rawValidators = validators; - this._composedValidatorFn = coerceToValidator(validators); - } - - /** - * Sets the asynchronous validators that are active on this control. Calling this - * overwrites any existing asynchronous validators. - * - * When you add or remove a validator at run time, you must call - * `updateValueAndValidity()` for the new validation to take effect. - * - * If you want to add a new validator without affecting existing ones, consider - * using `addAsyncValidators()` method instead. - */ - setAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]|null): void { - this._rawAsyncValidators = validators; - this._composedAsyncValidatorFn = coerceToAsyncValidator(validators); - } - - /** - * Add a synchronous validator or validators to this control, without affecting other validators. - * - * When you add or remove a validator at run time, you must call - * `updateValueAndValidity()` for the new validation to take effect. - * - * Adding a validator that already exists will have no effect. If duplicate validator functions - * are present in the `validators` array, only the first instance would be added to a form - * control. - * - * @param validators The new validator function or functions to add to this control. - */ - addValidators(validators: ValidatorFn|ValidatorFn[]): void { - this.setValidators(addValidators(validators, this._rawValidators)); - } - - /** - * Add an asynchronous validator or validators to this control, without affecting other - * validators. - * - * When you add or remove a validator at run time, you must call - * `updateValueAndValidity()` for the new validation to take effect. - * - * Adding a validator that already exists will have no effect. - * - * @param validators The new asynchronous validator function or functions to add to this control. - */ - addAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]): void { - this.setAsyncValidators(addValidators(validators, this._rawAsyncValidators)); - } - - /** - * Remove a synchronous validator from this control, without affecting other validators. - * Validators are compared by function reference; you must pass a reference to the exact same - * validator function as the one that was originally set. If a provided validator is not found, - * it is ignored. - * - * When you add or remove a validator at run time, you must call - * `updateValueAndValidity()` for the new validation to take effect. - * - * @param validators The validator or validators to remove. - */ - removeValidators(validators: ValidatorFn|ValidatorFn[]): void { - this.setValidators(removeValidators(validators, this._rawValidators)); - } - - /** - * Remove an asynchronous validator from this control, without affecting other validators. - * Validators are compared by function reference; you must pass a reference to the exact same - * validator function as the one that was originally set. If a provided validator is not found, it - * is ignored. - * - * When you add or remove a validator at run time, you must call - * `updateValueAndValidity()` for the new validation to take effect. - * - * @param validators The asynchronous validator or validators to remove. - */ - removeAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]): void { - this.setAsyncValidators(removeValidators(validators, this._rawAsyncValidators)); - } - - /** - * Check whether a synchronous validator function is present on this control. The provided - * validator must be a reference to the exact same function that was provided. - * - * @param validator The validator to check for presence. Compared by function reference. - * @returns Whether the provided validator was found on this control. - */ - hasValidator(validator: ValidatorFn): boolean { - return hasValidator(this._rawValidators, validator); - } - - /** - * Check whether an asynchronous validator function is present on this control. The provided - * validator must be a reference to the exact same function that was provided. - * - * @param validator The asynchronous validator to check for presence. Compared by function - * reference. - * @returns Whether the provided asynchronous validator was found on this control. - */ - hasAsyncValidator(validator: AsyncValidatorFn): boolean { - return hasValidator(this._rawAsyncValidators, validator); - } - - /** - * Empties out the synchronous validator list. - * - * When you add or remove a validator at run time, you must call - * `updateValueAndValidity()` for the new validation to take effect. - * - */ - clearValidators(): void { - 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(): void { - 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: {onlySelf?: boolean} = {}): void { - (this as {touched: boolean}).touched = true; - - if (this._parent && !opts.onlySelf) { - this._parent.markAsTouched(opts); - } - } - - /** - * Marks the control and all its descendant controls as `touched`. - * @see `markAsTouched()` - */ - markAllAsTouched(): void { - this.markAsTouched({onlySelf: true}); - - this._forEachChild((control: AbstractControl) => 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: {onlySelf?: boolean} = {}): void { - (this as {touched: boolean}).touched = false; - this._pendingTouched = false; - - this._forEachChild((control: AbstractControl) => { - control.markAsUntouched({onlySelf: true}); - }); - - if (this._parent && !opts.onlySelf) { - this._parent._updateTouched(opts); - } - } - - /** - * 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: {onlySelf?: boolean} = {}): void { - (this as {pristine: boolean}).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: {onlySelf?: boolean} = {}): void { - (this as {pristine: boolean}).pristine = true; - this._pendingDirty = false; - - this._forEachChild((control: AbstractControl) => { - 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. - * * `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), the `statusChanges` - * observable emits an event with the latest status the control is marked pending. - * When false, no events are emitted. - * - */ - markAsPending(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { - (this as {status: FormControlStatus}).status = PENDING; - - if (opts.emitEvent !== false) { - (this.statusChanges as EventEmitter).emit(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. - * * `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 disabled. - * When false, no events are emitted. - */ - disable(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { - // 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 as {status: FormControlStatus}).status = DISABLED; - (this as {errors: ValidationErrors | null}).errors = null; - this._forEachChild((control: AbstractControl) => { - control.disable({...opts, onlySelf: true}); - }); - this._updateValue(); - - if (opts.emitEvent !== false) { - (this.valueChanges as EventEmitter).emit(this.value); - (this.statusChanges as EventEmitter).emit(this.status); - } - - this._updateAncestors({...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: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { - // 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 as {status: FormControlStatus}).status = VALID; - this._forEachChild((control: AbstractControl) => { - control.enable({...opts, onlySelf: true}); - }); - this.updateValueAndValidity({onlySelf: true, emitEvent: opts.emitEvent}); - - this._updateAncestors({...opts, skipPristineCheck}); - this._onDisabledChange.forEach((changeFn) => changeFn(false)); - } - - private _updateAncestors( - opts: {onlySelf?: boolean, emitEvent?: boolean, skipPristineCheck?: boolean}) { - 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: FormGroup|FormArray): void { - this._parent = parent; - } - - /** - * Sets the value of the control. Abstract method (implemented in sub-classes). - */ - abstract setValue(value: any, options?: Object): void; - - /** - * Patches the value of the control. Abstract method (implemented in sub-classes). - */ - abstract patchValue(value: any, options?: Object): void; - - /** - * Resets the control. Abstract method (implemented in sub-classes). - */ - abstract reset(value?: any, options?: Object): void; - - /** - * The raw value of this control. For most control implementations, the raw value will include - * disabled children. - */ - getRawValue(): any { - return this.value; - } - - /** - * 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. - * * `onlySelf`: When true, only update this control. When false or not supplied, - * update 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 updated. - * When false, no events are emitted. - */ - updateValueAndValidity(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { - this._setInitialStatus(); - this._updateValue(); - - if (this.enabled) { - this._cancelExistingSubscription(); - (this as {errors: ValidationErrors | null}).errors = this._runValidator(); - (this as {status: FormControlStatus}).status = this._calculateStatus(); - - if (this.status === VALID || this.status === PENDING) { - this._runAsyncValidator(opts.emitEvent); - } - } - - if (opts.emitEvent !== false) { - (this.valueChanges as EventEmitter).emit(this.value); - (this.statusChanges as EventEmitter).emit(this.status); - } - - if (this._parent && !opts.onlySelf) { - this._parent.updateValueAndValidity(opts); - } - } - - /** @internal */ - _updateTreeValidity(opts: {emitEvent?: boolean} = {emitEvent: true}) { - this._forEachChild((ctrl: AbstractControl) => ctrl._updateTreeValidity(opts)); - this.updateValueAndValidity({onlySelf: true, emitEvent: opts.emitEvent}); - } - - private _setInitialStatus() { - (this as {status: FormControlStatus}).status = this._allControlsDisabled() ? DISABLED : VALID; - } - - private _runValidator(): ValidationErrors|null { - return this.validator ? this.validator(this) : null; - } - - private _runAsyncValidator(emitEvent?: boolean): void { - if (this.asyncValidator) { - (this as {status: FormControlStatus}).status = PENDING; - this._hasOwnPendingAsyncValidator = true; - const obs = toObservable(this.asyncValidator(this)); - this._asyncValidationSubscription = obs.subscribe((errors: ValidationErrors|null) => { - 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}); - }); - } - } - - private _cancelExistingSubscription(): void { - 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: ValidationErrors|null, opts: {emitEvent?: boolean} = {}): void { - (this as {errors: ValidationErrors | null}).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']);` - * - * ### Retrieve a control in a FormArray - * - * When accessing an element inside a FormArray, you can use an element index. - * For example, to get a `price` control from the first element in an `items` array you can use: - * - * * `this.form.get('items.0.price');` - * - * -OR- - * - * * `this.form.get(['items', 0, 'price']);` - */ - get(path: Array|string): AbstractControl|null { - if (path == null) return null; - if (!Array.isArray(path)) path = path.split('.'); - if (path.length === 0) return null; - return path.reduce( - (control: AbstractControl|null, name) => control && control._find(name), this); - } - - /** - * @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: string, path?: Array|string): any { - 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: string, path?: Array|string): boolean { - return !!this.getError(errorCode, path); - } - - /** - * Retrieves the top-level ancestor of this control. - */ - get root(): AbstractControl { - let x: AbstractControl = this; - - while (x._parent) { - x = x._parent; - } - - return x; - } - - /** @internal */ - _updateControlsErrors(emitEvent: boolean): void { - (this as {status: FormControlStatus}).status = this._calculateStatus(); - - if (emitEvent) { - (this.statusChanges as EventEmitter).emit(this.status); - } - - if (this._parent) { - this._parent._updateControlsErrors(emitEvent); - } - } - - /** @internal */ - _initObservables() { - (this as {valueChanges: Observable}).valueChanges = new EventEmitter(); - (this as {statusChanges: Observable}).statusChanges = new EventEmitter(); - } - - - private _calculateStatus(): FormControlStatus { - if (this._allControlsDisabled()) return DISABLED; - if (this.errors) return INVALID; - if (this._hasOwnPendingAsyncValidator || this._anyControlsHaveStatus(PENDING)) return PENDING; - if (this._anyControlsHaveStatus(INVALID)) return INVALID; - return VALID; - } - - /** @internal */ - abstract _updateValue(): void; - - /** @internal */ - abstract _forEachChild(cb: (c: AbstractControl) => void): void; - - /** @internal */ - abstract _anyControls(condition: (c: AbstractControl) => boolean): boolean; - - /** @internal */ - abstract _allControlsDisabled(): boolean; - - /** @internal */ - abstract _syncPendingControls(): boolean; - - /** @internal */ - _anyControlsHaveStatus(status: FormControlStatus): boolean { - return this._anyControls((control: AbstractControl) => control.status === status); - } - - /** @internal */ - _anyControlsDirty(): boolean { - return this._anyControls((control: AbstractControl) => control.dirty); - } - - /** @internal */ - _anyControlsTouched(): boolean { - return this._anyControls((control: AbstractControl) => control.touched); - } - - /** @internal */ - _updatePristine(opts: {onlySelf?: boolean} = {}): void { - (this as {pristine: boolean}).pristine = !this._anyControlsDirty(); - - if (this._parent && !opts.onlySelf) { - this._parent._updatePristine(opts); - } - } - - /** @internal */ - _updateTouched(opts: {onlySelf?: boolean} = {}): void { - (this as {touched: boolean}).touched = this._anyControlsTouched(); - - if (this._parent && !opts.onlySelf) { - this._parent._updateTouched(opts); - } - } - - /** @internal */ - _onDisabledChange: Array<(isDisabled: boolean) => void> = []; - - /** @internal */ - _isBoxedValue(formState: any): boolean { - return typeof formState === 'object' && formState !== null && - Object.keys(formState).length === 2 && 'value' in formState && 'disabled' in formState; - } - - /** @internal */ - _registerOnCollectionChange(fn: () => void): void { - this._onCollectionChange = fn; - } - - /** @internal */ - _setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void { - if (isOptionsObj(opts) && opts.updateOn != null) { - this._updateOn = opts.updateOn!; - } - } - /** - * Check to see if parent has been marked artificially dirty. - * - * @internal - */ - private _parentMarkedDirty(onlySelf?: boolean): boolean { - const parentDirty = this._parent && this._parent.dirty; - return !onlySelf && !!parentDirty && !this._parent!._anyControlsDirty(); - } - - /** @internal */ - _find(name: string|number): AbstractControl|null { - return null; - } -} - -/** - * 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; - -/** - * 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, value); - Object.keys(value).forEach(name => { - assertControlPresent(this, 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(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; - -/** - * 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, value); - value.forEach((newValue: any, index: number) => { - assertControlPresent(this, 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; diff --git a/packages/forms/src/model/abstract_model.ts b/packages/forms/src/model/abstract_model.ts new file mode 100644 index 00000000000..f653b53f91e --- /dev/null +++ b/packages/forms/src/model/abstract_model.ts @@ -0,0 +1,1149 @@ +/** + * @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 {EventEmitter, ɵRuntimeError as RuntimeError} from '@angular/core'; +import {Observable} from 'rxjs'; + +import {missingControlError, missingControlValueError, noControlsError} from '../directives/reactive_errors'; +import {AsyncValidatorFn, ValidationErrors, ValidatorFn} from '../directives/validators'; +import {RuntimeErrorCode} from '../errors'; +import {FormArray, FormGroup} from '../forms'; +import {addValidators, composeAsyncValidators, composeValidators, hasValidator, removeValidators, toObservable} from '../validators'; + +const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode; + +/** + * Constant indicating that a control is valid, meaning that no errors exist in the input value. + * + * @see `status` + */ +export const VALID = 'VALID'; + +/** + * Constant indicating that a control is invalid, meaning that an error exists in the input value. + * + * @see `status` + */ +export const INVALID = 'INVALID'; + +/** + * Constant indicating that a control is pending, meaning that that async validation is occurring + * and errors are not yet available for the input value. + * + * @see `markAsPending` + * @see `status` + */ +export const PENDING = 'PENDING'; + +/** + * Constant indicating that a control is disabled, meaning that the control is exempt from ancestor + * calculations of validity or value. + * + * @see `markAsDisabled` + * @see `status` + */ +export const DISABLED = 'DISABLED'; + +/** + * A form can have several different statuses. Each + * possible status is returned as a string literal. + * + * * **VALID**: Reports that a FormControl is valid, meaning that no errors exist in the input + * value. + * * **INVALID**: Reports that a FormControl is invalid, meaning that an error exists in the input + * value. + * * **PENDING**: Reports that a FormControl is pending, meaning that that async validation is + * occurring and errors are not yet available for the input value. + * * **DISABLED**: Reports that a FormControl is + * disabled, meaning that the control is exempt from ancestor calculations of validity or value. + * + * @publicApi + */ +export type FormControlStatus = 'VALID'|'INVALID'|'PENDING'|'DISABLED'; + +/** + * Gets validators from either an options object or given validators. + */ +export function pickValidators(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions| + null): ValidatorFn|ValidatorFn[]|null { + return (isOptionsObj(validatorOrOpts) ? validatorOrOpts.validators : validatorOrOpts) || null; +} + +/** + * Creates validator function by combining provided validators. + */ +function coerceToValidator(validator: ValidatorFn|ValidatorFn[]|null): ValidatorFn|null { + return Array.isArray(validator) ? composeValidators(validator) : validator || null; +} + +/** + * Gets async validators from either an options object or given validators. + */ +export function pickAsyncValidators( + asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null, + validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): AsyncValidatorFn| + AsyncValidatorFn[]|null { + return (isOptionsObj(validatorOrOpts) ? validatorOrOpts.asyncValidators : asyncValidator) || null; +} + +/** + * Creates async validator function by combining provided async validators. + */ +function coerceToAsyncValidator(asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]| + null): AsyncValidatorFn|null { + return Array.isArray(asyncValidator) ? composeAsyncValidators(asyncValidator) : + asyncValidator || null; +} + +export type FormHooks = 'change'|'blur'|'submit'; + +/** + * Interface for options provided to an `AbstractControl`. + * + * @publicApi + */ +export interface AbstractControlOptions { + /** + * @description + * The list of validators applied to a control. + */ + validators?: ValidatorFn|ValidatorFn[]|null; + /** + * @description + * The list of async validators applied to control. + */ + asyncValidators?: AsyncValidatorFn|AsyncValidatorFn[]|null; + /** + * @description + * The event name for control to update upon. + */ + updateOn?: 'change'|'blur'|'submit'; +} + +export function isOptionsObj(validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions| + null): validatorOrOpts is AbstractControlOptions { + return validatorOrOpts != null && !Array.isArray(validatorOrOpts) && + typeof validatorOrOpts === 'object'; +} + +export function assertControlPresent(parent: any, isGroup: boolean, key: string|number): void { + const controls = parent.controls as {[key: string|number]: unknown}; + const collection = isGroup ? Object.keys(controls) : controls; + if (!collection.length) { + throw new RuntimeError( + RuntimeErrorCode.NO_CONTROLS, NG_DEV_MODE ? noControlsError(isGroup) : ''); + } + if (!controls[key]) { + throw new RuntimeError( + RuntimeErrorCode.MISSING_CONTROL, NG_DEV_MODE ? missingControlError(isGroup, key) : ''); + } +} + +export function assertAllValuesPresent(control: any, isGroup: boolean, value: any): void { + control._forEachChild(((_: unknown, key: string|number) => { + if (value[key] === undefined) { + throw new RuntimeError( + RuntimeErrorCode.MISSING_CONTROL_VALUE, + NG_DEV_MODE ? missingControlValueError(isGroup, key) : ''); + } + }) as any); +} + +/** + * 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 abstract class AbstractControl { + /** @internal */ + _pendingDirty = false; + + /** + * Indicates that a control has its own pending asynchronous validation in progress. + * + * @internal + */ + _hasOwnPendingAsyncValidator = false; + + /** @internal */ + _pendingTouched = false; + + /** @internal */ + _onCollectionChange = () => {}; + + /** @internal */ + _updateOn?: FormHooks; + + private _parent: FormGroup|FormArray|null = null; + private _asyncValidationSubscription: any; + + /** + * Contains the result of merging synchronous validators into a single validator function + * (combined using `Validators.compose`). + * + * @internal + */ + private _composedValidatorFn: ValidatorFn|null; + + /** + * Contains the result of merging asynchronous validators into a single validator function + * (combined using `Validators.composeAsync`). + * + * @internal + */ + private _composedAsyncValidatorFn: AsyncValidatorFn|null; + + /** + * Synchronous validators as they were provided: + * - in `AbstractControl` constructor + * - as an argument while calling `setValidators` function + * - while calling the setter on the `validator` field (e.g. `control.validator = validatorFn`) + * + * @internal + */ + private _rawValidators: ValidatorFn|ValidatorFn[]|null; + + /** + * Asynchronous validators as they were provided: + * - in `AbstractControl` constructor + * - as an argument while calling `setAsyncValidators` function + * - while calling the setter on the `asyncValidator` field (e.g. `control.asyncValidator = + * asyncValidatorFn`) + * + * @internal + */ + private _rawAsyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null; + + /** + * The current value of the control. + * + * * For a `FormControl`, the current value. + * * For an enabled `FormGroup`, the values of enabled controls as an object + * with a key-value pair for each member of the group. + * * For a disabled `FormGroup`, the values of all controls as an object + * with a key-value pair for each member of the group. + * * For a `FormArray`, the values of enabled controls as an array. + * + */ + public readonly value: any; + + /** + * 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: ValidatorFn|ValidatorFn[]|null, + asyncValidators: AsyncValidatorFn|AsyncValidatorFn[]|null) { + this._rawValidators = validators; + this._rawAsyncValidators = asyncValidators; + this._composedValidatorFn = coerceToValidator(this._rawValidators); + this._composedAsyncValidatorFn = coerceToAsyncValidator(this._rawAsyncValidators); + } + + /** + * Returns the function that is used to determine the validity of this control synchronously. + * If multiple validators have been added, this will be a single composed function. + * See `Validators.compose()` for additional information. + */ + get validator(): ValidatorFn|null { + return this._composedValidatorFn; + } + set validator(validatorFn: ValidatorFn|null) { + this._rawValidators = this._composedValidatorFn = validatorFn; + } + + /** + * Returns the function that is used to determine the validity of this control asynchronously. + * If multiple validators have been added, this will be a single composed function. + * See `Validators.compose()` for additional information. + */ + get asyncValidator(): AsyncValidatorFn|null { + return this._composedAsyncValidatorFn; + } + set asyncValidator(asyncValidatorFn: AsyncValidatorFn|null) { + this._rawAsyncValidators = this._composedAsyncValidatorFn = asyncValidatorFn; + } + + /** + * The parent control. + */ + get parent(): FormGroup|FormArray|null { + return this._parent; + } + + /** + * The validation status of the control. + * + * @see `FormControlStatus` + * + * These status values are mutually exclusive, so a control cannot be + * both valid AND invalid or invalid AND disabled. + */ + public readonly status!: FormControlStatus; + + /** + * 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(): boolean { + return this.status === 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(): boolean { + return this.status === 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(): boolean { + return this.status == 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(): boolean { + return this.status === 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(): boolean { + return this.status !== DISABLED; + } + + /** + * An object containing any errors generated by failing validation, + * or null if there are no errors. + */ + public readonly errors!: ValidationErrors|null; + + /** + * 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. + */ + public readonly pristine: boolean = true; + + /** + * 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(): boolean { + return !this.pristine; + } + + /** + * True if the control is marked as `touched`. + * + * A control is marked `touched` once the user has triggered + * a `blur` event on it. + */ + public readonly touched: boolean = false; + + /** + * 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(): boolean { + return !this.touched; + } + + /** + * A multicasting observable that emits an event every time the value of the control changes, in + * the UI or programmatically. It also emits an event each time you call enable() or disable() + * without passing along {emitEvent: false} as a function argument. + */ + public readonly valueChanges!: Observable; + + /** + * A multicasting observable that emits an event every time the validation `status` of the control + * recalculates. + * + * @see `FormControlStatus` + * @see {@link AbstractControl.status} + * + */ + public readonly statusChanges!: Observable; + + /** + * 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(): FormHooks { + return this._updateOn ? this._updateOn : (this.parent ? this.parent.updateOn : 'change'); + } + + /** + * Sets the synchronous validators that are active on this control. Calling + * this overwrites any existing synchronous validators. + * + * When you add or remove a validator at run time, you must call + * `updateValueAndValidity()` for the new validation to take effect. + * + * If you want to add a new validator without affecting existing ones, consider + * using `addValidators()` method instead. + */ + setValidators(validators: ValidatorFn|ValidatorFn[]|null): void { + this._rawValidators = validators; + this._composedValidatorFn = coerceToValidator(validators); + } + + /** + * Sets the asynchronous validators that are active on this control. Calling this + * overwrites any existing asynchronous validators. + * + * When you add or remove a validator at run time, you must call + * `updateValueAndValidity()` for the new validation to take effect. + * + * If you want to add a new validator without affecting existing ones, consider + * using `addAsyncValidators()` method instead. + */ + setAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]|null): void { + this._rawAsyncValidators = validators; + this._composedAsyncValidatorFn = coerceToAsyncValidator(validators); + } + + /** + * Add a synchronous validator or validators to this control, without affecting other validators. + * + * When you add or remove a validator at run time, you must call + * `updateValueAndValidity()` for the new validation to take effect. + * + * Adding a validator that already exists will have no effect. If duplicate validator functions + * are present in the `validators` array, only the first instance would be added to a form + * control. + * + * @param validators The new validator function or functions to add to this control. + */ + addValidators(validators: ValidatorFn|ValidatorFn[]): void { + this.setValidators(addValidators(validators, this._rawValidators)); + } + + /** + * Add an asynchronous validator or validators to this control, without affecting other + * validators. + * + * When you add or remove a validator at run time, you must call + * `updateValueAndValidity()` for the new validation to take effect. + * + * Adding a validator that already exists will have no effect. + * + * @param validators The new asynchronous validator function or functions to add to this control. + */ + addAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]): void { + this.setAsyncValidators(addValidators(validators, this._rawAsyncValidators)); + } + + /** + * Remove a synchronous validator from this control, without affecting other validators. + * Validators are compared by function reference; you must pass a reference to the exact same + * validator function as the one that was originally set. If a provided validator is not found, + * it is ignored. + * + * When you add or remove a validator at run time, you must call + * `updateValueAndValidity()` for the new validation to take effect. + * + * @param validators The validator or validators to remove. + */ + removeValidators(validators: ValidatorFn|ValidatorFn[]): void { + this.setValidators(removeValidators(validators, this._rawValidators)); + } + + /** + * Remove an asynchronous validator from this control, without affecting other validators. + * Validators are compared by function reference; you must pass a reference to the exact same + * validator function as the one that was originally set. If a provided validator is not found, it + * is ignored. + * + * When you add or remove a validator at run time, you must call + * `updateValueAndValidity()` for the new validation to take effect. + * + * @param validators The asynchronous validator or validators to remove. + */ + removeAsyncValidators(validators: AsyncValidatorFn|AsyncValidatorFn[]): void { + this.setAsyncValidators(removeValidators(validators, this._rawAsyncValidators)); + } + + /** + * Check whether a synchronous validator function is present on this control. The provided + * validator must be a reference to the exact same function that was provided. + * + * @param validator The validator to check for presence. Compared by function reference. + * @returns Whether the provided validator was found on this control. + */ + hasValidator(validator: ValidatorFn): boolean { + return hasValidator(this._rawValidators, validator); + } + + /** + * Check whether an asynchronous validator function is present on this control. The provided + * validator must be a reference to the exact same function that was provided. + * + * @param validator The asynchronous validator to check for presence. Compared by function + * reference. + * @returns Whether the provided asynchronous validator was found on this control. + */ + hasAsyncValidator(validator: AsyncValidatorFn): boolean { + return hasValidator(this._rawAsyncValidators, validator); + } + + /** + * Empties out the synchronous validator list. + * + * When you add or remove a validator at run time, you must call + * `updateValueAndValidity()` for the new validation to take effect. + * + */ + clearValidators(): void { + 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(): void { + 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: {onlySelf?: boolean} = {}): void { + (this as {touched: boolean}).touched = true; + + if (this._parent && !opts.onlySelf) { + this._parent.markAsTouched(opts); + } + } + + /** + * Marks the control and all its descendant controls as `touched`. + * @see `markAsTouched()` + */ + markAllAsTouched(): void { + this.markAsTouched({onlySelf: true}); + + this._forEachChild((control: AbstractControl) => 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: {onlySelf?: boolean} = {}): void { + (this as {touched: boolean}).touched = false; + this._pendingTouched = false; + + this._forEachChild((control: AbstractControl) => { + control.markAsUntouched({onlySelf: true}); + }); + + if (this._parent && !opts.onlySelf) { + this._parent._updateTouched(opts); + } + } + + /** + * 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: {onlySelf?: boolean} = {}): void { + (this as {pristine: boolean}).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: {onlySelf?: boolean} = {}): void { + (this as {pristine: boolean}).pristine = true; + this._pendingDirty = false; + + this._forEachChild((control: AbstractControl) => { + 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. + * * `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), the `statusChanges` + * observable emits an event with the latest status the control is marked pending. + * When false, no events are emitted. + * + */ + markAsPending(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + (this as {status: FormControlStatus}).status = PENDING; + + if (opts.emitEvent !== false) { + (this.statusChanges as EventEmitter).emit(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. + * * `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 disabled. + * When false, no events are emitted. + */ + disable(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + // 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 as {status: FormControlStatus}).status = DISABLED; + (this as {errors: ValidationErrors | null}).errors = null; + this._forEachChild((control: AbstractControl) => { + control.disable({...opts, onlySelf: true}); + }); + this._updateValue(); + + if (opts.emitEvent !== false) { + (this.valueChanges as EventEmitter).emit(this.value); + (this.statusChanges as EventEmitter).emit(this.status); + } + + this._updateAncestors({...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: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + // 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 as {status: FormControlStatus}).status = VALID; + this._forEachChild((control: AbstractControl) => { + control.enable({...opts, onlySelf: true}); + }); + this.updateValueAndValidity({onlySelf: true, emitEvent: opts.emitEvent}); + + this._updateAncestors({...opts, skipPristineCheck}); + this._onDisabledChange.forEach((changeFn) => changeFn(false)); + } + + private _updateAncestors( + opts: {onlySelf?: boolean, emitEvent?: boolean, skipPristineCheck?: boolean}) { + 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: FormGroup|FormArray): void { + this._parent = parent; + } + + /** + * Sets the value of the control. Abstract method (implemented in sub-classes). + */ + abstract setValue(value: any, options?: Object): void; + + /** + * Patches the value of the control. Abstract method (implemented in sub-classes). + */ + abstract patchValue(value: any, options?: Object): void; + + /** + * Resets the control. Abstract method (implemented in sub-classes). + */ + abstract reset(value?: any, options?: Object): void; + + /** + * The raw value of this control. For most control implementations, the raw value will include + * disabled children. + */ + getRawValue(): any { + return this.value; + } + + /** + * 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. + * * `onlySelf`: When true, only update this control. When false or not supplied, + * update 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 updated. + * When false, no events are emitted. + */ + updateValueAndValidity(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + this._setInitialStatus(); + this._updateValue(); + + if (this.enabled) { + this._cancelExistingSubscription(); + (this as {errors: ValidationErrors | null}).errors = this._runValidator(); + (this as {status: FormControlStatus}).status = this._calculateStatus(); + + if (this.status === VALID || this.status === PENDING) { + this._runAsyncValidator(opts.emitEvent); + } + } + + if (opts.emitEvent !== false) { + (this.valueChanges as EventEmitter).emit(this.value); + (this.statusChanges as EventEmitter).emit(this.status); + } + + if (this._parent && !opts.onlySelf) { + this._parent.updateValueAndValidity(opts); + } + } + + /** @internal */ + _updateTreeValidity(opts: {emitEvent?: boolean} = {emitEvent: true}) { + this._forEachChild((ctrl: AbstractControl) => ctrl._updateTreeValidity(opts)); + this.updateValueAndValidity({onlySelf: true, emitEvent: opts.emitEvent}); + } + + private _setInitialStatus() { + (this as {status: FormControlStatus}).status = this._allControlsDisabled() ? DISABLED : VALID; + } + + private _runValidator(): ValidationErrors|null { + return this.validator ? this.validator(this) : null; + } + + private _runAsyncValidator(emitEvent?: boolean): void { + if (this.asyncValidator) { + (this as {status: FormControlStatus}).status = PENDING; + this._hasOwnPendingAsyncValidator = true; + const obs = toObservable(this.asyncValidator(this)); + this._asyncValidationSubscription = obs.subscribe((errors: ValidationErrors|null) => { + 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}); + }); + } + } + + private _cancelExistingSubscription(): void { + 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: ValidationErrors|null, opts: {emitEvent?: boolean} = {}): void { + (this as {errors: ValidationErrors | null}).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']);` + * + * ### Retrieve a control in a FormArray + * + * When accessing an element inside a FormArray, you can use an element index. + * For example, to get a `price` control from the first element in an `items` array you can use: + * + * * `this.form.get('items.0.price');` + * + * -OR- + * + * * `this.form.get(['items', 0, 'price']);` + */ + get(path: Array|string): AbstractControl|null { + if (path == null) return null; + if (!Array.isArray(path)) path = path.split('.'); + if (path.length === 0) return null; + return path.reduce( + (control: AbstractControl|null, name) => control && control._find(name), this); + } + + /** + * @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: string, path?: Array|string): any { + 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: string, path?: Array|string): boolean { + return !!this.getError(errorCode, path); + } + + /** + * Retrieves the top-level ancestor of this control. + */ + get root(): AbstractControl { + let x: AbstractControl = this; + + while (x._parent) { + x = x._parent; + } + + return x; + } + + /** @internal */ + _updateControlsErrors(emitEvent: boolean): void { + (this as {status: FormControlStatus}).status = this._calculateStatus(); + + if (emitEvent) { + (this.statusChanges as EventEmitter).emit(this.status); + } + + if (this._parent) { + this._parent._updateControlsErrors(emitEvent); + } + } + + /** @internal */ + _initObservables() { + (this as {valueChanges: Observable}).valueChanges = new EventEmitter(); + (this as {statusChanges: Observable}).statusChanges = new EventEmitter(); + } + + + private _calculateStatus(): FormControlStatus { + if (this._allControlsDisabled()) return DISABLED; + if (this.errors) return INVALID; + if (this._hasOwnPendingAsyncValidator || this._anyControlsHaveStatus(PENDING)) return PENDING; + if (this._anyControlsHaveStatus(INVALID)) return INVALID; + return VALID; + } + + /** @internal */ + abstract _updateValue(): void; + + /** @internal */ + abstract _forEachChild(cb: (c: AbstractControl) => void): void; + + /** @internal */ + abstract _anyControls(condition: (c: AbstractControl) => boolean): boolean; + + /** @internal */ + abstract _allControlsDisabled(): boolean; + + /** @internal */ + abstract _syncPendingControls(): boolean; + + /** @internal */ + _anyControlsHaveStatus(status: FormControlStatus): boolean { + return this._anyControls((control: AbstractControl) => control.status === status); + } + + /** @internal */ + _anyControlsDirty(): boolean { + return this._anyControls((control: AbstractControl) => control.dirty); + } + + /** @internal */ + _anyControlsTouched(): boolean { + return this._anyControls((control: AbstractControl) => control.touched); + } + + /** @internal */ + _updatePristine(opts: {onlySelf?: boolean} = {}): void { + (this as {pristine: boolean}).pristine = !this._anyControlsDirty(); + + if (this._parent && !opts.onlySelf) { + this._parent._updatePristine(opts); + } + } + + /** @internal */ + _updateTouched(opts: {onlySelf?: boolean} = {}): void { + (this as {touched: boolean}).touched = this._anyControlsTouched(); + + if (this._parent && !opts.onlySelf) { + this._parent._updateTouched(opts); + } + } + + /** @internal */ + _onDisabledChange: Array<(isDisabled: boolean) => void> = []; + + /** @internal */ + _isBoxedValue(formState: any): boolean { + return typeof formState === 'object' && formState !== null && + Object.keys(formState).length === 2 && 'value' in formState && 'disabled' in formState; + } + + /** @internal */ + _registerOnCollectionChange(fn: () => void): void { + this._onCollectionChange = fn; + } + + /** @internal */ + _setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void { + if (isOptionsObj(opts) && opts.updateOn != null) { + this._updateOn = opts.updateOn!; + } + } + /** + * Check to see if parent has been marked artificially dirty. + * + * @internal + */ + private _parentMarkedDirty(onlySelf?: boolean): boolean { + const parentDirty = this._parent && this._parent.dirty; + return !onlySelf && !!parentDirty && !this._parent!._anyControlsDirty(); + } + + /** @internal */ + _find(name: string|number): AbstractControl|null { + return null; + } +} diff --git a/packages/forms/src/model/form_array.ts b/packages/forms/src/model/form_array.ts new file mode 100644 index 00000000000..205011ec313 --- /dev/null +++ b/packages/forms/src/model/form_array.ts @@ -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; diff --git a/packages/forms/src/model/form_control.ts b/packages/forms/src/model/form_control.ts new file mode 100644 index 00000000000..dbd0c5611f9 --- /dev/null +++ b/packages/forms/src/model/form_control.ts @@ -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; diff --git a/packages/forms/src/model/form_group.ts b/packages/forms/src/model/form_group.ts new file mode 100644 index 00000000000..1037e040f8b --- /dev/null +++ b/packages/forms/src/model/form_group.ts @@ -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(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; diff --git a/packages/forms/src/util.ts b/packages/forms/src/util.ts new file mode 100644 index 00000000000..cdaaaca13be --- /dev/null +++ b/packages/forms/src/util.ts @@ -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(list: T[], el: T): void { + const index = list.indexOf(el); + if (index > -1) list.splice(index, 1); +} diff --git a/packages/forms/src/validators.ts b/packages/forms/src/validators.ts index 9aacc6731d6..d2f974fb510 100644 --- a/packages/forms/src/validators.ts +++ b/packages/forms/src/validators.ts @@ -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 { /**