UNPKG

@ngneat/reactive-forms

Version:

(Angular Reactive) Forms with Benefits

593 lines (582 loc) 22.5 kB
import { UntypedFormGroup, UntypedFormControl, UntypedFormArray, UntypedFormBuilder } from '@angular/forms'; import { merge, defer, of, Subject, isObservable, from } from 'rxjs'; import { map, distinctUntilChanged, startWith, pairwise, filter, take, tap, switchMap, debounceTime } from 'rxjs/operators'; import * as i0 from '@angular/core'; import { Injectable } from '@angular/core'; function selectControlValue$(control, mapFn) { return control.value$.pipe(map(mapFn), distinctUntilChanged()); } function controlValueChanges$(control) { return merge(defer(() => of(control.getRawValue())), control.valueChanges.pipe(map(() => control.getRawValue()))); } function controlStatus$(control, type) { return merge(defer(() => of(control[type])), control.statusChanges.pipe(map(() => control[type]), distinctUntilChanged())); } function enableControl(control, enabled, opts) { if (enabled) { control.enable(opts); } else { control.disable(opts); } } function disableControl(control, disabled, opts) { enableControl(control, !disabled, opts); } function controlDisabledWhile(control, observable, opts) { return observable.subscribe((isDisabled) => disableControl(control, isDisabled, opts)); } function controlEnabledWhile(control, observable, opts) { return observable.subscribe((isEnabled) => enableControl(control, isEnabled, opts)); } function mergeErrors(existing, toAdd) { if (!existing && !toAdd) { return null; } return { ...existing, ...toAdd, }; } function removeError(errors, key) { if (!errors) { return null; } const updatedErrors = { ...errors, }; delete updatedErrors[key]; return Object.keys(updatedErrors).length > 0 ? updatedErrors : null; } function hasErrorAnd(and, control, error, path) { const hasError = control.hasError(error, !path || path.length === 0 ? undefined : path); return hasError && control[and]; } function controlErrorChanges$(control, errors$) { return merge(defer(() => of(control.errors)), errors$, control.valueChanges.pipe(map(() => control.errors), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)))); } function markAllDirty(control) { control.markAsDirty({ onlySelf: true }); control._forEachChild((control) => control.markAllAsDirty?.() || control.markAsDirty({ onlySelf: true })); } class FormGroup extends UntypedFormGroup { constructor(controls, validatorOrOpts, asyncValidator) { super(controls, validatorOrOpts, asyncValidator); this.controls = controls; this.touchChanges = new Subject(); this.dirtyChanges = new Subject(); this.errorsSubject = new Subject(); this.touch$ = this.touchChanges .asObservable() .pipe(distinctUntilChanged()); this.dirty$ = this.dirtyChanges .asObservable() .pipe(distinctUntilChanged()); this.value$ = controlValueChanges$(this); this.disabled$ = controlStatus$(this, 'disabled'); this.enabled$ = controlStatus$(this, 'enabled'); this.invalid$ = controlStatus$(this, 'invalid'); this.valid$ = controlStatus$(this, 'valid'); this.status$ = controlStatus$(this, 'status'); this.errors$ = controlErrorChanges$(this, this.errorsSubject.asObservable()); } select(mapFn) { return selectControlValue$(this, mapFn); } get(key) { return super.get(key); } setValue(valueOrObservable, options) { if (isObservable(valueOrObservable)) { return valueOrObservable.subscribe((value) => super.setValue(value, options)); } super.setValue(valueOrObservable, options); } patchValue(valueOrObservable, options) { if (isObservable(valueOrObservable)) { return valueOrObservable.subscribe((value) => super.patchValue(value, options)); } super.patchValue(valueOrObservable, options); } getRawValue() { return super.getRawValue(); } markAsTouched(...opts) { super.markAsTouched(...opts); this.touchChanges.next(true); } markAsUntouched(...opts) { super.markAsUntouched(...opts); this.touchChanges.next(false); } markAsPristine(...opts) { super.markAsPristine(...opts); this.dirtyChanges.next(false); } markAsDirty(...opts) { super.markAsDirty(...opts); this.dirtyChanges.next(true); } markAllAsDirty() { markAllDirty(this); } setEnable(enable = true, opts) { enableControl(this, enable, opts); } setDisable(disable = true, opts) { disableControl(this, disable, opts); } disabledWhile(observable, options) { return controlDisabledWhile(this, observable, options); } enabledWhile(observable, options) { return controlEnabledWhile(this, observable, options); } reset(formState, options) { super.reset(formState, options); } setValidators(newValidators, options) { super.setValidators(newValidators); super.updateValueAndValidity(options); } setAsyncValidators(newValidator, options) { super.setAsyncValidators(newValidator); super.updateValueAndValidity(options); } getError(...params) { return super.getError(...params); } setErrors(...opts) { /** * @description * Use an elvis operator to avoid a throw when the control is used with an async validator * Which will be instantly resolved (like with `of(null)`) * In such case, Angular will call this method instantly before even instancing the properties causing the throw * Can be easily reproduced with a step-by-step debug once compiled when checking the stack trace of the constructor * * Issue: https://github.com/ngneat/reactive-forms/issues/91 * Reproduction: https://codesandbox.io/embed/github/C0ZEN/ngneat-reactive-forms-error-issue-cs/tree/main/?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark */ this.errorsSubject?.next(opts[0]); return super.setErrors(...opts); } mergeErrors(errors, opts) { this.setErrors(mergeErrors(this.errors, errors), opts); } removeError(key, opts) { this.setErrors(removeError(this.errors, key), opts); } hasErrorAndTouched(error, path) { return hasErrorAnd('touched', this, error, path); } hasErrorAndDirty(error, path) { return hasErrorAnd('dirty', this, error, path); } } class FormControl extends UntypedFormControl { constructor(formState, validatorOrOpts, asyncValidator) { super(formState, validatorOrOpts, asyncValidator); this.touchChanges = new Subject(); this.dirtyChanges = new Subject(); this.errorsSubject = new Subject(); this.touch$ = this.touchChanges .asObservable() .pipe(distinctUntilChanged()); this.dirty$ = this.dirtyChanges .asObservable() .pipe(distinctUntilChanged()); this.value$ = controlValueChanges$(this); this.disabled$ = controlStatus$(this, 'disabled'); this.enabled$ = controlStatus$(this, 'enabled'); this.invalid$ = controlStatus$(this, 'invalid'); this.valid$ = controlStatus$(this, 'valid'); this.status$ = controlStatus$(this, 'status'); this.errors$ = controlErrorChanges$(this, this.errorsSubject.asObservable()); } setValue(valueOrObservable, options) { if (isObservable(valueOrObservable)) { return valueOrObservable.subscribe((value) => super.setValue(value, options)); } super.setValue(valueOrObservable, options); } patchValue(valueOrObservable, options) { if (isObservable(valueOrObservable)) { return valueOrObservable.subscribe((value) => super.patchValue(value, options)); } super.patchValue(valueOrObservable, options); } getRawValue() { return this.value; } markAsTouched(...opts) { super.markAsTouched(...opts); this.touchChanges.next(true); } markAsUntouched(...opts) { super.markAsUntouched(...opts); this.touchChanges.next(false); } markAsPristine(...opts) { super.markAsPristine(...opts); this.dirtyChanges.next(false); } markAsDirty(...opts) { super.markAsDirty(...opts); this.dirtyChanges.next(true); } setEnable(enable = true, opts) { enableControl(this, enable, opts); } setDisable(disable = true, opts) { disableControl(this, disable, opts); } disabledWhile(observable, options) { return controlDisabledWhile(this, observable, options); } enabledWhile(observable, options) { return controlEnabledWhile(this, observable, options); } reset(formState, options) { super.reset(formState, options); } setValidators(newValidators, options) { super.setValidators(newValidators); super.updateValueAndValidity(options); } setAsyncValidators(newValidator, options) { super.setAsyncValidators(newValidator); super.updateValueAndValidity(options); } getError(...params) { return super.getError(...params); } setErrors(...opts) { /** * @description * Use an elvis operator to avoid a throw when the control is used with an async validator * Which will be instantly resolved (like with `of(null)`) * In such case, Angular will call this method instantly before even instancing the properties causing the throw * Can be easily reproduced with a step-by-step debug once compiled when checking the stack trace of the constructor * * Issue: https://github.com/ngneat/reactive-forms/issues/91 * Reproduction: https://codesandbox.io/embed/github/C0ZEN/ngneat-reactive-forms-error-issue-cs/tree/main/?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark */ this.errorsSubject?.next(opts[0]); return super.setErrors(...opts); } mergeErrors(errors, opts) { this.setErrors(mergeErrors(this.errors, errors), opts); } removeError(key, opts) { this.setErrors(removeError(this.errors, key), opts); } hasErrorAndTouched(error) { return hasErrorAnd('touched', this, error); } hasErrorAndDirty(error) { return hasErrorAnd('dirty', this, error); } } class FormArray extends UntypedFormArray { constructor(controls, validatorOrOpts, asyncValidator) { super(controls, validatorOrOpts, asyncValidator); this.controls = controls; this.touchChanges = new Subject(); this.dirtyChanges = new Subject(); this.errorsSubject = new Subject(); this.touch$ = this.touchChanges .asObservable() .pipe(distinctUntilChanged()); this.dirty$ = this.dirtyChanges .asObservable() .pipe(distinctUntilChanged()); this.value$ = controlValueChanges$(this); this.disabled$ = controlStatus$(this, 'disabled'); this.enabled$ = controlStatus$(this, 'enabled'); this.invalid$ = controlStatus$(this, 'invalid'); this.valid$ = controlStatus$(this, 'valid'); this.status$ = controlStatus$(this, 'status'); this.errors$ = controlErrorChanges$(this, this.errorsSubject.asObservable()); } select(mapFn) { return this.value$.pipe(map(mapFn), distinctUntilChanged()); } setValue(valueOrObservable, options) { if (isObservable(valueOrObservable)) { return valueOrObservable.subscribe((value) => super.setValue(value, options)); } super.setValue(valueOrObservable, options); } patchValue(valueOrObservable, options) { if (isObservable(valueOrObservable)) { return valueOrObservable.subscribe((value) => super.patchValue(value, options)); } super.patchValue(valueOrObservable, options); } getRawValue() { return super.getRawValue(); } push(control, options) { return super.push(control, options); } insert(index, control, options) { return super.insert(index, control, options); } setControl(index, control, options) { return super.setControl(index, control, options); } at(index) { return super.at(index); } remove(value, options) { this.removeWhen((v) => v.value === value); } removeWhen(predicate, options) { for (let i = this.length - 1; i >= 0; --i) { if (predicate(this.at(i))) { this.removeAt(i, options); } } } markAsTouched(...opts) { super.markAsTouched(...opts); this.touchChanges.next(true); } markAsUntouched(...opts) { super.markAsUntouched(...opts); this.touchChanges.next(false); } markAsPristine(...opts) { super.markAsPristine(...opts); this.dirtyChanges.next(false); } markAsDirty(...opts) { super.markAsDirty(...opts); this.dirtyChanges.next(true); } markAllAsDirty() { markAllDirty(this); } setEnable(enable = true, opts) { enableControl(this, enable, opts); } setDisable(disable = true, opts) { disableControl(this, disable, opts); } disabledWhile(observable, options) { return controlDisabledWhile(this, observable, options); } enabledWhile(observable, options) { return controlEnabledWhile(this, observable, options); } reset(formState, options) { super.reset(formState, options); } setValidators(newValidators, options) { super.setValidators(newValidators); super.updateValueAndValidity(options); } setAsyncValidators(newValidator, options) { super.setAsyncValidators(newValidator); super.updateValueAndValidity(options); } getError(...params) { return super.getError(...params); } setErrors(...opts) { /** * @description * Use an elvis operator to avoid a throw when the control is used with an async validator * Which will be instantly resolved (like with `of(null)`) * In such case, Angular will call this method instantly before even instancing the properties causing the throw * Can be easily reproduced with a step-by-step debug once compiled when checking the stack trace of the constructor * * Issue: https://github.com/ngneat/reactive-forms/issues/91 * Reproduction: https://codesandbox.io/embed/github/C0ZEN/ngneat-reactive-forms-error-issue-cs/tree/main/?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark */ this.errorsSubject?.next(opts[0]); return super.setErrors(...opts); } mergeErrors(errors, opts) { this.setErrors(mergeErrors(this.errors, errors), opts); } removeError(key, opts) { this.setErrors(removeError(this.errors, key), opts); } hasErrorAndTouched(error, path) { return hasErrorAnd('touched', this, error, path); } hasErrorAndDirty(error, path) { return hasErrorAnd('dirty', this, error, path); } } class FormBuilder extends UntypedFormBuilder { control(formState, validatorOrOpts, asyncValidator) { return new FormControl(formState, validatorOrOpts, asyncValidator); } array(controlsConfig, validatorOrOpts, asyncValidator) { const controls = controlsConfig.map(c => this._createControl(c)); return new FormArray(controls, validatorOrOpts, asyncValidator); } group(controlsConfig, options) { const controls = this._reduceControls(controlsConfig); let validators = null; let asyncValidators = null; let updateOn; if (options != null) { validators = options.validators != null ? options.validators : null; asyncValidators = options.asyncValidators != null ? options.asyncValidators : null; updateOn = options.updateOn != null ? options.updateOn : undefined; } return new FormGroup(controls, { asyncValidators, updateOn, validators }); } } FormBuilder.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: FormBuilder, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); FormBuilder.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: FormBuilder, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.1", ngImport: i0, type: FormBuilder, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const toArray = (object) => Object.keys(object); const isArray = (value) => value && Array.isArray(value); const isObject = (value) => typeof value === 'object' && value !== null; const isFormArray = (prev, curr) => isArray(curr) || isArray(prev); const isFormGroup = (prev, curr) => isObject(curr) || isObject(prev); const isFormControl = (prev, curr) => !isFormArray(prev, curr) && !isFormGroup(prev, curr); const convertTypesToArray = (left, right) => [left, right]; /** * An operator which is used to filter valueChanges$ output, that it would emit only changed parts. * * @return {MonoTypeOperatorFunction} An Observable that emits items from the source Observable with only changed values. */ function diff() { return (source$) => source$.pipe(startWith(undefined), pairwise(), map(control => reduceControlValue(...control)), filter(control => control !== undefined)); } function reduceControlValue(prev, curr) { if (prev === undefined) { return curr; } if (isFormControl(prev, curr)) { return prev === curr ? undefined : curr; } if (isFormArray(prev, curr)) { const [left, right] = convertTypesToArray(prev, curr); return compareArraysContent(left, right) ? undefined : curr; } return compareFormGroup(prev, curr); } function compareFormGroup(prev, curr) { const reduced = reduceFormGroup(prev, curr); return toArray(reduced).length === 0 ? undefined : reduced; } function reduceFormGroup(prev, curr) { if (!prev) { return curr; } return toArray(curr).reduce((acc, key) => { const control = reduceControlValue(prev[key], curr[key]); if (control !== undefined) { acc[key] = control; } return acc; }, {}); } function compareArraysContent(left, right) { left = Array.isArray(left) ? left : []; right = Array.isArray(right) ? right : []; return left.length === right.length && left.every(value => right.includes(value)); } function persistControl(control, key, { debounceTime, manager, arrControlFactory, persistDisabledControls }) { const persistManager = manager || new LocalStorageManager(); return restoreControl(control, key, persistManager, arrControlFactory).pipe(switchMap(() => persistValue$(control, key, { debounceTime: debounceTime || 250, manager: persistManager, persistDisabledControls }))); } function persistValue$(control, key, options) { return control.valueChanges.pipe( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion debounceTime(options.debounceTime), switchMap(value => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion wrapIntoObservable(options.manager.setValue(key, options.persistDisabledControls ? control.getRawValue() : value)))); } function restoreControl(control, key, manager, arrControlFactory) { return wrapIntoObservable(manager.getValue(key)).pipe(take(1), tap(value => { if (!value) return; if (arrControlFactory) { handleFormArrays(control, value, arrControlFactory); } control.patchValue(value, { emitEvent: false }); })); } function handleFormArrays(control, formValue, arrControlFactory) { Object.keys(formValue).forEach(controlName => { const value = formValue[controlName]; if (Array.isArray(value) && control.get(controlName) instanceof UntypedFormArray) { if (!arrControlFactory || (arrControlFactory && !(controlName in arrControlFactory))) { throw new Error(`Please provide arrControlFactory for ${controlName}`); } const current = control.get(controlName); const fc = arrControlFactory[controlName]; clearFormArray(current); value.forEach((v, i) => current.insert(i, fc(v))); } }); } function clearFormArray(control) { while (control.length !== 0) { control.removeAt(0); } } function wrapIntoObservable(value) { if (isObservable(value) || isPromise(value)) { return from(value); } return of(value); } function isPromise(value) { return typeof value?.then === 'function'; } class LocalStorageManager { setValue(key, data) { localStorage.setItem(key, JSON.stringify(data)); return data; } getValue(key) { return JSON.parse(localStorage.getItem(key) || '{}'); } } class SessionStorageManager { setValue(key, data) { sessionStorage.setItem(key, JSON.stringify(data)); return data; } getValue(key) { return JSON.parse(sessionStorage.getItem(key) || '{}'); } } class ControlValueAccessor { constructor() { // eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-unused-vars this.onChange = (value) => { // }; // eslint-disable-next-line @typescript-eslint/no-empty-function this.onTouched = () => { }; } registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { this.onTouched = fn; } } /** * Generated bundle index. Do not edit. */ export { ControlValueAccessor, FormArray, FormBuilder, FormControl, FormGroup, LocalStorageManager, SessionStorageManager, diff, persistControl, restoreControl }; //# sourceMappingURL=ngneat-reactive-forms.mjs.map