diff --git a/packages/core/src/components/fields.tsx b/packages/core/src/components/fields.tsx
index d1703b5..4e57f64 100644
--- a/packages/core/src/components/fields.tsx
+++ b/packages/core/src/components/fields.tsx
@@ -1,22 +1,22 @@
import React, { useLayoutEffect } from "react";
-import clsx from 'clsx';
+import clsx from "clsx";
import {
- AbstractControl,
- FormControl,
- FormGroup,
- FormArray,
- Form
+ AbstractControl,
+ FormControl,
+ FormGroup,
+ FormArray,
+ Form,
} from "../models/forms";
import {
- StaticElement,
- InputElement,
- InputBase,
- InputGroup,
- InputArray,
+ StaticElement,
+ InputElement,
+ InputBase,
+ InputGroup,
+ InputArray,
} from "../models/inputs";
-import { ErrorTranslators } from '../models/errors';
-import { useForceUpdate } from '../hooks/force-update';
+import { ErrorTranslators } from "../models/errors";
+import { useForceUpdate } from "../hooks/force-update";
const inputHasControl = (input: InputElement): boolean => {
// NOTE: static elements are skipped, since they are just
@@ -48,7 +48,7 @@ export const Field = (props: FieldProps): JSX.Element | null => {
return;
}
- const opts = { emitEvent: false };
+ const opts = { emitEvent: false, form };
if (isHidden) {
control?.disable(opts);
} else {
@@ -94,12 +94,7 @@ export const Field = (props: FieldProps): JSX.Element | null => {
/>
);
} else if (input instanceof StaticElement) {
- return (
-
- );
+ return ;
}
// unknown case
@@ -148,7 +143,9 @@ export const FieldGroup = (
return (
{props.input?.inputs?.map((inp) => {
- const ctrl = inp?.name ? props.control?.get(inp.name) : undefined;
+ const ctrl = inp?.name
+ ? props.control?.get(inp.name)
+ : undefined;
return (
{
-
const C = props.input?.component;
if (!C) {
return null;
diff --git a/packages/core/src/models/controls.ts b/packages/core/src/models/controls.ts
index 9d118a6..8cadd1a 100644
--- a/packages/core/src/models/controls.ts
+++ b/packages/core/src/models/controls.ts
@@ -1,51 +1,58 @@
-import { Errors } from './errors';
+import { Errors } from "./errors";
export type Status = "VALID" | "INVALID" | "DISABLED" | "PENDING";
export interface SelfOnlyOpts {
- onlySelf?: boolean;
+ onlySelf?: boolean;
}
export interface EmitEventOpts {
- emitEvent?: boolean;
+ emitEvent?: boolean;
}
-export interface ControlChangeOpts extends SelfOnlyOpts, EmitEventOpts {}
+export interface FormEventOpts {
+ form?: any;
+}
+
+export interface ControlChangeOpts
+ extends SelfOnlyOpts,
+ EmitEventOpts,
+ FormEventOpts {}
export interface BaseControl {
- readonly value?: T;
- readonly status: Status;
- readonly pristine: boolean;
- readonly dirty: boolean;
- readonly valid: boolean;
- readonly invalid: boolean;
- readonly pending: boolean;
- readonly untouched: boolean;
- readonly touched: boolean;
- readonly enabled: boolean;
- readonly disabled: boolean;
- readonly root: BaseControl;
- readonly errors: Errors | null;
+ readonly value?: T;
+ readonly status: Status;
+ readonly pristine: boolean;
+ readonly dirty: boolean;
+ readonly valid: boolean;
+ readonly invalid: boolean;
+ readonly pending: boolean;
+ readonly untouched: boolean;
+ readonly touched: boolean;
+ readonly enabled: boolean;
+ readonly disabled: boolean;
+ readonly root: BaseControl;
+ readonly errors: Errors | null;
- updateValueAndValidity: (options: ControlChangeOpts) => void;
- markAsTouched: (opts?: ControlChangeOpts) => void;
- markAsPristine: (opts?: ControlChangeOpts) => void;
- markAsUntouched: (opts?: ControlChangeOpts) => void;
- markAsDirty: (opts?: ControlChangeOpts) => void;
- markAsPending: (opts?: ControlChangeOpts) => void;
- setErrors: (errors?: Errors | null, opts?: ControlChangeOpts) => void;
- getError: (
- errorCode: string,
- path: string | Array
- ) => any | null;
- hasError: (
- errorCode: string,
- path: string | Array
- ) => boolean;
- updatePristine: (opts?: ControlChangeOpts) => void;
- updateTouched: (opts?: ControlChangeOpts) => void;
- reset: (formState: any, opts?: ControlChangeOpts) => void;
- setValue: (value?: T, opts?: ControlChangeOpts) => void;
- patchValue: (value?: T, opts?: ControlChangeOpts) => void;
- emitEvents: (onlySelf?: boolean) => void;
+ updateValueAndValidity: (options: ControlChangeOpts) => void;
+ markAsTouched: (opts?: ControlChangeOpts) => void;
+ markAsPristine: (opts?: ControlChangeOpts) => void;
+ markAsUntouched: (opts?: ControlChangeOpts) => void;
+ markAsDirty: (opts?: ControlChangeOpts) => void;
+ markAsPending: (opts?: ControlChangeOpts) => void;
+ setErrors: (errors?: Errors | null, opts?: ControlChangeOpts) => void;
+ getError: (
+ errorCode: string,
+ path: string | Array
+ ) => any | null;
+ hasError: (
+ errorCode: string,
+ path: string | Array
+ ) => boolean;
+ updatePristine: (opts?: ControlChangeOpts) => void;
+ updateTouched: (opts?: ControlChangeOpts) => void;
+ reset: (formState: any, opts?: ControlChangeOpts) => void;
+ setValue: (value?: T, opts?: ControlChangeOpts) => void;
+ patchValue: (value?: T, opts?: ControlChangeOpts) => void;
+ emitEvents: (onlySelf?: boolean) => void;
}
diff --git a/packages/core/src/models/forms.ts b/packages/core/src/models/forms.ts
index 5ce9bf2..6773683 100644
--- a/packages/core/src/models/forms.ts
+++ b/packages/core/src/models/forms.ts
@@ -2,32 +2,30 @@ import { Validators } from "../validation/validators";
import { Observable, Subscription } from "./observable";
import {
- ValidatorFn,
- AsyncValidatorFn,
- Validator,
- AsyncValidator,
- PossibleValidatorFn,
- PossibleAsyncValidatorFn,
+ ValidatorFn,
+ AsyncValidatorFn,
+ Validator,
+ AsyncValidator,
+ PossibleValidatorFn,
+ PossibleAsyncValidatorFn,
} from "./validators";
-import { Errors } from './errors';
+import { Errors } from "./errors";
import { InputElement, InputGroup, InputArray, InputBase } from "./inputs";
import { Status, BaseControl, ControlChangeOpts } from "./controls";
interface FormGroupValue {
- [key: string]: any;
+ [key: string]: any;
}
type ControlOptionsObject = {
- validators?: PossibleValidatorFn;
- asyncValidators?: PossibleAsyncValidatorFn;
+ validators?: PossibleValidatorFn;
+ asyncValidators?: PossibleAsyncValidatorFn;
};
-type ValidatorOrOpts =
- | PossibleValidatorFn
- | ControlOptionsObject;
+type ValidatorOrOpts = PossibleValidatorFn | ControlOptionsObject;
interface NamedControlsMap {
- [name: string]: AbstractControl;
+ [name: string]: AbstractControl;
}
/**
@@ -36,46 +34,41 @@ interface NamedControlsMap {
* @param {String} delimiter
*/
const find = (
- control: AbstractControl,
- path: Array | string,
- delimiter: string
+ control: AbstractControl,
+ path: Array | string,
+ delimiter: string
): AbstractControl | undefined => {
- if (path === null || path === undefined) {
- return undefined;
- }
- if (!(path instanceof Array)) {
- path = path.split(delimiter);
- }
- if (path instanceof Array && path.length === 0) {
- return undefined;
- }
- return path.reduce(
- (v: AbstractControl | undefined, nameOrIndex: string | number) => {
- if (v instanceof FormGroup) {
- return v.controls[nameOrIndex] || undefined;
- }
- if (v instanceof FormArray) {
- return v.at(nameOrIndex as number) || undefined;
- }
- return undefined;
- },
- control
- );
+ if (path === null || path === undefined) {
+ return undefined;
+ }
+ if (!(path instanceof Array)) {
+ path = path.split(delimiter);
+ }
+ if (path instanceof Array && path.length === 0) {
+ return undefined;
+ }
+ return path.reduce(
+ (v: AbstractControl | undefined, nameOrIndex: string | number) => {
+ if (v instanceof FormGroup) {
+ return v.controls[nameOrIndex] || undefined;
+ }
+ if (v instanceof FormArray) {
+ return v.at(nameOrIndex as number) || undefined;
+ }
+ return undefined;
+ },
+ control
+ );
};
-const isOptionsObj = (
- obj: any
-): obj is ControlOptionsObject => {
- return Boolean(obj && !Array.isArray(obj) && typeof obj === "object");
+const isOptionsObj = (obj: any): obj is ControlOptionsObject => {
+ return Boolean(obj && !Array.isArray(obj) && typeof obj === "object");
};
-const isValidator = <
- V,
- S = Validator | AsyncValidator
->(
- obj: any
+const isValidator = | AsyncValidator>(
+ obj: any
): obj is S => {
- return Boolean(obj && "validate" in obj);
+ return Boolean(obj && "validate" in obj);
};
/**
@@ -83,12 +76,13 @@ const isValidator = <
* @return {Function}
*/
function normalizeValidator(
- validator: Validator | ValidatorFn
+ validator: Validator | ValidatorFn
): ValidatorFn {
- if (isValidator>(validator)) {
- return (control: BaseControl, form: any) => validator.validate(control, form);
- }
- return validator;
+ if (isValidator>(validator)) {
+ return (control: BaseControl, form: any) =>
+ validator.validate(control, form);
+ }
+ return validator;
}
/**
@@ -96,12 +90,13 @@ function normalizeValidator(
* @return {Function}
*/
function normalizeAsyncValidator(
- validator: AsyncValidator | AsyncValidatorFn
+ validator: AsyncValidator | AsyncValidatorFn
): AsyncValidatorFn {
- if (isValidator>(validator)) {
- return (control: BaseControl, form: any) => validator.validate(control, form);
- }
- return validator;
+ if (isValidator>(validator)) {
+ return (control: BaseControl, form: any) =>
+ validator.validate(control, form);
+ }
+ return validator;
}
/**
@@ -109,9 +104,9 @@ function normalizeAsyncValidator(
* @return {Function|null}
*/
const composeValidators = (
- validators: ValidatorFn[]
+ validators: ValidatorFn[]
): ValidatorFn | null => {
- return Validators.compose(validators.map(normalizeValidator));
+ return Validators.compose(validators.map(normalizeValidator));
};
/**
@@ -119,40 +114,40 @@ const composeValidators = (
* @return {Function|null}
*/
const composeAsyncValidators = (
- validators: AsyncValidatorFn[]
+ validators: AsyncValidatorFn[]
): AsyncValidatorFn | null => {
- return Validators.composeAsync(validators.map(normalizeAsyncValidator));
+ return Validators.composeAsync(validators.map(normalizeAsyncValidator));
};
const coerceToValidator = (
- validatorOrOpts?: ValidatorOrOpts
+ validatorOrOpts?: ValidatorOrOpts
): ValidatorFn | null => {
- const validator = isOptionsObj(validatorOrOpts)
- ? (validatorOrOpts.validators as PossibleValidatorFn)
- : (validatorOrOpts as PossibleValidatorFn);
+ const validator = isOptionsObj(validatorOrOpts)
+ ? (validatorOrOpts.validators as PossibleValidatorFn)
+ : (validatorOrOpts as PossibleValidatorFn);
- if (!validator) {
- return null;
- }
+ if (!validator) {
+ return null;
+ }
- return Array.isArray(validator) ? composeValidators(validator) : validator;
+ return Array.isArray(validator) ? composeValidators(validator) : validator;
};
const coerceToAsyncValidator = (
- asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
- validatorOrOpts?: ValidatorOrOpts
+ asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
+ validatorOrOpts?: ValidatorOrOpts
): AsyncValidatorFn | null => {
- const origAsyncValidator = isOptionsObj(validatorOrOpts)
- ? (validatorOrOpts.asyncValidators as PossibleAsyncValidatorFn)
- : (asyncValidator as PossibleAsyncValidatorFn);
+ const origAsyncValidator = isOptionsObj(validatorOrOpts)
+ ? (validatorOrOpts.asyncValidators as PossibleAsyncValidatorFn)
+ : (asyncValidator as PossibleAsyncValidatorFn);
- if (!origAsyncValidator) {
- return null;
- }
+ if (!origAsyncValidator) {
+ return null;
+ }
- return Array.isArray(origAsyncValidator)
- ? composeAsyncValidators(origAsyncValidator)
- : origAsyncValidator;
+ return Array.isArray(origAsyncValidator)
+ ? composeAsyncValidators(origAsyncValidator)
+ : origAsyncValidator;
};
/**
@@ -164,61 +159,17 @@ const coerceToAsyncValidator = (
* that are shared between all sub-classes, like `value`, `valid`, and `dirty`. It shouldn't be
* instantiated directly.
*/
-export abstract class AbstractControl
- implements BaseControl {
- value?: V;
-
- status!: Status;
-
- /**
- * A control is marked `touched` once the user has triggered
- * a `blur` event on it.
- */
- touched: boolean;
-
- /**
- * A control is `pristine` if the user has not yet changed
- * the value in the UI.
- *
- * Note that programmatic changes to a control's value will
- * *not* mark it dirty.
- */
- pristine: boolean;
-
- errors: Errors | null;
-
- validator: ValidatorFn | null;
- asyncValidator: AsyncValidatorFn | null;
-
- valueChanges!: Observable;
- statusChanges!: Observable;
- stateChanges!: Observable;
- anythingChanges!: Observable;
-
- protected _parent?: FormGroup | FormArray;
- protected pendingChange: boolean;
- protected pendingDirty: boolean;
- protected pendingTouched: boolean;
-
- private onCollectionChange?: () => void;
-
- private asyncValidationSubscription?: Subscription;
-
- /**
- * @param {Function|null} validator
- * @param {Function|null} asyncValidator
- */
- constructor(
- validator: ValidatorFn | null,
- asyncValidator: AsyncValidatorFn | null
- ) {
- this.validator = validator;
- this.asyncValidator = asyncValidator;
+export abstract class AbstractControl implements BaseControl {
+ value?: V;
+
+ status!: Status;
+
/**
* A control is marked `touched` once the user has triggered
* a `blur` event on it.
*/
- this.touched = false;
+ touched: boolean;
+
/**
* A control is `pristine` if the user has not yet changed
* the value in the UI.
@@ -226,1264 +177,1370 @@ export abstract class AbstractControl
* Note that programmatic changes to a control's value will
* *not* mark it dirty.
*/
- this.pristine = true;
-
- this.errors = null;
-
- this.pendingChange = false;
- this.pendingDirty = false;
- this.pendingTouched = false;
-
- // this.hasError = this.hasError.bind(this);
- // this.getError = this.getError.bind(this);
- // this.reset = this.reset.bind(this);
- // this.get = this.get.bind(this);
- // this.patchValue = this.patchValue.bind(this);
- // this.setValue = this.setValue.bind(this);
- }
-
- abstract getRawValue(): any;
-
- /**
- * A control is `dirty` if the user has changed the value
- * in the UI.
- *
- * Note that programmatic changes to a control's value will
- * *not* mark it dirty.
- * @return {Boolean}
- */
- get dirty(): boolean {
- return !this.pristine;
- }
-
- /**
- * A control is `valid` when its `status === VALID`.
- *
- * In order to have this status, the control must have passed all its
- * validation checks.
- * @return {Boolean}
- */
- get valid(): boolean {
- return this.status === "VALID";
- }
-
- /**
- * A control is `invalid` when its `status === INVALID`.
- *
- * In order to have this status, the control must have failed
- * at least one of its validation checks.
- * @return {Boolean}
- */
- get invalid(): boolean {
- return this.status === "INVALID";
- }
-
- /**
- * A control is `pending` when its `status === PENDING`.
- *
- * In order to have this status, the control must be in the
- * middle of conducting a validation check.
- */
- get pending(): boolean {
- return this.status === "PENDING";
- }
-
- /**
- * The parent control.
- * * @return {FormGroup|FormArray}
- */
- get parent(): FormGroup | FormArray | undefined {
- return this._parent;
- }
-
- /**
- * A control is `untouched` if the user has not yet triggered
- * a `blur` event on it.
- * @return {Boolean}
- */
- get untouched(): boolean {
- return !this.touched;
- }
-
- /**
- * A control is `enabled` as long as its `status !== DISABLED`.
- *
- * In other words, it has a status of `VALID`, `INVALID`, or
- * `PENDING`.
- * @return {Boolean}
- */
- get enabled(): boolean {
- return this.status !== "DISABLED";
- }
-
- /**
- * A control is disabled if it's status is `DISABLED`
- */
- get disabled(): boolean {
- return this.status === "DISABLED";
- }
-
- /**
- * Retrieves the top-level ancestor of this control.
- * @return {AbstractControl}
- */
- get root(): AbstractControl {
- // eslint-disable-next-line @typescript-eslint/no-this-alias
- let x: AbstractControl = this;
- while (x._parent) {
- x = x._parent;
- }
- return x;
- }
-
- setInitialStatus(): void {
- if (this.disabled) {
- this.status = "DISABLED";
- } else {
- this.status = "VALID";
- }
- }
-
- emitEvents(onlySelf?: boolean): void {
- this.stateChanges?.next();
- this.statusChanges?.next(this.status);
- this.valueChanges?.next(this.value);
- this.anythingChanges?.next();
-
- if (this._parent && !onlySelf) {
- this._parent.emitEvents();
- }
- }
-
- /**
- * Disables the control. This means the control will be exempt from validation checks and
- * excluded from the aggregate value of any parent. Its status is `DISABLED`.
- *
- * If the control has children, all children will be disabled to maintain the model.
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
- * @return {void}
- */
- disable(opts: ControlChangeOpts = {}): void {
- this.status = "DISABLED";
- this.errors = null;
- this.forEachChild((control) => {
- control.disable({
- emitEvent: false,
- onlySelf: true,
- });
- });
- this.updateValueAndValidity({
- onlySelf: true,
- emitEvent: opts.emitEvent,
- });
- this.updateAncestors(opts.onlySelf, opts.emitEvent);
- }
-
- /**
- * Enables the control. This means the control will be included in validation checks and
- * the aggregate value of its parent. Its status is re-calculated based on its value and
- * its validators.
- *
- * If the control has children, all children will be enabled.
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
- * @return {void}
- */
- enable(opts: ControlChangeOpts = {}): void {
- this.status = "VALID";
- this.forEachChild((control) => {
- control.enable({
- emitEvent: false,
- onlySelf: true,
- });
- });
- this.updateValueAndValidity({
- onlySelf: true,
- emitEvent: opts.emitEvent,
- });
- this.updateAncestors(opts.onlySelf, opts.emitEvent);
- }
-
- /**
- * Re-calculates the value and validation status of the control.
- *
- * By default, it will also update the value and validity of its ancestors.
- * @param {{onlySelf: Boolean, emitEvent: Booelan}} options
- */
- updateValueAndValidity(opts: ControlChangeOpts = {}): void {
- this.setInitialStatus();
- this.updateValue();
- if (this.enabled) {
- this.cancelExistingSubscription();
- this.errors = this.runValidator();
- this.status = this.calculateStatus();
- if (this.status === "VALID" || this.status === "PENDING") {
- this.runAsyncValidator(opts.emitEvent !== false);
- }
- }
- if (opts.emitEvent !== false) {
- this.emitEvents(opts.onlySelf);
- }
- if (this._parent && !opts.onlySelf) {
- this._parent.updateValueAndValidity(opts);
- }
- }
-
- /**
- * Marks the control as `touched`.
- *
- * This will also mark all direct ancestors as `touched` to maintain
- * the model.
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
- * @return {void}
- */
- markAsTouched(opts: ControlChangeOpts = {}): void {
- this.touched = true;
- if (this._parent && !opts.onlySelf) {
- this._parent.markAsTouched(opts);
- }
- if (opts.emitEvent) {
- this.stateChanges?.next();
- }
- }
-
- /**
- * Marks the control as `pristine`.
- *
- * If the control has any children, it will also mark all children as `pristine`
- * to maintain the model, and re-calculate the `pristine` status of all parent
- * controls.
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
- * @return {void}
- */
- markAsPristine(opts: ControlChangeOpts = {}): void {
- this.pristine = true;
- this.pendingDirty = false;
- if (opts.emitEvent) {
- this.stateChanges?.next();
- }
- this.forEachChild((control) => {
- control.markAsPristine({
- onlySelf: true,
- });
- });
- if (this._parent && !opts.onlySelf) {
- this._parent.updatePristine(opts);
- }
- }
-
- /**
- * Marks the control as `untouched`.
- *
- * If the control has any children, it will also mark all children as `untouched`
- * to maintain the model, and re-calculate the `touched` status of all parent
- * controls.
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
- * @return {void}
- */
- markAsUntouched(opts: ControlChangeOpts = {}): void {
- this.touched = false;
- this.pendingTouched = false;
- this.forEachChild((control) => {
- control.markAsUntouched({
- onlySelf: true,
- });
- });
- if (this._parent && !opts.onlySelf) {
- this._parent.updateTouched(opts);
- }
- if (opts.emitEvent) {
- this.stateChanges.next();
- }
- }
-
- /**
- * Marks the control as `dirty`.
- *
- * This will also mark all direct ancestors as `dirty` to maintain
- * the model.
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
- * @return {void}
- */
- markAsDirty(opts: ControlChangeOpts = {}): void {
- this.pristine = false;
- if (opts.emitEvent) {
- this.stateChanges?.next();
- }
- if (this._parent && !opts.onlySelf) {
- this._parent.markAsDirty(opts);
- }
- }
-
- /**
- * Marks the control as `pending`.
- * @param {{onlySelf: Boolean}} opts
- * @return {void}
- */
- markAsPending(opts: ControlChangeOpts = {}): void {
- this.status = "PENDING";
-
- if (this._parent && !opts.onlySelf) {
- this._parent.markAsPending(opts);
- }
- }
-
- /**
- * Sets the synchronous validators that are active on this control. Calling
- * this will overwrite any existing sync validators.
- * @param {Function|Function[]|null} newValidator
- * @return {void}
- */
- setValidators(newValidator: ValidatorFn | ValidatorFn[] | null): void {
- this.validator = coerceToValidator(newValidator);
- }
-
- /**
- * Sets the async validators that are active on this control. Calling this
- * will overwrite any existing async validators.
- */
- setAsyncValidators(
- newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null
- ): void {
- this.asyncValidator = coerceToAsyncValidator(newValidator);
- }
-
- /**
- * Sets errors on a form control.
- *
- * This is used when validations are run manually by the user, rather than automatically.
- *
- * Calling `setErrors` will also update the validity of the parent control.
- *
- * ### Example
- *
- * ```
- * const login = new FormControl("someLogin");
- * login.setErrors({
- * "notUnique": true
- * });
- *
- * ```
- * @param {{onlySelf: boolean}} opts
- * @return {void}
- */
- setErrors(errors?: Errors | null, opts: ControlChangeOpts = {}): void {
- this.errors = errors ?? null;
- this.updateControlsErrors(opts.emitEvent !== false);
- }
-
- /**
- * Retrieves a child control given the control's name or path.
- *
- * Paths can be passed in as an array or a string delimited by a dot.
- *
- * To get a control nested within a `person` sub-group:
- *
- * * `this.form.get('person.name');`
- *
- * -OR-
- *
- * * `this.form.get(['person', 'name']);`
- * @param {(String|Number)[]|String} path
- * @return {AbstractControl|null}
- */
- get(
- path: string | Array
- ): AbstractControl | undefined {
- return find(this, path, ".");
- }
-
- /**
- * Returns error data if the control with the given path has the error specified. Otherwise
- * returns null or undefined.
- *
- * If no path is given, it checks for the error on the present control.
- * @param {String} errorCode
- * @param {(String|Number)[]|String} path
- */
- getError(
- errorCode: string,
- path: string | Array
- ): any | null {
- const control: AbstractControl | undefined = path
- ? this.get(path)
- : this;
- return control?.errors ? control.errors[errorCode] : null;
- }
-
- /**
- * Returns true if the control with the given path has the error specified. Otherwise
- * returns false.
- *
- * If no path is given, it checks for the error on the present control.
- * @param {String} errorCode
- * @param {(String|Number)[]|String} path
- * @return {Booelan}
- */
- hasError(errorCode: string, path: string | Array): boolean {
- return Boolean(this.getError(errorCode, path));
- }
-
- /**
- * Empties out the sync validator list.
- */
- clearValidators(): void {
- this.validator = null;
- }
-
- /**
- * Empties out the async validator list.
- */
- clearAsyncValidators(): void {
- this.asyncValidator = null;
- }
-
- /**
- * @param {FormGroup|FormArray} parent
- * @return {Void}
- */
- setParent(parent: FormGroup | FormArray): void {
- this._parent = parent;
- }
-
- /**
- * @param {Boolean} onlySelf
- */
- protected updateAncestors(onlySelf?: boolean, emitEvent?: boolean): void {
- if (this._parent && !onlySelf) {
- const opts = { emitEvent };
- this._parent.updateValueAndValidity(opts);
- this._parent.updatePristine(opts);
- this._parent.updateTouched(opts);
- }
- }
-
- /**
- * @param {String} status
- * @return {Booelan}
- */
- protected anyControlsHaveStatus(status: Status): boolean {
- return this.anyControls((control) => control.status === status);
- }
-
- /**
- * @return {String}
- */
- protected calculateStatus(): Status {
- if (this.allControlsDisabled()) {
- return "DISABLED";
- }
- if (this.errors) {
- return "INVALID";
- }
- if (this.anyControlsHaveStatus("PENDING")) {
- return "PENDING";
- }
- if (this.anyControlsHaveStatus("INVALID")) {
- return "INVALID";
- }
- return "VALID";
- }
-
- protected runValidator(): any | null {
- return this.validator ? this.validator((this as unknown) as BaseControl, this.root) : null;
- }
-
- /**
- * @param {Booelan} emitEvent
- * @return {void}
- */
- protected runAsyncValidator(emitEvent: boolean): void {
- if (this.asyncValidator) {
- this.status = "PENDING";
- const obs = Observable.toObservable(
- this.asyncValidator((this as unknown) as BaseControl, this.root)
- );
- this.asyncValidationSubscription = obs.subscribe((errors) =>
- this.setErrors(errors, { emitEvent })
- );
- }
- }
-
- protected cancelExistingSubscription(): void {
- this.asyncValidationSubscription?.unsubscribe();
- }
-
- /**
- * @param {{onlySelf: boolean}} opts
- * @return {void}
- */
- updatePristine(opts: ControlChangeOpts = {}): void {
- this.pristine = !this.anyControlsDirty();
- if (this._parent && !opts.onlySelf) {
- this._parent.updatePristine(opts);
- }
- }
-
- /**
- * @param {{onlySelf: boolean}} opts
- * @return {void}
- */
- updateTouched(opts: ControlChangeOpts = {}): void {
- this.touched = this.anyControlsTouched();
- if (this._parent && !opts.onlySelf) {
- this._parent.updateTouched(opts);
- }
- }
-
- /**
- * @return {Boolean}
- */
- protected anyControlsDirty(): boolean {
- return this.anyControls((control) => control.dirty);
- }
-
- /**
- * @return {Boolean}
- */
- protected anyControlsTouched(): boolean {
- return this.anyControls((control) => control.touched);
- }
-
- /**
- * @param {Booelan} emitEvent
- * @return {void}
- */
- updateControlsErrors(emitEvent: boolean): void {
- this.status = this.calculateStatus();
- if (emitEvent) {
- this.statusChanges?.next();
- this.stateChanges?.next();
- this.anythingChanges?.next();
- }
- if (this._parent) {
- this._parent.updateControlsErrors(emitEvent);
- }
- }
-
- protected initObservables(): void {
- this.valueChanges = new Observable();
- this.statusChanges = new Observable();
- this.stateChanges = new Observable();
- this.anythingChanges = new Observable();
- }
-
- abstract reset(formState: unknown, opts?: ControlChangeOpts): void;
-
- abstract setValue(value?: V, opts?: ControlChangeOpts): void;
- abstract patchValue(value?: V, opts?: ControlChangeOpts): void;
-
- protected abstract updateValue(): void;
-
- protected abstract forEachChild(
- callback: (
- control: AbstractControl,
- nameOrIndex?: string | number
- ) => void
- ): void;
-
- protected abstract allControlsDisabled(): boolean;
-
- protected abstract anyControls(
- callback: (control: AbstractControl) => boolean
- ): boolean;
-
- registerOnCollectionChange(fn: () => void = () => {}): void {
- this.onCollectionChange = fn;
- }
-
- triggerOnCollectionChange(): void {
- if (this.onCollectionChange) {
- this.onCollectionChange();
- }
- }
+ pristine: boolean;
+
+ errors: Errors | null;
+
+ validator: ValidatorFn | null;
+ asyncValidator: AsyncValidatorFn | null;
+
+ valueChanges!: Observable;
+ statusChanges!: Observable;
+ stateChanges!: Observable;
+ anythingChanges!: Observable;
+
+ protected _parent?: FormGroup | FormArray;
+ protected pendingChange: boolean;
+ protected pendingDirty: boolean;
+ protected pendingTouched: boolean;
+
+ private onCollectionChange?: () => void;
+
+ private asyncValidationSubscription?: Subscription;
+
+ /**
+ * @param {Function|null} validator
+ * @param {Function|null} asyncValidator
+ */
+ constructor(
+ validator: ValidatorFn | null,
+ asyncValidator: AsyncValidatorFn | null
+ ) {
+ this.validator = validator;
+ this.asyncValidator = asyncValidator;
+ /**
+ * A control is marked `touched` once the user has triggered
+ * a `blur` event on it.
+ */
+ this.touched = false;
+ /**
+ * A control is `pristine` if the user has not yet changed
+ * the value in the UI.
+ *
+ * Note that programmatic changes to a control's value will
+ * *not* mark it dirty.
+ */
+ this.pristine = true;
+
+ this.errors = null;
+
+ this.pendingChange = false;
+ this.pendingDirty = false;
+ this.pendingTouched = false;
+
+ // this.hasError = this.hasError.bind(this);
+ // this.getError = this.getError.bind(this);
+ // this.reset = this.reset.bind(this);
+ // this.get = this.get.bind(this);
+ // this.patchValue = this.patchValue.bind(this);
+ // this.setValue = this.setValue.bind(this);
+ }
+
+ abstract getRawValue(): any;
+
+ /**
+ * A control is `dirty` if the user has changed the value
+ * in the UI.
+ *
+ * Note that programmatic changes to a control's value will
+ * *not* mark it dirty.
+ * @return {Boolean}
+ */
+ get dirty(): boolean {
+ return !this.pristine;
+ }
+
+ /**
+ * A control is `valid` when its `status === VALID`.
+ *
+ * In order to have this status, the control must have passed all its
+ * validation checks.
+ * @return {Boolean}
+ */
+ get valid(): boolean {
+ return this.status === "VALID";
+ }
+
+ /**
+ * A control is `invalid` when its `status === INVALID`.
+ *
+ * In order to have this status, the control must have failed
+ * at least one of its validation checks.
+ * @return {Boolean}
+ */
+ get invalid(): boolean {
+ return this.status === "INVALID";
+ }
+
+ /**
+ * A control is `pending` when its `status === PENDING`.
+ *
+ * In order to have this status, the control must be in the
+ * middle of conducting a validation check.
+ */
+ get pending(): boolean {
+ return this.status === "PENDING";
+ }
+
+ /**
+ * The parent control.
+ * * @return {FormGroup|FormArray}
+ */
+ get parent(): FormGroup | FormArray | undefined {
+ return this._parent;
+ }
+
+ /**
+ * A control is `untouched` if the user has not yet triggered
+ * a `blur` event on it.
+ * @return {Boolean}
+ */
+ get untouched(): boolean {
+ return !this.touched;
+ }
+
+ /**
+ * A control is `enabled` as long as its `status !== DISABLED`.
+ *
+ * In other words, it has a status of `VALID`, `INVALID`, or
+ * `PENDING`.
+ * @return {Boolean}
+ */
+ get enabled(): boolean {
+ return this.status !== "DISABLED";
+ }
+
+ /**
+ * A control is disabled if it's status is `DISABLED`
+ */
+ get disabled(): boolean {
+ return this.status === "DISABLED";
+ }
+
+ /**
+ * Retrieves the top-level ancestor of this control.
+ * @return {AbstractControl}
+ */
+ get root(): AbstractControl {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ let x: AbstractControl = this;
+ while (x._parent) {
+ x = x._parent;
+ }
+ return x;
+ }
+
+ setInitialStatus(): void {
+ if (this.disabled) {
+ this.status = "DISABLED";
+ } else {
+ this.status = "VALID";
+ }
+ }
+
+ emitEvents(onlySelf?: boolean): void {
+ this.stateChanges?.next();
+ this.statusChanges?.next(this.status);
+ this.valueChanges?.next(this.value);
+ this.anythingChanges?.next();
+
+ if (this._parent && !onlySelf) {
+ this._parent.emitEvents();
+ }
+ }
+
+ /**
+ * Disables the control. This means the control will be exempt from validation checks and
+ * excluded from the aggregate value of any parent. Its status is `DISABLED`.
+ *
+ * If the control has children, all children will be disabled to maintain the model.
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
+ * @return {void}
+ */
+ disable(opts: ControlChangeOpts = {}): void {
+ this.status = "DISABLED";
+ this.errors = null;
+ this.forEachChild((control) => {
+ control.disable({
+ emitEvent: false,
+ onlySelf: true,
+ });
+ });
+ this.updateValueAndValidity({
+ onlySelf: true,
+ emitEvent: opts.emitEvent,
+ });
+ this.updateAncestors(opts.onlySelf, opts.emitEvent);
+ }
+
+ /**
+ * Enables the control. This means the control will be included in validation checks and
+ * the aggregate value of its parent. Its status is re-calculated based on its value and
+ * its validators.
+ *
+ * If the control has children, all children will be enabled.
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
+ * @return {void}
+ */
+ enable(opts: ControlChangeOpts = {}): void {
+ this.status = "VALID";
+
+ this.forEachChild((control) => {
+ control.enable({
+ emitEvent: false,
+ onlySelf: true,
+ });
+ });
+ this.updateValueAndValidity({
+ onlySelf: true,
+ emitEvent: opts.emitEvent,
+ });
+ this.updateAncestors(opts.onlySelf, opts.emitEvent);
+ }
+
+ /**
+ * Re-calculates the value and validation status of the control.
+ *
+ * By default, it will also update the value and validity of its ancestors.
+ * @param {{onlySelf: Boolean, emitEvent: Booelan}} options
+ */
+ updateValueAndValidity(opts: ControlChangeOpts = {}): void {
+ this.setInitialStatus();
+ this.updateValue();
+ if (this.enabled) {
+ this.cancelExistingSubscription();
+ this.errors = this.runValidator();
+ this.status = this.calculateStatus();
+ if (this.status === "VALID" || this.status === "PENDING") {
+ this.runAsyncValidator(opts.emitEvent !== false);
+ }
+ }
+ if (opts.emitEvent !== false) {
+ this.emitEvents(opts.onlySelf);
+ }
+ if (this._parent && !opts.onlySelf) {
+ this._parent.updateValueAndValidity(opts);
+ }
+ }
+
+ /**
+ * Marks the control as `touched`.
+ *
+ * This will also mark all direct ancestors as `touched` to maintain
+ * the model.
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
+ * @return {void}
+ */
+ markAsTouched(opts: ControlChangeOpts = {}): void {
+ this.touched = true;
+ if (this._parent && !opts.onlySelf) {
+ this._parent.markAsTouched(opts);
+ }
+ if (opts.emitEvent) {
+ this.stateChanges?.next();
+ }
+ }
+
+ /**
+ * Marks the control as `pristine`.
+ *
+ * If the control has any children, it will also mark all children as `pristine`
+ * to maintain the model, and re-calculate the `pristine` status of all parent
+ * controls.
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
+ * @return {void}
+ */
+ markAsPristine(opts: ControlChangeOpts = {}): void {
+ this.pristine = true;
+ this.pendingDirty = false;
+ if (opts.emitEvent) {
+ this.stateChanges?.next();
+ }
+ this.forEachChild((control) => {
+ control.markAsPristine({
+ onlySelf: true,
+ });
+ });
+ if (this._parent && !opts.onlySelf) {
+ this._parent.updatePristine(opts);
+ }
+ }
+
+ /**
+ * Marks the control as `untouched`.
+ *
+ * If the control has any children, it will also mark all children as `untouched`
+ * to maintain the model, and re-calculate the `touched` status of all parent
+ * controls.
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
+ * @return {void}
+ */
+ markAsUntouched(opts: ControlChangeOpts = {}): void {
+ this.touched = false;
+ this.pendingTouched = false;
+ this.forEachChild((control) => {
+ control.markAsUntouched({
+ onlySelf: true,
+ });
+ });
+ if (this._parent && !opts.onlySelf) {
+ this._parent.updateTouched(opts);
+ }
+ if (opts.emitEvent) {
+ this.stateChanges.next();
+ }
+ }
+
+ /**
+ * Marks the control as `dirty`.
+ *
+ * This will also mark all direct ancestors as `dirty` to maintain
+ * the model.
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
+ * @return {void}
+ */
+ markAsDirty(opts: ControlChangeOpts = {}): void {
+ this.pristine = false;
+ if (opts.emitEvent) {
+ this.stateChanges?.next();
+ }
+ if (this._parent && !opts.onlySelf) {
+ this._parent.markAsDirty(opts);
+ }
+ }
+
+ /**
+ * Marks the control as `pending`.
+ * @param {{onlySelf: Boolean}} opts
+ * @return {void}
+ */
+ markAsPending(opts: ControlChangeOpts = {}): void {
+ this.status = "PENDING";
+
+ if (this._parent && !opts.onlySelf) {
+ this._parent.markAsPending(opts);
+ }
+ }
+
+ /**
+ * Sets the synchronous validators that are active on this control. Calling
+ * this will overwrite any existing sync validators.
+ * @param {Function|Function[]|null} newValidator
+ * @return {void}
+ */
+ setValidators(
+ newValidator: ValidatorFn | ValidatorFn[] | null
+ ): void {
+ this.validator = coerceToValidator(newValidator);
+ }
+
+ /**
+ * Sets the async validators that are active on this control. Calling this
+ * will overwrite any existing async validators.
+ */
+ setAsyncValidators(
+ newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null
+ ): void {
+ this.asyncValidator = coerceToAsyncValidator(newValidator);
+ }
+
+ /**
+ * Sets errors on a form control.
+ *
+ * This is used when validations are run manually by the user, rather than automatically.
+ *
+ * Calling `setErrors` will also update the validity of the parent control.
+ *
+ * ### Example
+ *
+ * ```
+ * const login = new FormControl("someLogin");
+ * login.setErrors({
+ * "notUnique": true
+ * });
+ *
+ * ```
+ * @param {{onlySelf: boolean}} opts
+ * @return {void}
+ */
+ setErrors(errors?: Errors | null, opts: ControlChangeOpts = {}): void {
+ this.errors = errors ?? null;
+ this.updateControlsErrors(opts.emitEvent !== false);
+ }
+
+ /**
+ * Retrieves a child control given the control's name or path.
+ *
+ * Paths can be passed in as an array or a string delimited by a dot.
+ *
+ * To get a control nested within a `person` sub-group:
+ *
+ * * `this.form.get('person.name');`
+ *
+ * -OR-
+ *
+ * * `this.form.get(['person', 'name']);`
+ * @param {(String|Number)[]|String} path
+ * @return {AbstractControl|null}
+ */
+ get(path: string | Array): AbstractControl | undefined {
+ return find(this, path, ".");
+ }
+
+ /**
+ * Returns error data if the control with the given path has the error specified. Otherwise
+ * returns null or undefined.
+ *
+ * If no path is given, it checks for the error on the present control.
+ * @param {String} errorCode
+ * @param {(String|Number)[]|String} path
+ */
+ getError(
+ errorCode: string,
+ path: string | Array
+ ): any | null {
+ const control: AbstractControl | undefined = path
+ ? this.get(path)
+ : this;
+ return control?.errors ? control.errors[errorCode] : null;
+ }
+
+ /**
+ * Returns true if the control with the given path has the error specified. Otherwise
+ * returns false.
+ *
+ * If no path is given, it checks for the error on the present control.
+ * @param {String} errorCode
+ * @param {(String|Number)[]|String} path
+ * @return {Booelan}
+ */
+ hasError(
+ errorCode: string,
+ path: string | Array
+ ): boolean {
+ return Boolean(this.getError(errorCode, path));
+ }
+
+ /**
+ * Empties out the sync validator list.
+ */
+ clearValidators(): void {
+ this.validator = null;
+ }
+
+ /**
+ * Empties out the async validator list.
+ */
+ clearAsyncValidators(): void {
+ this.asyncValidator = null;
+ }
+
+ /**
+ * @param {FormGroup|FormArray} parent
+ * @return {Void}
+ */
+ setParent(parent: FormGroup | FormArray): void {
+ this._parent = parent;
+ }
+
+ /**
+ * @param {Boolean} onlySelf
+ */
+ protected updateAncestors(onlySelf?: boolean, emitEvent?: boolean): void {
+ if (this._parent && !onlySelf) {
+ const opts = { emitEvent };
+ this._parent.updateValueAndValidity(opts);
+ this._parent.updatePristine(opts);
+ this._parent.updateTouched(opts);
+ }
+ }
+
+ /**
+ * @param {String} status
+ * @return {Booelan}
+ */
+ protected anyControlsHaveStatus(status: Status): boolean {
+ return this.anyControls((control) => control.status === status);
+ }
+
+ /**
+ * @return {String}
+ */
+ protected calculateStatus(): Status {
+ if (this.allControlsDisabled()) {
+ return "DISABLED";
+ }
+ if (this.errors) {
+ return "INVALID";
+ }
+ if (this.anyControlsHaveStatus("PENDING")) {
+ return "PENDING";
+ }
+ if (this.anyControlsHaveStatus("INVALID")) {
+ return "INVALID";
+ }
+ return "VALID";
+ }
+
+ protected runValidator(): any | null {
+ return this.validator
+ ? this.validator((this as unknown) as BaseControl, this.root)
+ : null;
+ }
+
+ /**
+ * @param {Booelan} emitEvent
+ * @return {void}
+ */
+ protected runAsyncValidator(emitEvent: boolean): void {
+ if (this.asyncValidator) {
+ this.status = "PENDING";
+ const obs = Observable.toObservable(
+ this.asyncValidator(
+ (this as unknown) as BaseControl,
+ this.root
+ )
+ );
+ this.asyncValidationSubscription = obs.subscribe((errors) =>
+ this.setErrors(errors, { emitEvent })
+ );
+ }
+ }
+
+ protected cancelExistingSubscription(): void {
+ this.asyncValidationSubscription?.unsubscribe();
+ }
+
+ /**
+ * @param {{onlySelf: boolean}} opts
+ * @return {void}
+ */
+ updatePristine(opts: ControlChangeOpts = {}): void {
+ this.pristine = !this.anyControlsDirty();
+ if (this._parent && !opts.onlySelf) {
+ this._parent.updatePristine(opts);
+ }
+ }
+
+ /**
+ * @param {{onlySelf: boolean}} opts
+ * @return {void}
+ */
+ updateTouched(opts: ControlChangeOpts = {}): void {
+ this.touched = this.anyControlsTouched();
+ if (this._parent && !opts.onlySelf) {
+ this._parent.updateTouched(opts);
+ }
+ }
+
+ /**
+ * @return {Boolean}
+ */
+ protected anyControlsDirty(): boolean {
+ return this.anyControls((control) => control.dirty);
+ }
+
+ /**
+ * @return {Boolean}
+ */
+ protected anyControlsTouched(): boolean {
+ return this.anyControls((control) => control.touched);
+ }
+
+ /**
+ * @param {Booelan} emitEvent
+ * @return {void}
+ */
+ updateControlsErrors(emitEvent: boolean): void {
+ this.status = this.calculateStatus();
+ if (emitEvent) {
+ this.statusChanges?.next();
+ this.stateChanges?.next();
+ this.anythingChanges?.next();
+ }
+ if (this._parent) {
+ this._parent.updateControlsErrors(emitEvent);
+ }
+ }
+
+ protected initObservables(): void {
+ this.valueChanges = new Observable();
+ this.statusChanges = new Observable();
+ this.stateChanges = new Observable();
+ this.anythingChanges = new Observable();
+ }
+
+ abstract reset(formState: unknown, opts?: ControlChangeOpts): void;
+
+ abstract setValue(value?: V, opts?: ControlChangeOpts): void;
+ abstract patchValue(value?: V, opts?: ControlChangeOpts): void;
+
+ protected abstract updateValue(): void;
+
+ protected abstract forEachChild(
+ callback: (
+ control: AbstractControl,
+ nameOrIndex?: string | number
+ ) => void
+ ): void;
+
+ protected abstract allControlsDisabled(): boolean;
+
+ protected abstract anyControls(
+ callback: (control: AbstractControl) => boolean
+ ): boolean;
+
+ registerOnCollectionChange(fn: () => void = () => {}): void {
+ this.onCollectionChange = fn;
+ }
+
+ triggerOnCollectionChange(): void {
+ if (this.onCollectionChange) {
+ this.onCollectionChange();
+ }
+ }
}
export class FormControl extends AbstractControl {
- input: InputBase;
+ input: InputBase;
- active: boolean;
+ active: boolean;
- private pendingValue?: V;
+ private pendingValue?: V;
- constructor(
- input: InputBase,
- formState: unknown,
- validatorOrOpts?: ValidatorOrOpts,
- asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
- ) {
- super(
- coerceToValidator(validatorOrOpts),
- coerceToAsyncValidator(asyncValidator, validatorOrOpts)
- );
- this.input = input;
- this.applyFormState(formState);
- this.pendingChange = true;
- this.pendingDirty = false;
- this.pendingTouched = false;
-
- /**
- * A control is `active` when its focused.
- */
- this.active = false;
-
- this.updateValueAndValidity({
- onlySelf: true,
- emitEvent: false,
- });
- this.initObservables();
- }
-
- /**
- * A control is `inactive` when its not focused.
- * @return {Boolean}
- */
- get inactive(): boolean {
- return !this.active;
- }
-
- getRawValue(): V | undefined {
- return this.value;
- }
-
- /**
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} options
- * @return {void}
- */
- setValue(value?: V, options: ControlChangeOpts = {}): void {
- this.value = this.pendingValue = value;
- this.updateValueAndValidity(options);
- }
-
- /**
- * Patches the value of a control.
- *
- * This function is functionally the same as setValue at this level.
- * It exists for symmetry with patchValue on `FormGroups` and
- * `FormArrays`, where it does behave differently.
- * @param {any} value
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} options
- * @return {void}
- */
- patchValue(value?: V, options: ControlChangeOpts = {}): void {
- this.setValue(value, options);
- }
-
- /**
- * @param {{onlySelf: Boolean, emitEvent: Boolean}} options
- * @return {void}
- */
- reset(formState: any = null, options: ControlChangeOpts = {}): void {
- this.applyFormState(formState);
- this.markAsPristine(options);
- this.markAsUntouched(options);
- this.setValue(this.value as V, options);
- this.pendingChange = false;
- }
-
- protected updateValue(): void {
- // do nothing
- }
-
- protected forEachChild(_: unknown): void {
- // do nothing
- }
-
- /**
- * @param {Function} condition
- * @return {Boolean}
- */
- protected anyControls(_: unknown): boolean {
- return false;
- }
-
- /**
- * @return {Boolean}
- */
- protected allControlsDisabled(): boolean {
- return this.disabled;
- }
-
- /**
- * @return {Boolean}
- */
- private isBoxedValue(formState: any) {
- return (
- typeof formState === "object" &&
- formState !== null &&
- Object.keys(formState).length === 2 &&
- "value" in formState &&
- "disabled" in formState
- );
- }
-
- private applyFormState(formState: any) {
- if (this.isBoxedValue(formState)) {
- this.value = this.pendingValue = formState.value;
- if (formState.disabled) {
- this.disable({
- onlySelf: true,
- emitEvent: false,
- });
- } else {
- this.enable({
- onlySelf: true,
- emitEvent: false,
+ constructor(
+ input: InputBase,
+ formState: unknown,
+ validatorOrOpts?: ValidatorOrOpts,
+ asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
+ ) {
+ super(
+ coerceToValidator(validatorOrOpts),
+ coerceToAsyncValidator(asyncValidator, validatorOrOpts)
+ );
+ this.input = input;
+ this.applyFormState(formState);
+ this.pendingChange = true;
+ this.pendingDirty = false;
+ this.pendingTouched = false;
+
+ /**
+ * A control is `active` when its focused.
+ */
+ this.active = false;
+
+ this.updateValueAndValidity({
+ onlySelf: true,
+ emitEvent: false,
});
- }
- } else {
- this.value = this.pendingValue = formState;
+ this.initObservables();
+ }
+
+ /**
+ * A control is `inactive` when its not focused.
+ * @return {Boolean}
+ */
+ get inactive(): boolean {
+ return !this.active;
+ }
+
+ getRawValue(): V | undefined {
+ return this.value;
+ }
+
+ /**
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} options
+ * @return {void}
+ */
+ setValue(value?: V, options: ControlChangeOpts = {}): void {
+ this.value = this.pendingValue = value;
+ this.updateValueAndValidity(options);
+ }
+
+ /**
+ * Patches the value of a control.
+ *
+ * This function is functionally the same as setValue at this level.
+ * It exists for symmetry with patchValue on `FormGroups` and
+ * `FormArrays`, where it does behave differently.
+ * @param {any} value
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} options
+ * @return {void}
+ */
+ patchValue(value?: V, options: ControlChangeOpts = {}): void {
+ this.setValue(value, options);
+ }
+
+ /**
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} options
+ * @return {void}
+ */
+ reset(formState: any = null, options: ControlChangeOpts = {}): void {
+ this.applyFormState(formState);
+ this.markAsPristine(options);
+ this.markAsUntouched(options);
+ this.setValue(this.value as V, options);
+ this.pendingChange = false;
+ }
+
+ protected updateValue(): void {
+ // do nothing
+ }
+
+ protected forEachChild(_: unknown): void {
+ // do nothing
+ }
+
+ /**
+ * @param {Function} condition
+ * @return {Boolean}
+ */
+ protected anyControls(_: unknown): boolean {
+ return false;
+ }
+
+ /**
+ * @return {Boolean}
+ */
+ protected allControlsDisabled(): boolean {
+ return this.disabled;
+ }
+
+ /**
+ * @return {Boolean}
+ */
+ private isBoxedValue(formState: any) {
+ return (
+ typeof formState === "object" &&
+ formState !== null &&
+ Object.keys(formState).length === 2 &&
+ "value" in formState &&
+ "disabled" in formState
+ );
+ }
+
+ private applyFormState(formState: any) {
+ if (this.isBoxedValue(formState)) {
+ this.value = this.pendingValue = formState.value;
+ if (formState.disabled) {
+ this.disable({
+ onlySelf: true,
+ emitEvent: false,
+ });
+ } else {
+ this.enable({
+ onlySelf: true,
+ emitEvent: false,
+ });
+ }
+ } else {
+ this.value = this.pendingValue = formState;
+ }
}
- }
}
-export class FormGroup<
- V extends FormGroupValue
-> extends AbstractControl {
- group: InputGroup;
- controls: NamedControlsMap;
-
- constructor(
- group: InputGroup,
- controls: NamedControlsMap,
- validatorOrOpts?: ValidatorOrOpts,
- asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
- ) {
- super(
- coerceToValidator(validatorOrOpts),
- coerceToAsyncValidator(asyncValidator, validatorOrOpts)
- );
- this.group = group || {};
- this.controls = controls || {};
- this.initObservables();
- this.setUpControls();
- this.updateValueAndValidity({
- onlySelf: true,
- emitEvent: false,
- });
- }
-
- /**
- * Check whether there is an enabled control with the given name in the group.
- *
- * It will return false for disabled controls. If you'd like to check for existence in the group
- * only, use `AbstractControl` get instead.
- * @param {String} controlName
- * @return {Boolean}
- */
- contains(controlName: string): boolean {
- return Boolean(this.controls[controlName]?.enabled);
- }
-
- /**
- * Registers a control with the group's list of controls.
- *
- * This method does not update the value or validity of the control, so for most cases you'll want
- * to use addControl instead.
- * @param {String} name
- * @param {AbstractControl} control
- * @return {AbstractControl}
- */
- registerControl(
- name: string,
- control: AbstractControl
- ): AbstractControl {
- if (this.controls[name]) return this.controls[name];
- this.controls[name] = control;
- control.setParent(this as any);
- control.registerOnCollectionChange(this._onCollectionChange);
- return control;
- }
-
- /**
- * Add a control to this group.
- * @param {String} name
- * @param {AbstractControl} control
- * @return {void}
- */
- addControl(
- name: string,
- control: AbstractControl
- ): void {
- this.registerControl(name, control);
- this.updateValueAndValidity();
- this._onCollectionChange();
- }
-
- /**
- * Remove a control from this group.
- * @param {String} name
- * @return {void}
- */
- removeControl(name: string): void {
- if (this.controls[name]) this.controls[name].registerOnCollectionChange();
- delete this.controls[name];
- this.updateValueAndValidity();
- this._onCollectionChange();
- }
-
- /**
- * Replace an existing control.
- * @param {String} name
- * @param {AbstractControl} control
- * @return {void}
- */
- setControl(
- name: string,
- control: AbstractControl
- ): void {
- if (this.controls[name]) {
- this.controls[name].registerOnCollectionChange();
- delete this.controls[name];
- }
- if (control) {
- this.registerControl(name, control);
- }
- this.updateValueAndValidity();
- this._onCollectionChange();
- }
-
- /**
- * Sets the value of the FormGroup. It accepts an object that matches
- * the structure of the group, with control names as keys.
- *
- * This method performs strict checks, so it will throw 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.
- *
- * ### Example
- * form.setValue({first: 'Jon', last: 'Snow'});
- * console.log(form.value); // {first: 'Jon', last: 'Snow'}
- * @param {{[key: string]: any}} value
- * @param {{onlySelf: boolean, emitEvent: boolean}} options
- * @return {void}
- */
- setValue(value?: V, options: ControlChangeOpts = {}): void {
- this.checkAllValuesPresent(value);
- if (value) {
- Object.keys(value).forEach((name) => {
- this.throwIfControlMissing(name);
- this.controls[name].setValue(value[name], {
- onlySelf: true,
- emitEvent: options.emitEvent,
+export class FormGroup extends AbstractControl {
+ group: InputGroup;
+ controls: NamedControlsMap;
+
+ constructor(
+ group: InputGroup,
+ controls: NamedControlsMap,
+ validatorOrOpts?: ValidatorOrOpts,
+ asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
+ ) {
+ super(
+ coerceToValidator(validatorOrOpts),
+ coerceToAsyncValidator(asyncValidator, validatorOrOpts)
+ );
+ this.group = group || {};
+ this.controls = controls || {};
+ this.initObservables();
+ this.setUpControls();
+ this.updateValueAndValidity({
+ onlySelf: true,
+ emitEvent: false,
});
- });
- }
- this.updateValueAndValidity(options);
- }
-
- /**
- * Resets the `FormGroup`.
- * @param {any} value
- * @param {{onlySelf: boolean, emitEvent: boolean}} options
- * @return {void}
- */
- reset(value?: unknown, options: ControlChangeOpts = {}): void {
- const v = value as any;
- this.forEachChild((control, name) => {
- control.reset(name && v ? v[name] : null, {
- onlySelf: true,
- emitEvent: options.emitEvent,
- });
- });
- this.updateValueAndValidity(options);
- this.updatePristine(options);
- this.updateTouched(options);
- }
-
- /**
- * Patches the value of the FormGroup. It accepts an object with control
- * names as keys, and will do 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.
- *
- * ### Example
- * ```
- * console.log(form.value); // {first: null, last: null}
- *
- * form.patchValue({first: 'Jon'});
- * console.log(form.value); // {first: 'Jon', last: null}
- *
- * ```
- * @param {{[key: string]: any}} value
- * @param {{onlySelf: boolean, emitEvent: boolean}} options
- * @return {void}
- */
- patchValue(value?: V, options: ControlChangeOpts = {}): void {
- if (value) {
- Object.keys(value).forEach((name) => {
- if (this.controls[name]) {
- this.controls[name].patchValue(value[name], {
+ }
+
+ /**
+ * Check whether there is an enabled control with the given name in the group.
+ *
+ * It will return false for disabled controls. If you'd like to check for existence in the group
+ * only, use `AbstractControl` get instead.
+ * @param {String} controlName
+ * @return {Boolean}
+ */
+ contains(controlName: string): boolean {
+ return Boolean(this.controls[controlName]?.enabled);
+ }
+
+ /**
+ * Registers a control with the group's list of controls.
+ *
+ * This method does not update the value or validity of the control, so for most cases you'll want
+ * to use addControl instead.
+ * @param {String} name
+ * @param {AbstractControl} control
+ * @return {AbstractControl}
+ */
+ registerControl(
+ name: string,
+ control: AbstractControl
+ ): AbstractControl {
+ if (this.controls[name]) return this.controls[name];
+ this.controls[name] = control;
+ control.setParent(this as any);
+ control.registerOnCollectionChange(this._onCollectionChange);
+ return control;
+ }
+
+ /**
+ * Enables the control. This means the control will be included in validation checks and
+ * the aggregate value of its parent. Its status is re-calculated based on its value and
+ * its validators.
+ *
+ * If the control has children, all children will be enabled.
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
+ * @return {void}
+ */
+ enable(opts: ControlChangeOpts = {}): void {
+ this.status = "VALID";
+
+ const isHidden = this.group.hidden && this.group.hidden(opts.form);
+
+ this.forEachChild((control) => {
+ const childOpts = {
+ emitEvent: false,
+ onlySelf: true,
+ };
+
+ if (isHidden) {
+ control.disable(childOpts);
+ } else {
+ control.enable(childOpts);
+ }
+ });
+ this.updateValueAndValidity({
onlySelf: true,
- emitEvent: options.emitEvent,
- });
+ emitEvent: opts.emitEvent,
+ });
+ this.updateAncestors(opts.onlySelf, opts.emitEvent);
+ }
+
+ /**
+ * Add a control to this group.
+ * @param {String} name
+ * @param {AbstractControl} control
+ * @return {void}
+ */
+ addControl(name: string, control: AbstractControl): void {
+ this.registerControl(name, control);
+ this.updateValueAndValidity();
+ this._onCollectionChange();
+ }
+
+ /**
+ * Remove a control from this group.
+ * @param {String} name
+ * @return {void}
+ */
+ removeControl(name: string): void {
+ if (this.controls[name])
+ this.controls[name].registerOnCollectionChange();
+ delete this.controls[name];
+ this.updateValueAndValidity();
+ this._onCollectionChange();
+ }
+
+ /**
+ * Replace an existing control.
+ * @param {String} name
+ * @param {AbstractControl} control
+ * @return {void}
+ */
+ setControl(name: string, control: AbstractControl): void {
+ if (this.controls[name]) {
+ this.controls[name].registerOnCollectionChange();
+ delete this.controls[name];
}
- });
- }
- this.updateValueAndValidity(options);
- }
-
- /**
- * The aggregate value of the FormGroup, including any disabled controls.
- *
- * If you'd like to include all values regardless of disabled status, use this method.
- * Otherwise, the `value` property is the best way to get the value of the group.
- */
- getRawValue(): any {
- return this.reduceChildren({} as any, (acc, control, name) => {
- acc[name] = control.getRawValue();
- return acc;
- });
- }
-
- /**
- * @param {{(v: any, k: String) => void}} callback
- * @return {void}
- */
- forEachChild(
- callback: (control: AbstractControl, name?: string) => void
- ): void {
- Object.keys(this.controls).forEach((name) =>
- callback(this.controls[name], name)
- );
- }
-
- // TODO, do we need this?
- private _onCollectionChange(): void {
- // do nothing
- }
-
- /**
- * @param {Function} condition
- * @return {Boolean}
- */
- anyControls(callback: (control: AbstractControl) => boolean): boolean {
- let res = false;
- this.forEachChild((control, name) => {
- res = res || Boolean(name && this.contains(name) && callback(control));
- });
- return res;
- }
-
- updateValue(): void {
- this.value = this.reduceValue();
- }
-
- private reduceValue(): V {
- return this.reduceChildren({} as any, (value, control, name) => {
- if (control.enabled || this.disabled) {
- value[name] = control.value;
- }
- return value;
- });
- }
-
- /**
- * @param {Function} fn
- */
- private reduceChildren(
- initValue: U,
- fn: (accumilated: U, control: AbstractControl, name: string) => U
- ): U {
- let res = initValue;
- this.forEachChild((control, name) => {
- res = fn(res, control, name as string);
- });
- return res;
- }
-
- private setUpControls(): void {
- this.forEachChild((control) => {
- control.setParent(this as any);
- control.registerOnCollectionChange(this._onCollectionChange);
- });
- }
-
- /**
- * @return {Boolean}
- */
- protected allControlsDisabled(): boolean {
- for (const controlName of Object.keys(this.controls)) {
- if (this.controls[controlName].enabled) {
- return false;
- }
+ if (control) {
+ this.registerControl(name, control);
+ }
+ this.updateValueAndValidity();
+ this._onCollectionChange();
+ }
+
+ /**
+ * Sets the value of the FormGroup. It accepts an object that matches
+ * the structure of the group, with control names as keys.
+ *
+ * This method performs strict checks, so it will throw 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.
+ *
+ * ### Example
+ * form.setValue({first: 'Jon', last: 'Snow'});
+ * console.log(form.value); // {first: 'Jon', last: 'Snow'}
+ * @param {{[key: string]: any}} value
+ * @param {{onlySelf: boolean, emitEvent: boolean}} options
+ * @return {void}
+ */
+ setValue(value?: V, options: ControlChangeOpts = {}): void {
+ this.checkAllValuesPresent(value);
+ if (value) {
+ Object.keys(value).forEach((name) => {
+ this.throwIfControlMissing(name);
+ this.controls[name].setValue(value[name], {
+ onlySelf: true,
+ emitEvent: options.emitEvent,
+ });
+ });
+ }
+ this.updateValueAndValidity(options);
+ }
+
+ /**
+ * Resets the `FormGroup`.
+ * @param {any} value
+ * @param {{onlySelf: boolean, emitEvent: boolean}} options
+ * @return {void}
+ */
+ reset(value?: unknown, options: ControlChangeOpts = {}): void {
+ const v = value as any;
+ this.forEachChild((control, name) => {
+ control.reset(name && v ? v[name] : null, {
+ onlySelf: true,
+ emitEvent: options.emitEvent,
+ });
+ });
+ this.updateValueAndValidity(options);
+ this.updatePristine(options);
+ this.updateTouched(options);
}
- return Object.keys(this.controls).length > 0 || this.disabled;
- }
- private checkAllValuesPresent(value: any) {
- this.forEachChild((control, name) => {
- if (value[name as string] === undefined) {
- throw new Error(
- `Must supply a value for form control with name: '${name}'.`
+ /**
+ * Patches the value of the FormGroup. It accepts an object with control
+ * names as keys, and will do 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.
+ *
+ * ### Example
+ * ```
+ * console.log(form.value); // {first: null, last: null}
+ *
+ * form.patchValue({first: 'Jon'});
+ * console.log(form.value); // {first: 'Jon', last: null}
+ *
+ * ```
+ * @param {{[key: string]: any}} value
+ * @param {{onlySelf: boolean, emitEvent: boolean}} options
+ * @return {void}
+ */
+ patchValue(value?: V, options: ControlChangeOpts = {}): void {
+ if (value) {
+ Object.keys(value).forEach((name) => {
+ if (this.controls[name]) {
+ this.controls[name].patchValue(value[name], {
+ onlySelf: true,
+ emitEvent: options.emitEvent,
+ });
+ }
+ });
+ }
+ this.updateValueAndValidity(options);
+ }
+
+ /**
+ * The aggregate value of the FormGroup, including any disabled controls.
+ *
+ * If you'd like to include all values regardless of disabled status, use this method.
+ * Otherwise, the `value` property is the best way to get the value of the group.
+ */
+ getRawValue(): any {
+ return this.reduceChildren({} as any, (acc, control, name) => {
+ acc[name] = control.getRawValue();
+ return acc;
+ });
+ }
+
+ /**
+ * @param {{(v: any, k: String) => void}} callback
+ * @return {void}
+ */
+ forEachChild(
+ callback: (control: AbstractControl, name?: string) => void
+ ): void {
+ Object.keys(this.controls).forEach((name) =>
+ callback(this.controls[name], name)
);
- }
- });
- }
+ }
+
+ // TODO, do we need this?
+ private _onCollectionChange(): void {
+ // do nothing
+ }
+
+ /**
+ * @param {Function} condition
+ * @return {Boolean}
+ */
+ anyControls(callback: (control: AbstractControl) => boolean): boolean {
+ let res = false;
+ this.forEachChild((control, name) => {
+ res =
+ res ||
+ Boolean(name && this.contains(name) && callback(control));
+ });
+ return res;
+ }
+
+ updateValue(): void {
+ this.value = this.reduceValue();
+ }
+
+ private reduceValue(): V {
+ return this.reduceChildren({} as any, (value, control, name) => {
+ if (control.enabled || this.disabled) {
+ value[name] = control.value;
+ }
+ return value;
+ });
+ }
- private throwIfControlMissing(name: string): void {
- if (!Object.keys(this.controls).length) {
- throw new Error(
- "There are no form controls registered with this group yet."
- );
+ /**
+ * @param {Function} fn
+ */
+ private reduceChildren(
+ initValue: U,
+ fn: (accumilated: U, control: AbstractControl, name: string) => U
+ ): U {
+ let res = initValue;
+ this.forEachChild((control, name) => {
+ res = fn(res, control, name as string);
+ });
+ return res;
}
- if (!this.controls[name]) {
- throw new Error(`Cannot find form control with name: ${name}.`);
+
+ private setUpControls(): void {
+ this.forEachChild((control) => {
+ control.setParent(this as any);
+ control.registerOnCollectionChange(this._onCollectionChange);
+ });
+ }
+
+ /**
+ * @return {Boolean}
+ */
+ protected 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;
+ }
+
+ private checkAllValuesPresent(value: any) {
+ this.forEachChild((control, name) => {
+ if (value[name as string] === undefined) {
+ throw new Error(
+ `Must supply a value for form control with name: '${name}'.`
+ );
+ }
+ });
+ }
+
+ private throwIfControlMissing(name: string): void {
+ if (!Object.keys(this.controls).length) {
+ throw new Error(
+ "There are no form controls registered with this group yet."
+ );
+ }
+ if (!this.controls[name]) {
+ throw new Error(`Cannot find form control with name: ${name}.`);
+ }
}
- }
}
export class FormArray extends AbstractControl {
- inputArray: InputArray;
- controls: AbstractControl[];
-
- constructor(
- inputArray: InputArray,
- controls: AbstractControl[],
- validatorOrOpts?: ValidatorOrOpts,
- asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
- ) {
- super(
- coerceToValidator(validatorOrOpts),
- coerceToAsyncValidator(asyncValidator, validatorOrOpts)
- );
- this.inputArray = inputArray || {};
- this.controls = controls || [];
- this.initObservables();
- this.setUpControls();
- this.updateValueAndValidity({
- onlySelf: true,
- emitEvent: false,
- });
- }
-
- /**
- * Get the `AbstractControl` at the given `index` in the array.
- * @param {Number} index
- * @return {AbstractControl}
- */
- at(
- index: number
- ): AbstractControl | undefined {
- return this.controls[index];
- }
-
- /**
- * Insert a new `AbstractControl` at the end of the array.
- * @param {AbstractControl} control
- * @return {Void}
- */
- push(control: AbstractControl): void {
- this.controls.push(control);
- this.registerControl(control);
- this.updateValueAndValidity();
- this._onCollectionChange();
- }
-
- /**
- * Insert a new `AbstractControl` at the given `index` in the array.
- * @param {Number} index
- * @param {AbstractControl} control
- */
- insert(
- index: number,
- control: AbstractControl
- ): void {
- this.controls.splice(index, 0, control);
- this.registerControl(control);
- this.updateValueAndValidity();
- this._onCollectionChange();
- }
-
- /**
- * Remove the control at the given `index` in the array.
- * @param {Number} index
- */
- removeAt(index: number): void {
- if (!this.controls[index]) {
- return;
- }
- this.controls[index].registerOnCollectionChange();
- this.controls.splice(index, 1);
- this.updateValueAndValidity();
- this._onCollectionChange();
- }
-
- /**
- * Replace an existing control.
- * @param {Number} index
- * @param {AbstractControl} control
- */
- setControl(
- index: number,
- control: AbstractControl
- ): void {
- if (this.controls[index]) {
- this.controls[index].registerOnCollectionChange();
- }
- this.controls.splice(index, 1);
-
- if (control) {
- this.controls.splice(index, 0, control);
- this.registerControl(control);
- }
-
- this.updateValueAndValidity();
- this._onCollectionChange();
- }
-
- /**
- * Length of the control array.
- * @return {Number}
- */
- get length(): number {
- return this.controls.length;
- }
-
- /**
- * Sets the value of the `FormArray`. It accepts an array that matches
- * the structure of the control.
- * @param {any[]} value
- * @param {{onlySelf?: boolean, emitEvent?: boolean}} options
- */
- setValue(value: T[], options: ControlChangeOpts = {}): void {
- this.checkAllValuesPresent(value);
- value.forEach((newValue, index) => {
- this.throwIfControlMissing(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 will do its best to match the values to the correct
- * controls in the group.
- * @param {any[]} value
- * @param {{onlySelf?: boolean, emitEvent?: boolean}} options
- */
- patchValue(value: T[], options: ControlChangeOpts = {}): void {
- value.forEach((newValue, index) => {
- this.at(index)?.patchValue(newValue, {
- onlySelf: true,
- emitEvent: options.emitEvent,
- });
- });
- this.updateValueAndValidity(options);
- }
-
- /**
- * Resets the `FormArray`.
- * @param {any[]} value
- * @param {{onlySelf?: boolean, emitEvent?: boolean}} options
- */
- reset(value = [], options: ControlChangeOpts = {}): void {
- this.forEachChild((control, index) => {
- control.reset(value[index as number], {
- onlySelf: true,
- emitEvent: options.emitEvent,
- });
- });
- this.updateValueAndValidity(options);
- this.updatePristine(options);
- this.updateTouched(options);
- }
-
- /**
- * The aggregate value of the array, including any disabled controls.
- *
- * If you'd like to include all values regardless of disabled status, use this method.
- * Otherwise, the `value` property is the best way to get the value of the array.
- * @return {any[]}
- */
- getRawValue(): any[] {
- return this.controls.map((control) => control.getRawValue());
- }
-
- private throwIfControlMissing(index: number): void {
- if (!this.controls.length) {
- throw new Error(
- "There are no form controls registered with this array yet."
- );
- }
- if (!this.at(index)) {
- throw new Error(`Cannot find form control at index ${index}`);
- }
- }
-
- protected forEachChild(
- callback: (control: AbstractControl, index?: number) => void
- ): void {
- this.controls.forEach((control, index) => {
- callback(control, index);
- });
- }
-
- protected updateValue(): void {
- this.value = this.controls
- .filter((control) => control.enabled || this.disabled)
- .map((control) => control.value);
- }
-
- protected anyControls(
- callback: (control: AbstractControl) => boolean
- ): boolean {
- return this.controls.some(
- (control) => control.enabled && callback(control)
- );
- }
+ inputArray: InputArray;
+ controls: AbstractControl[];
+
+ constructor(
+ inputArray: InputArray,
+ controls: AbstractControl[],
+ validatorOrOpts?: ValidatorOrOpts,
+ asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
+ ) {
+ super(
+ coerceToValidator(validatorOrOpts),
+ coerceToAsyncValidator(asyncValidator, validatorOrOpts)
+ );
+ this.inputArray = inputArray || {};
+ this.controls = controls || [];
+ this.initObservables();
+ this.setUpControls();
+ this.updateValueAndValidity({
+ onlySelf: true,
+ emitEvent: false,
+ });
+ }
- private setUpControls() {
- this.forEachChild((control) => this.registerControl(control));
- }
+ /**
+ * Get the `AbstractControl` at the given `index` in the array.
+ * @param {Number} index
+ * @return {AbstractControl}
+ */
+ at(index: number): AbstractControl | undefined {
+ return this.controls[index];
+ }
+
+ /**
+ * Insert a new `AbstractControl` at the end of the array.
+ * @param {AbstractControl} control
+ * @return {Void}
+ */
+ push(control: AbstractControl): void {
+ this.controls.push(control);
+ this.registerControl(control);
+ this.updateValueAndValidity();
+ this._onCollectionChange();
+ }
+
+ /**
+ * Insert a new `AbstractControl` at the given `index` in the array.
+ * @param {Number} index
+ * @param {AbstractControl} control
+ */
+ insert(index: number, control: AbstractControl): void {
+ this.controls.splice(index, 0, control);
+ this.registerControl(control);
+ this.updateValueAndValidity();
+ this._onCollectionChange();
+ }
+
+ /**
+ * Remove the control at the given `index` in the array.
+ * @param {Number} index
+ */
+ removeAt(index: number): void {
+ if (!this.controls[index]) {
+ return;
+ }
+ this.controls[index].registerOnCollectionChange();
+ this.controls.splice(index, 1);
+ this.updateValueAndValidity();
+ this._onCollectionChange();
+ }
+
+ /**
+ * Replace an existing control.
+ * @param {Number} index
+ * @param {AbstractControl} control
+ */
+ setControl(index: number, control: AbstractControl): void {
+ if (this.controls[index]) {
+ this.controls[index].registerOnCollectionChange();
+ }
+ this.controls.splice(index, 1);
- private checkAllValuesPresent(value: any[]) {
- this.forEachChild((control, index) => {
- if (value[index as number] === undefined) {
- throw new Error(
- `Must supply a value for form control at index: ${index}.`
+ if (control) {
+ this.controls.splice(index, 0, control);
+ this.registerControl(control);
+ }
+
+ this.updateValueAndValidity();
+ this._onCollectionChange();
+ }
+
+ /**
+ * Enables the control. This means the control will be included in validation checks and
+ * the aggregate value of its parent. Its status is re-calculated based on its value and
+ * its validators.
+ *
+ * If the control has children, all children will be enabled.
+ * @param {{onlySelf: Boolean, emitEvent: Boolean}} opts
+ * @return {void}
+ */
+ enable(opts: ControlChangeOpts = {}): void {
+ this.status = "VALID";
+
+ const isHidden =
+ this.inputArray.hidden && this.inputArray.hidden(opts.form);
+
+ this.forEachChild((control) => {
+ const childOpts = {
+ emitEvent: false,
+ onlySelf: true,
+ };
+
+ if (isHidden) {
+ control.disable(childOpts);
+ } else {
+ control.enable(childOpts);
+ }
+ });
+ this.updateValueAndValidity({
+ onlySelf: true,
+ emitEvent: opts.emitEvent,
+ });
+ this.updateAncestors(opts.onlySelf, opts.emitEvent);
+ }
+
+ /**
+ * Length of the control array.
+ * @return {Number}
+ */
+ get length(): number {
+ return this.controls.length;
+ }
+
+ /**
+ * Sets the value of the `FormArray`. It accepts an array that matches
+ * the structure of the control.
+ * @param {any[]} value
+ * @param {{onlySelf?: boolean, emitEvent?: boolean}} options
+ */
+ setValue(value: T[], options: ControlChangeOpts = {}): void {
+ this.checkAllValuesPresent(value);
+ value.forEach((newValue, index) => {
+ this.throwIfControlMissing(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 will do its best to match the values to the correct
+ * controls in the group.
+ * @param {any[]} value
+ * @param {{onlySelf?: boolean, emitEvent?: boolean}} options
+ */
+ patchValue(value: T[], options: ControlChangeOpts = {}): void {
+ value.forEach((newValue, index) => {
+ this.at(index)?.patchValue(newValue, {
+ onlySelf: true,
+ emitEvent: options.emitEvent,
+ });
+ });
+ this.updateValueAndValidity(options);
+ }
+
+ /**
+ * Resets the `FormArray`.
+ * @param {any[]} value
+ * @param {{onlySelf?: boolean, emitEvent?: boolean}} options
+ */
+ reset(value = [], options: ControlChangeOpts = {}): void {
+ this.forEachChild((control, index) => {
+ control.reset(value[index as number], {
+ onlySelf: true,
+ emitEvent: options.emitEvent,
+ });
+ });
+ this.updateValueAndValidity(options);
+ this.updatePristine(options);
+ this.updateTouched(options);
+ }
+
+ /**
+ * The aggregate value of the array, including any disabled controls.
+ *
+ * If you'd like to include all values regardless of disabled status, use this method.
+ * Otherwise, the `value` property is the best way to get the value of the array.
+ * @return {any[]}
+ */
+ getRawValue(): any[] {
+ return this.controls.map((control) => control.getRawValue());
+ }
+
+ private throwIfControlMissing(index: number): void {
+ if (!this.controls.length) {
+ throw new Error(
+ "There are no form controls registered with this array yet."
+ );
+ }
+ if (!this.at(index)) {
+ throw new Error(`Cannot find form control at index ${index}`);
+ }
+ }
+
+ protected forEachChild(
+ callback: (control: AbstractControl, index?: number) => void
+ ): void {
+ this.controls.forEach((control, index) => {
+ callback(control, index);
+ });
+ }
+
+ protected updateValue(): void {
+ this.value = this.controls
+ .filter((control) => control.enabled || this.disabled)
+ .map((control) => control.value);
+ }
+
+ protected anyControls(
+ callback: (control: AbstractControl) => boolean
+ ): boolean {
+ return this.controls.some(
+ (control) => control.enabled && callback(control)
);
- }
- });
- }
+ }
- protected allControlsDisabled(): boolean {
- for (const control of this.controls) {
- if (control.enabled) {
- return false;
- }
+ private setUpControls() {
+ this.forEachChild((control) => this.registerControl(control));
+ }
+
+ private checkAllValuesPresent(value: any[]) {
+ this.forEachChild((control, index) => {
+ if (value[index as number] === undefined) {
+ throw new Error(
+ `Must supply a value for form control at index: ${index}.`
+ );
+ }
+ });
+ }
+
+ protected allControlsDisabled(): boolean {
+ for (const control of this.controls) {
+ if (control.enabled) {
+ return false;
+ }
+ }
+ return this.controls.length > 0 || this.disabled;
}
- return this.controls.length > 0 || this.disabled;
- }
- private registerControl(control: AbstractControl): void {
- control.setParent(this);
- control.registerOnCollectionChange(this._onCollectionChange);
- }
+ private registerControl(control: AbstractControl): void {
+ control.setParent(this);
+ control.registerOnCollectionChange(this._onCollectionChange);
+ }
- _onCollectionChange(): void {
- // do nothing
- }
+ _onCollectionChange(): void {
+ // do nothing
+ }
}
export class Form extends FormGroup {
- inputs: InputElement[];
-
- constructor(
- inputs: InputElement[],
- controls: NamedControlsMap,
- validatorOrOpts?: ValidatorOrOpts,
- asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
- ) {
- super(
- (null as unknown) as InputGroup,
- controls,
- validatorOrOpts,
- asyncValidator
- );
- this.inputs = inputs;
- }
+ inputs: InputElement[];
+
+ constructor(
+ inputs: InputElement[],
+ controls: NamedControlsMap,
+ validatorOrOpts?: ValidatorOrOpts,
+ asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
+ ) {
+ super(
+ (null as unknown) as InputGroup,
+ controls,
+ validatorOrOpts,
+ asyncValidator
+ );
+ this.inputs = inputs;
+ }
}