@ngneat/reactive-forms
Version:
(Angular Reactive) Forms with Benefits
593 lines (582 loc) • 22.5 kB
JavaScript
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