/** * @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, ɵRawValue, ɵTypedOrUntyped, ɵValue} from './abstract_model'; /** * FormArrayValue extracts the type of `.value` from a FormArray's element type, and wraps it in an * array. * * Angular uses this type internally to support Typed Forms; do not use it directly. The untyped * case falls back to any[]. */ export type ɵFormArrayValue> = ɵTypedOrUntyped>, any[]>; /** * FormArrayRawValue extracts the type of `.getRawValue()` from a FormArray's element type, and * wraps it in an array. The untyped case falls back to any[]. * * Angular uses this type internally to support Typed Forms; do not use it directly. */ export type ɵFormArrayRawValue> = ɵTypedOrUntyped>, any[]>; /** * 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` accepts one generic argument, which is the type of the controls inside. * If you need a heterogenous array, use {@link UntypedFormArray}. * * `FormArray` is one of the four fundamental building blocks used to define forms in Angular, * along with `FormControl`, `FormGroup`, and `FormRecord`. * * @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 = any> extends AbstractControl< ɵTypedOrUntyped, any>, ɵTypedOrUntyped, any>> { /** * 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( controls: Array, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null, asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) { super(pickValidators(validatorOrOpts), pickAsyncValidators(asyncValidator, validatorOrOpts)); this.controls = controls; this._initObservables(); this._setUpdateStrategy(validatorOrOpts); this._setUpControls(); this.updateValueAndValidity({ onlySelf: true, // If `asyncValidator` is present, it will trigger control status change from `PENDING` to // `VALID` or `INVALID`. // The status should be broadcasted via the `statusChanges` observable, so we set `emitEvent` // to `true` to allow that during the control creation process. emitEvent: !!this.asyncValidator }); } public controls: ɵTypedOrUntyped, Array>>; /** * 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): ɵTypedOrUntyped> { return (this.controls as any)[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: TControl, 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: TControl, 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: TControl, 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: ɵFormArrayRawValue, 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: ɵFormArrayValue, 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, index) => { 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: ɵTypedOrUntyped, 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. */ override getRawValue(): ɵFormArrayRawValue { 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) => 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 as any).reduce((updated: any, child: any) => { 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) => control.enabled && condition(control)); } /** @internal */ _setUpControls(): void { this._forEachChild((control) => 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, which * permits heterogenous controls. */ export type UntypedFormArray = FormArray; export const UntypedFormArray: UntypedFormArrayCtor = FormArray; /** * @description * Asserts that the given control is an instance of `FormArray` * * @publicApi */ export const isFormArray = (control: unknown): control is FormArray => control instanceof FormArray;