UNPKG

@al00x/forms

Version:

Angular Forms, the way it should be!

377 lines (368 loc) 13.8 kB
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