@al00x/forms
Version:
Angular Forms, the way it should be!
377 lines (368 loc) • 13.8 kB
JavaScript
import { Validators as Validators$1, FormControl, FormGroup } from '@angular/forms';
import { Subscription, take, debounceTime, Subject, BehaviorSubject, merge, startWith, map, combineLatest } from 'rxjs';
import { inject, InjectionToken, provideAppInitializer } from '@angular/core';
function injectOptional(token) {
try {
return inject(token);
}
catch {
return undefined;
}
}
const RegexIP = new RegExp(`^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$`);
const RegexPassword = new RegExp(`^(?=.*\\d)(?=.*[A-Za-z])(?=.*[a-zA-Z]).*$`);
const VALIDATORS_DEFAULTS = {
phone: {
min: 11,
max: 11,
},
nationalCode: {
min: 10,
max: 10,
},
password: {
regex: RegexPassword,
min: 8,
},
};
const VALIDATORS_CONFIG = new InjectionToken('Validators');
function withDefault(input, defaultValue) {
return input !== undefined ? input : defaultValue;
}
class Validators extends Validators$1 {
static config() {
const value = injectOptional(VALIDATORS_CONFIG);
return {
...VALIDATORS_DEFAULTS,
...value,
};
}
static nationalCode(min, max) {
const config = this.config();
const minValue = withDefault(min, config.nationalCode.min);
const maxValue = withDefault(max, config.nationalCode.max);
return (control) => {
return Validators.number(minValue, maxValue)(control) !== null ? { codeMelli: { value: control.value } } : null;
};
}
static clone(controller) {
return () => {
return controller.invalid ? { clone: true } : null;
};
}
static ip(control) {
const value = control?.value?.toString() ?? '';
return !RegexIP.test(value) ? { ip: { value: control.value } } : null;
}
static alphabet(lang) {
return (control) => {
const value = control?.value?.toString() ?? '';
const regex = new RegExp(lang === 'en' ? '^[A-Za-z0-9\\s!@#$%^&*()_+={}\\[\\]:;"\'<>,.?/\\\\|-]+$' : /^(\p{L}|\p{N}|\p{S}|\s)+$/u);
return !regex.test(value) ? { alphabet: { value: control.value } } : null;
};
}
static match(secondControl) {
let sub = new Subscription();
return (control) => {
sub.unsubscribe();
sub = secondControl.valueChanges.pipe(take(1), debounceTime(100)).subscribe(() => {
control.updateValueAndValidity();
});
const v1 = secondControl?.value ?? '';
const v2 = control?.value ?? '';
return v1 !== v2 ? { unmatched: { value: control.value } } : null;
};
}
static number(minLength = 0, maxLength) {
return (control) => {
const value = control?.value?.toString() ?? '';
const regex = new RegExp(`\\b(\\d){${minLength},${maxLength ?? ''}}\\b`);
return !regex.test(value) ? { number: { value: control.value } } : null;
};
}
static password(minLength, regex) {
const config = this.config();
const minValue = withDefault(minLength, config.password.min);
const regexValue = withDefault(regex, config.password.regex);
return (control) => {
const value = control?.value ?? '';
if (!value || value === '')
return null;
const tested = regexValue.test(value);
const length = (value.length ?? 0) >= minValue;
return !tested
? { passwordChars: { value: control.value } }
: !length
? { passwordLength: { value: control.value } }
: null;
};
}
static phone(min, max) {
const config = this.config();
const minValue = withDefault(min, config.phone.min);
const maxValue = withDefault(max, config.phone.max);
return (control) => {
return Validators.number(minValue, maxValue)(control) !== null ? { phoneNumber: { value: control.value } } : null;
};
}
static requiredNullable(control) {
const value = control.value;
const isEmpty = value === undefined || ((typeof value === 'string' || Array.isArray(value)) && value.length === 0);
return isEmpty ? { required: true } : null;
}
}
const DEFAULT_CONTROL_ERROR = {
default: `This field is incorrect.`,
required: `This field is required.`,
email: `Email format is incorrect.`,
codeMelli: `National code format is incorrect.`,
unmatched: `This field does not match.`,
passwordLength: `Password should at least be 8 characters long.`,
passwordChars: `Password should contain both numbers and letters.`,
};
let CONTROL_ERRORS = {
...DEFAULT_CONTROL_ERROR
};
function setControlErrors(errors) {
CONTROL_ERRORS = {
...DEFAULT_CONTROL_ERROR,
...errors,
};
}
function getControlErrorMessage(control, customMessage) {
if (control.status !== 'INVALID')
return undefined;
if (customMessage) {
return customMessage;
}
const errorKeys = Object.keys(control.errors ?? {});
const firstKey = errorKeys.length > 0 ? errorKeys[0] : undefined;
let error;
if (firstKey) {
error = CONTROL_ERRORS[firstKey];
if (!error) {
error = !customMessage ? CONTROL_ERRORS['default'] : customMessage;
}
}
return error;
}
function formControl(value, validators, nullable = true) {
const control = new FormControl(value, {
validators: validators,
nonNullable: nullable,
});
const statusTrigger = new Subject();
const disableTrigger = new BehaviorSubject(control.disabled);
const dirtyTrigger = new BehaviorSubject(control.dirty);
const touchedTrigger = new BehaviorSubject(control.touched);
const valueSubject = new BehaviorSubject(control.value);
const selectedItemsTrigger = new BehaviorSubject(undefined);
const readonlyTrigger = new BehaviorSubject(false);
const displayTextTrigger = new BehaviorSubject(undefined);
const dataTrigger = new BehaviorSubject(undefined);
const subControls = [];
// Extended new properties!
control.extended = true;
control.status$ = merge(control.statusChanges, statusTrigger).pipe(startWith(control.status));
control.invalid$ = control.status$.pipe(map((t) => t === 'INVALID'));
control.valid$ = control.status$.pipe(map((t) => t === 'VALID'));
control.value$ = valueSubject.pipe();
control.hasValue$ = valueSubject.pipe(map((v) => {
if (value === null || value === undefined)
return false;
if (typeof value === 'string' && value.length === 0)
return false;
if (value instanceof Array && value.length === 0)
return false;
return true;
}));
control.empty$ = control.hasValue$.pipe(map(v => !v));
control.selectedItems$ = selectedItemsTrigger.asObservable();
control.selectedItems = selectedItemsTrigger.value;
control.readonly$ = readonlyTrigger.asObservable();
control.readonly = false;
control.disabled$ = disableTrigger.asObservable();
control.enabled$ = control.disabled$.pipe(map((t) => !t));
control.dirty$ = combineLatest({ trigger: dirtyTrigger, status: control.status$ }).pipe(map((t) => (t.status === 'DISABLED' ? false : t.trigger)));
control.touched$ = combineLatest({ trigger: touchedTrigger, status: control.status$ }).pipe(map((t) => (t.status === 'DISABLED' ? false : t.trigger)));
control.showError$ = combineLatest({ invalid: control.invalid$, touched: control.touched$ }).pipe(map((t) => t.invalid && t.touched));
control.displayText$ = displayTextTrigger.asObservable();
control.displayText = undefined;
control.displayControl = new FormControl(undefined, [Validators.clone(control)]);
control.data$ = dataTrigger.asObservable();
control.data = undefined;
control.error$ = control.status$.pipe(debounceTime(1), map(() => {
if (control.status === 'INVALID') {
return getControlErrorMessage(control, control.customError);
}
return undefined;
}));
control.customError = undefined;
control.setReadonly = (value) => {
control.readonly = value;
readonlyTrigger.next(value);
return control;
};
control.setDisplayText = (value) => {
let newValue = undefined;
if (typeof value === 'string' || typeof value === 'number') {
newValue = typeof value === 'number' ? value.toString() : value;
}
control.displayText = newValue;
displayTextTrigger.next(newValue);
control.displayControl.setValue(newValue);
return control;
};
control.setSelectedItems = (value) => {
control.selectedItems = value;
selectedItemsTrigger.next(value);
return control;
};
control.setCustomError = (value) => {
control.customError = value;
return control;
};
control.setData = (value) => {
control.data = value;
dataTrigger.next(value);
return control;
};
control.setDisabled = (value) => {
if (value) {
control.disable();
}
else {
control.enable();
}
return control;
};
control.getValue = () => {
return control.valid ? control.value : null;
};
control.emitStatus = () => {
statusTrigger.next(control.status);
};
subControls.push(control.displayControl);
// Override original functions
// @ts-ignore
// control._forEachChild = function (cb: (c: AbstractControl, index: number) => void) {
// subControls.forEach((control: AbstractControl, index: number) => {
// cb(control, index);
// });
// }
//
// // @ts-ignore
// const _runValidatorFn = control._runValidator
// // @ts-ignore
// control._runValidator = function () {
// const errors = _runValidatorFn.apply(this);
// subControls.forEach(c => {
// c.setErrors(errors);
// });
// return errors;
// }
const { setValue, patchValue, reset, disable, enable, markAsTouched, markAsUntouched, markAsDirty, markAsPristine } = control;
control.setValue = function (value, options) {
setValue.apply(this, [value, options]);
valueSubject.next(value);
// control.setDisplayText(value);
};
control.patchValue = function (value, options) {
patchValue.apply(this, [value, options]);
valueSubject.next(value);
// control.setDisplayText(value);
};
control.reset = function (value, options) {
reset.apply(this, [value, options]);
valueSubject.next(value && typeof value === 'object' && 'value' in value ? value.value : value);
control.setData(undefined);
control.setSelectedItems(undefined);
control.setDisplayText(undefined);
dirtyTrigger.next(control.dirty);
touchedTrigger.next(control.touched);
};
control.disable = function (e) {
disable.apply(this, [e]);
disableTrigger.next(true);
};
control.enable = function (e) {
enable.apply(this, [e]);
disableTrigger.next(false);
};
control.markAsTouched = function (opts) {
markAsTouched.apply(this, [opts]);
touchedTrigger.next(true);
};
control.markAsDirty = function (opts) {
markAsDirty.apply(this, [opts]);
dirtyTrigger.next(true);
};
control.markAsUntouched = function (opts) {
markAsUntouched.apply(this, [opts]);
touchedTrigger.next(false);
};
control.markAsPristine = function (opts) {
markAsPristine.apply(this, [opts]);
dirtyTrigger.next(false);
};
// Initialize
control.setDisplayText(value);
return control;
}
function isFormControlExtended(control) {
return 'extended' in control;
}
function formGroup(inputs) {
const group = new FormGroup(inputs);
const dataTrigger = new BehaviorSubject(undefined);
group.extended = true;
group.status$ = merge(group.statusChanges).pipe(startWith(group.status));
group.invalid$ = group.status$.pipe(map((t) => t === 'INVALID'));
group.valid$ = group.status$.pipe(map((t) => t === 'VALID'));
group.data$ = dataTrigger.asObservable();
group.data = undefined;
const forEachControl = (fn) => {
Object.entries(group.controls).forEach(([key, control]) => fn(key, control));
};
group.setReadonly = (value, ...keys) => {
forEachControl((key, control) => {
if (isFormControlExtended(control) && (keys?.length ? keys.includes(key) : true)) {
control.setReadonly(value);
}
});
return group;
};
group.setDisabled = (value, ...keys) => {
forEachControl((key, control) => {
if ((keys?.length ? keys.includes(key) : true)) {
value ? control.disable() : control.enable();
}
});
return group;
};
group.markAllAsDirty = () => {
forEachControl((key, control) => {
control.markAsDirty();
control.markAsTouched();
});
};
group.setData = (value) => {
group.data = value;
dataTrigger.next(value);
return group;
};
return group;
}
function isFormGroupExtended(group) {
return 'extended' in group;
}
function provideFormErrors(errorsObject) {
return provideAppInitializer(() => {
setControlErrors(errorsObject);
});
}
/**
* Generated bundle index. Do not edit.
*/
export { VALIDATORS_CONFIG, Validators, formControl, formGroup, isFormControlExtended, isFormGroupExtended, provideFormErrors };
//# sourceMappingURL=al00x-forms.mjs.map