import type { FormGroup } from './form-group';
import type { AsyncValidatorFn, ValidationErrors, ValidatorFn } from './validators';
import type { Subscription } from 'rxjs';
import { isArray } from 'lodash';
import { from, Subject } from 'rxjs';
import { forkJoin } from 'rxjs';
export type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
export const [VALID, INVALID, PENDING, DISABLED] = ['VALID', 'INVALID', 'PENDING', 'DISABLED'] as FormControlStatus[];
function mergeErrors(arrayOfErrors: (ValidationErrors | null)[]): ValidationErrors | null {
const res: { [key: string]: any } = {};
arrayOfErrors.forEach((errors: ValidationErrors | null) => {
if (errors != null) {
Object.assign(res, errors);
}
});
return Object.keys(res).length === 0 ? null : res;
}
function composeValidators(validators: (ValidatorFn | null)[]): ValidatorFn | null {
const presentValidators = validators.filter((validator) => validator !== null) as ValidatorFn[];
if (presentValidators.length === 0) {
return null;
}
return function (control: AbstractControl) {
return mergeErrors(presentValidators.map((fn) => fn(control)));
};
}
function composeAsyncValidators(validators: (AsyncValidatorFn | null)[]): AsyncValidatorFn | null {
const presentValidators = validators.filter((validator) => validator !== null) as AsyncValidatorFn[];
if (presentValidators.length === 0) {
return null;
}
return function (control: AbstractControl) {
return new Promise((resolve) => {
const observables = forkJoin(presentValidators.map((fn) => fn(control)));
observables.subscribe({
next: (errors) => {
resolve(mergeErrors(errors));
},
});
});
};
}
function coerceToValidator(validator: ValidatorFn | ValidatorFn[] | null): ValidatorFn | null {
return isArray(validator) ? composeValidators(validator) : validator;
}
function coerceToAsyncValidator(asyncValidator: AsyncValidatorFn | AsyncValidatorFn[] | null): AsyncValidatorFn | null {
return isArray(asyncValidator) ? composeAsyncValidators(asyncValidator) : asyncValidator;
}
function makeValidatorsArray<T extends ValidatorFn | AsyncValidatorFn>(validators: T | T[] | null): T[] {
if (!validators) {
return [];
}
return isArray(validators) ? validators : [validators];
}
function hasValidator<T extends ValidatorFn | AsyncValidatorFn>(validators: T | T[] | null, validator: T): boolean {
return Array.isArray(validators) ? validators.includes(validator) : validators === validator;
}
function addValidators<T extends ValidatorFn | AsyncValidatorFn>(validators: T | T[], currentValidators: T | T[] | null) {
const current = makeValidatorsArray(currentValidators);
const validatorsToAdd = makeValidatorsArray(validators);
return current.concat(validatorsToAdd.filter((validator) => !hasValidator(current, validator)));
}
function removeValidators<T extends ValidatorFn | AsyncValidatorFn>(validators: T | T[], currentValidators: T | T[] | null): T[] {
return makeValidatorsArray(currentValidators).filter((validator) => !hasValidator(validators, validator));
}
export abstract class AbstractControl<V = any> {
private _parent: FormGroup | null = null;
private _pristine: boolean = true;
private _errors: ValidationErrors | null = null;
private _hasOwnPendingAsyncValidator = false;
private _asyncValidationSubscription?: Subscription;
private _composedValidatorFn: ValidatorFn | null;
private _composedAsyncValidatorFn: AsyncValidatorFn | null;
private _rawValidators: ValidatorFn | ValidatorFn[] | null;
private _rawAsyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null;
constructor(validators: ValidatorFn | ValidatorFn[] | null, asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null) {
this._rawValidators = validators;
this._rawAsyncValidators = asyncValidators;
this._composedValidatorFn = coerceToValidator(this._rawValidators);
this._composedAsyncValidatorFn = coerceToAsyncValidator(this._rawAsyncValidators);
}
get validator(): ValidatorFn | null {
return this._composedValidatorFn;
}
set validator(validatorFn: ValidatorFn | null) {
this._rawValidators = this._composedValidatorFn = validatorFn;
}
get asyncValidator(): AsyncValidatorFn | null {
return this._composedAsyncValidatorFn;
}
set asyncValidator(asyncValidatorFn: AsyncValidatorFn | null) {
this._rawAsyncValidators = this._composedAsyncValidatorFn = asyncValidatorFn;
}
get parent(): FormGroup | null {
return this._parent;
}
get root(): AbstractControl {
let control: AbstractControl = this;
while (control.parent) {
control = control.parent;
}
return control;
}
get value(): V {
return this._value;
}
get status(): FormControlStatus {
return this._status;
}
get errors(): ValidationErrors | null {
return this._errors;
}
get valid(): boolean {
return this._status === VALID;
}
get invalid(): boolean {
return this._status === INVALID;
}
get pending(): boolean {
return this._status === PENDING;
}
get disabled(): boolean {
return this._status === DISABLED;
}
get enabled(): boolean {
return this._status !== DISABLED;
}
get pristine(): boolean {
return this._pristine;
}
get dirty(): boolean {
return !this._pristine;
}
public readonly asyncVerifyComplete$ = new Subject<AbstractControl<V>>();
clone(): this {
return new Proxy(this, {});
}
setValidators(validators: ValidatorFn | ValidatorFn[] | null): void {
this._rawValidators = validators;
this._composedValidatorFn = coerceToValidator(validators);
}
setAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[] | null): void {
this._rawAsyncValidators = validators;
this._composedAsyncValidatorFn = coerceToAsyncValidator(validators);
}
addValidators(validators: ValidatorFn | ValidatorFn[]): void {
this.setValidators(addValidators(validators, this._rawValidators));
}
addAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void {
this.setAsyncValidators(addValidators(validators, this._rawAsyncValidators));
}
removeValidators(validators: ValidatorFn | ValidatorFn[]): void {
this.setValidators(removeValidators(validators, this._rawValidators));
}
removeAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void {
this.setAsyncValidators(removeValidators(validators, this._rawAsyncValidators));
}
hasValidator(validator: ValidatorFn): boolean {
return hasValidator(this._rawValidators, validator);
}
hasAsyncValidator(validator: AsyncValidatorFn): boolean {
return hasValidator(this._rawAsyncValidators, validator);
}
clearValidators(): void {
this.validator = null;
}
clearAsyncValidators(): void {
this.asyncValidator = null;
}
markAsDirty(onlySelf = false): void {
this._pristine = false;
if (this._parent && !onlySelf) {
this._parent.markAsDirty(onlySelf);
}
}
markAsPristine(onlySelf = false): void {
this._pristine = true;
this._forEachChild((control) => {
control.markAsPristine(true);
});
if (this._parent && !onlySelf) {
this._parent._updatePristine(onlySelf);
}
}
markAsPending(onlySelf = false): void {
this._status = PENDING;
if (this._parent && !onlySelf) {
this._parent.markAsPending(onlySelf);
}
}
disable(onlySelf = false): void {
const skipPristineCheck = this._parentMarkedDirty(onlySelf);
this._status = DISABLED;
this._forEachChild((control) => {
control.disable(true);
});
this._updateAncestors(onlySelf, skipPristineCheck);
}
enable(onlySelf = false): void {
const skipPristineCheck = this._parentMarkedDirty(onlySelf);
this._status = VALID;
this._forEachChild((control) => {
control.enable(true);
});
this.updateValueAndValidity(true);
this._updateAncestors(onlySelf, skipPristineCheck);
}
setParent(parent: FormGroup): void {
this._parent = parent;
}
setErrors(errors: ValidationErrors | null): void {
this._errors = errors;
this._updateControlsErrors();
}
updateValueAndValidity(onlySelf = false): void {
this._updateValue();
if (this.enabled) {
this._cancelExistingSubscription();
this._errors = this._runValidator();
this._status = this._calculateStatus();
if (this.status === VALID || this.status === PENDING) {
this._runAsyncValidator();
}
}
if (this._parent && !onlySelf) {
this._parent.updateValueAndValidity(onlySelf);
}
}
protected abstract _value: V;
protected abstract _status: FormControlStatus;
abstract setValue(value: V, onlySelf?: boolean): void;
abstract patchValue(value: any, onlySelf?: boolean): void;
abstract reset(value?: any, onlySelf?: boolean): void;
protected abstract _updateValue(): void;
protected abstract _forEachChild(cb: (c: AbstractControl) => void): void;
protected abstract _anyControls(condition: (c: AbstractControl) => boolean): boolean;
protected abstract _allControlsDisabled(): boolean;
protected _anyControlsHaveStatus(status: FormControlStatus): boolean {
return this._anyControls((control) => control.status === status);
}
protected _anyControlsDirty(): boolean {
return this._anyControls((control) => control.dirty);
}
protected _parentMarkedDirty(onlySelf: boolean): boolean {
return !!(!onlySelf && this._parent && this._parent.dirty && !this._parent._anyControlsDirty());
}
protected _updatePristine(onlySelf = false): void {
this._pristine = !this._anyControlsDirty();
if (this._parent && !onlySelf) {
this._parent._updatePristine(onlySelf);
}
}
protected _updateControlsErrors(): void {
this._status = this._calculateStatus();
if (this._parent) {
this._parent._updateControlsErrors();
}
}
protected _calculateStatus(): FormControlStatus {
if (this._allControlsDisabled()) return DISABLED;
if (this.errors) return INVALID;
if (this._hasOwnPendingAsyncValidator || this._anyControlsHaveStatus(PENDING)) return PENDING;
if (this._anyControlsHaveStatus(INVALID)) return INVALID;
return VALID;
}
protected _updateAncestors(onlySelf: boolean, skipPristineCheck: boolean) {
if (this._parent && !onlySelf) {
this._parent.updateValueAndValidity(onlySelf);
if (!skipPristineCheck) {
this._parent._updatePristine();
}
}
}
protected _runValidator(): ValidationErrors | null {
return this.validator ? this.validator(this) : null;
}
protected _cancelExistingSubscription(): void {
if (this._asyncValidationSubscription) {
this._asyncValidationSubscription.unsubscribe();
this._hasOwnPendingAsyncValidator = false;
}
}
protected _runAsyncValidator(): void {
if (this.asyncValidator) {
this._status = PENDING;
this._hasOwnPendingAsyncValidator = true;
this._asyncValidationSubscription = from(this.asyncValidator(this)).subscribe((errors) => {
this._hasOwnPendingAsyncValidator = false;
this.setErrors(errors);
this.asyncVerifyComplete$.next(this);
});
}
}
}