ngx-reactive-forms-utils
Version:
Reactive forms in Angular are a great way to manage forms. This library provides utilities that make it easier to work with reactive forms.
244 lines (235 loc) • 12.1 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, Component, Inject, Input, ContentChild } from '@angular/core';
import { NgControl } from '@angular/forms';
import { startWith, debounceTime, map, of, EMPTY, combineLatest } from 'rxjs';
import { AsyncPipe, JsonPipe } from '@angular/common';
function addCustomErrorMessage(name, fn) {
defaultCustomErrorMessages[name] = fn;
}
const defaultCustomErrorMessages = {
required: () => `This field is required`,
minlength: ({ requiredLength, actualLength }) => `Expected length of ${requiredLength} but got ${actualLength}`,
maxlength: ({ requiredLength, actualLength }) => `Expected length of ${requiredLength} but got ${actualLength}`,
min: ({ min }) => `You must provide a minimum value of ${min}.`,
max: ({ max, actual }) => `You entered a value of ${actual}, but the max you can enter is ${max}.`,
email: () => `Your email address is not valid.`,
number: () => `This field can only contain numbers.`,
confirmStringMatch: ({ field1, field2 }) => {
const formattedField1 = field1.replace(/([a-zA-Z])(?=[A-Z])/g, '$1 ').toLowerCase();
const formattedField2 = field2.replace(/([a-zA-Z])(?=[A-Z])/g, '$1 ').toLowerCase();
return `The values of ${formattedField1} and ${formattedField2} don't match.`;
},
minAge: ({ minAge, actual }) => `You must be at least ${minAge} years old. You are currently ${actual} years old.`,
maxAge: ({ maxAge, actual }) => `You must be no more than ${maxAge} years old. You are currently ${actual} years old.`,
};
const FORM_ERRORS = new InjectionToken('FORM_ERRORS', {
providedIn: 'root',
factory: () => defaultCustomErrorMessages,
});
const FORM_ERRORS_DEBOUNCE_TIME = new InjectionToken('FORM_ERRORS_DEBOUNCE_TIME', {
providedIn: 'root',
factory: () => 0,
});
class ControlErrorsDisplayComponent {
get rulesBroken() {
return this.rules.every((rule) => this.control[rule]);
}
constructor(_errors, debounceTime) {
this._errors = _errors;
this.debounceTime = debounceTime;
this.containerClasses = '';
this.errorClasses = '';
this.rules = ['touched'];
this._errorMessages = this._errors;
}
ngAfterContentInit() {
if (this.control) {
this.errorsList$ = this.control.statusChanges?.pipe(startWith(this.control.status), debounceTime(this.debounceTime), map(() => {
const errors = this.control.errors;
if (errors) {
return Object.keys(errors).map((errorKey) => {
const getError = this._errorMessages[errorKey];
return getError ? getError(errors[errorKey]) : 'Unknown Error';
});
}
return [];
}));
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: ControlErrorsDisplayComponent, deps: [{ token: FORM_ERRORS }, { token: FORM_ERRORS_DEBOUNCE_TIME }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.3", type: ControlErrorsDisplayComponent, isStandalone: true, selector: "ngx-control-errors-display", inputs: { containerClasses: "containerClasses", errorClasses: "errorClasses", rules: "rules" }, queries: [{ propertyName: "control", first: true, predicate: NgControl, descendants: true, static: true }], ngImport: i0, template: "<div class=\"{{ containerClasses }}\">\n\t<ng-content></ng-content>\n\n\t@if (rulesBroken) {\n\t\t@if (errorsList$ | async; as errorsList) {\n\t\t\t@if (errorsList && errorsList.length) {\n\t\t\t\t@for (error of errorsList; track error) {\n\t\t\t\t\t<p class=\"{{ errorClasses }}\">{{ error }}</p>\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n</div>\n", styles: [""], dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: ControlErrorsDisplayComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-control-errors-display', standalone: true, imports: [AsyncPipe], template: "<div class=\"{{ containerClasses }}\">\n\t<ng-content></ng-content>\n\n\t@if (rulesBroken) {\n\t\t@if (errorsList$ | async; as errorsList) {\n\t\t\t@if (errorsList && errorsList.length) {\n\t\t\t\t@for (error of errorsList; track error) {\n\t\t\t\t\t<p class=\"{{ errorClasses }}\">{{ error }}</p>\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n</div>\n" }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [FORM_ERRORS]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [FORM_ERRORS_DEBOUNCE_TIME]
}] }], propDecorators: { containerClasses: [{
type: Input
}], errorClasses: [{
type: Input
}], rules: [{
type: Input
}], control: [{
type: ContentChild,
args: [NgControl, { static: true }]
}] } });
function calculateAge(birthdate) {
const currentDate = fixTimezoneOffset(new Date());
const birthDate = fixTimezoneOffset(new Date(birthdate));
let age = currentDate.getFullYear() - birthDate.getFullYear();
if (currentDate.getMonth() < birthDate.getMonth() ||
(currentDate.getMonth() === birthDate.getMonth() && currentDate.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
function fixTimezoneOffset(date) {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
}
class CustomValidators {
static phoneNumber(control) {
if (!control.value) {
return null;
}
// eslint-disable-next-line no-useless-escape
const PHONE_REGEX = /^(1\s?)?((\([2-9][0-9]{2}\))|[2-9][0-9]{2})[\s\-]?[2-9][0-9]{2}[\s\-]?[0-9]{4}$/;
return PHONE_REGEX.test(control.value) ? null : { phoneNumber: true };
}
static number(control) {
// eslint-disable-next-line no-useless-escape
const NUMBER_REGEX = /^([0-9])*([\.])?[0-9]+$/;
return NUMBER_REGEX.test(control.value) ? null : { number: true };
}
static validZipCode(control) {
if (!control.value) {
return null;
}
// XXXXX or XXXXX-XXXX
const ZIP_REGEX = /(^\d{5}$)|(^\d{5}-\d{4}$)/;
return ZIP_REGEX.test(control.value) ? null : { validZipCode: true };
}
static confirmStringMatch(field1, field2) {
return (form) => {
const valueControl = form.get(field1);
const confirmValueControl = form.get(field2);
if (!valueControl || !confirmValueControl || !valueControl.value || !confirmValueControl.value) {
return null;
}
if (confirmValueControl.errors && !confirmValueControl.errors[field2]) {
return null;
}
if (valueControl.value !== confirmValueControl.value) {
confirmValueControl.setErrors({ confirmStringMatch: { field1, field2 } });
return { confirmStringMatch: { field1, field2 } };
}
confirmValueControl.setErrors(null);
return null;
};
}
static minAge(minAge) {
return (control) => {
if (!control.value) {
return null;
}
const birthDate = new Date(control.value);
const age = calculateAge(birthDate);
if (age < minAge) {
return { minAge: { minAge, actual: age } };
}
return null;
};
}
static maxAge(maxAge) {
return (control) => {
if (!control.value) {
return null;
}
const birthDate = new Date(control.value);
const age = calculateAge(birthDate);
if (age > maxAge) {
return { maxAge: { maxAge, actual: age } };
}
return null;
};
}
}
var FormDebugFieldEnum;
(function (FormDebugFieldEnum) {
FormDebugFieldEnum["Value"] = "Value";
FormDebugFieldEnum["FormErrors"] = "FormErrors";
FormDebugFieldEnum["ControlErrors"] = "ControlErrors";
FormDebugFieldEnum["Status"] = "Status";
FormDebugFieldEnum["Valid"] = "Valid";
FormDebugFieldEnum["Invalid"] = "Invalid";
})(FormDebugFieldEnum || (FormDebugFieldEnum = {}));
const DEFAULT_DEBUG_FIELDS = Object.keys(FormDebugFieldEnum).map((key) => key);
function debugForm(form, debugFields = [...DEFAULT_DEBUG_FIELDS]) {
if (!form) {
return of(EMPTY);
}
return combineLatest([
form.valueChanges.pipe(startWith(form.value)),
form.statusChanges.pipe(startWith(form.status)),
]).pipe(map(() => {
const returnObject = {};
if (debugFields.includes(FormDebugFieldEnum.Value)) {
returnObject.value = form.value;
}
if (debugFields.includes(FormDebugFieldEnum.FormErrors)) {
returnObject.formErrors = form.errors;
}
if (debugFields.includes(FormDebugFieldEnum.ControlErrors)) {
const controlErrors = Object.keys(form.controls).reduce((acc, controlName) => {
const control = form.get(controlName);
return {
...acc,
[controlName]: {
errors: control?.errors,
status: control?.status,
},
};
}, {});
returnObject.controlErrors = controlErrors;
}
if (debugFields.includes(FormDebugFieldEnum.Status)) {
returnObject.status = form.status;
}
if (debugFields.includes(FormDebugFieldEnum.Valid)) {
returnObject.valid = form.valid;
}
return returnObject;
}));
}
class FormDebugDisplayComponent {
constructor() {
this.debugFields = [...DEFAULT_DEBUG_FIELDS];
}
ngOnChanges(changes) {
if (changes['form'] || changes['debugFields']) {
if (this.form) {
this.debugData$ = debugForm(this.form, this.debugFields);
}
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: FormDebugDisplayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.3", type: FormDebugDisplayComponent, isStandalone: true, selector: "ngx-form-debug-display", inputs: { debugFields: "debugFields", form: "form" }, usesOnChanges: true, ngImport: i0, template: "@if (form) {\n\t<pre><code>\n{{ debugData$ | async | json }}\n</code></pre>\n}\n", styles: ["pre{padding:0 12px 12px;background-color:#f4f4f4;border:1px solid #333;border-radius:5px}\n"], dependencies: [{ kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: JsonPipe, name: "json" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: FormDebugDisplayComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-form-debug-display', standalone: true, imports: [AsyncPipe, JsonPipe], template: "@if (form) {\n\t<pre><code>\n{{ debugData$ | async | json }}\n</code></pre>\n}\n", styles: ["pre{padding:0 12px 12px;background-color:#f4f4f4;border:1px solid #333;border-radius:5px}\n"] }]
}], propDecorators: { debugFields: [{
type: Input
}], form: [{
type: Input,
args: [{ required: true }]
}] } });
/**
* Generated bundle index. Do not edit.
*/
export { ControlErrorsDisplayComponent, CustomValidators, DEFAULT_DEBUG_FIELDS, FORM_ERRORS, FORM_ERRORS_DEBOUNCE_TIME, FormDebugDisplayComponent, FormDebugFieldEnum, addCustomErrorMessage, debugForm, defaultCustomErrorMessages };
//# sourceMappingURL=ngx-reactive-forms-utils.mjs.map