ngx-show-form-errors
Version:
An Angular library providing a directive to automatically display form error messages.
261 lines (250 loc) • 11.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Optional, Directive } from '@angular/core';
import * as i1 from '@angular/forms';
/*
* Copyright (c) Vinícius Bastos da Silva 2025
* This file is part of ngx-show-form-errors.
* Licensed under the GNU Lesser General Public License v3 (LGPL v3).
* See the LICENSE file in the project root for full details.
*/
const cssClassConfig = {
invalidControl: 'is-invalid',
containerErrors: 'invalid-feedback',
itemError: 'invalid-feedback-item',
itemErrorHide: 'd-none',
};
/*
* Copyright (c) Vinícius Bastos da Silva 2025
* This file is part of ngx-show-form-errors.
* Licensed under the GNU Lesser General Public License v3 (LGPL v3).
* See the LICENSE file in the project root for full details.
*/
class ShowFormErrorsBase {
elementRef;
formWasSubmitted;
controlsToValidate = [];
formElement;
subscriptionsToUnsubscribe = [];
get formIdPrefix() { return this.formElement?.id ? `${this.formElement.id}-` : ''; }
constructor(elementRef, formWasSubmitted) {
this.elementRef = elementRef;
this.formWasSubmitted = formWasSubmitted;
}
transformTemplates() {
this.controlsToValidate.forEach(controlToValidate => controlToValidate.transformErrorMessagesTemplate(this.formIdPrefix));
}
bindEventToRefreshValidationMessagesOnControls() {
for (let controlToValidate of this.controlsToValidate) {
this.formElement?.addEventListener('submit', this.refreshValidationMessagesState.bind(this, controlToValidate));
// Without valueChanges or statusChanges, the message will only change on blur, and not while the user types
const statusChangesSubscription = controlToValidate.control.statusChanges.subscribe(_ => this.refreshValidationMessagesState(controlToValidate));
this.subscriptionsToUnsubscribe.push(statusChangesSubscription);
controlToValidate.htmlElement.addEventListener('blur', _ => this.refreshValidationMessagesState(controlToValidate));
}
}
refreshValidationMessagesState(controlToValidate) {
if (!this.fieldShouldShowError(controlToValidate))
return;
controlToValidate.htmlElement.classList.toggle('is-invalid', controlToValidate.control.invalid);
controlToValidate.invalidFeedback.items.forEach(({ validator, element }) => element.classList.toggle(cssClassConfig.itemErrorHide, !controlToValidate.controlHasError(validator)));
}
// It can be a function defined by the user in the future
fieldShouldShowError(controlToValidate) {
const formIsSubmitted = this.formWasSubmitted() ?? false;
const controlIsTouched = controlToValidate.control.touched;
return formIsSubmitted || controlIsTouched; // || controlToValidate.control.dirty;
}
refreshValidationMessagesStateForAllControls() {
this.controlsToValidate.forEach(controlToValidate => this.refreshValidationMessagesState(controlToValidate));
}
}
/*
* Copyright (c) Vinícius Bastos da Silva 2025
* This file is part of ngx-show-form-errors.
* Licensed under the GNU Lesser General Public License v3 (LGPL v3).
* See the LICENSE file in the project root for full details.
*/
class HtmlHelper {
static getClosestFormParent(element) {
let formElement = element.closest('form');
if (!formElement)
throw new Error('NgxShowFormErrorsDirective: No form found');
return formElement;
}
static formControlHasADataField(formElement, controlName) {
return !!formElement.querySelector(`[data-field="${controlName}"]`);
}
}
/*
* Copyright (c) Vinícius Bastos da Silva 2025
* This file is part of ngx-show-form-errors.
* Licensed under the GNU Lesser General Public License v3 (LGPL v3).
* See the LICENSE file in the project root for full details.
*/
class ControlToValidate {
htmlElement;
control;
controlName;
invalidFeedback;
constructor(htmlElement, control, controlName) {
this.htmlElement = htmlElement;
this.control = control;
this.controlName = controlName;
const invalidFeedbackContainer = this.queryInClosestParentUntilForm(htmlElement, `[data-field="${this.controlName}"]`);
const elements = Array.from(invalidFeedbackContainer.querySelectorAll('[data-validator]'));
const invalidFeedbackItems = elements.map((element) => ({
validator: element.dataset['validator'],
element,
}));
this.invalidFeedback = {
container: invalidFeedbackContainer,
items: invalidFeedbackItems,
};
}
queryInClosestParentUntilForm(el, selector) {
const res = el.parentElement?.querySelector(selector);
if (!res &&
el.parentElement &&
el.parentElement?.tagName.toLowerCase() !== 'body')
return this.queryInClosestParentUntilForm(el.parentElement, selector);
return res;
}
transformErrorMessagesTemplate(formIdPrefix) {
this.invalidFeedback.container.classList.add(cssClassConfig.containerErrors);
this.invalidFeedback.items.forEach(({ validator, element }) => {
const controlIdentifier = this.htmlElement.id || this.controlName;
const validatorName = validator.replace('.', '-');
element.id = formIdPrefix + controlIdentifier + '-' + validatorName;
element.classList.add(cssClassConfig.itemError);
element.classList.add(cssClassConfig.itemErrorHide);
});
}
controlHasError(validator) {
if (!this.control.errors)
return false;
const validatorsToCheck = validator.split('.'); // Ex: 'required.minlength'
const existingErrors = Object.keys(this.control.errors).filter(e => this.control.errors[e]);
return validatorsToCheck.some(v => existingErrors.includes(v));
}
}
/*
* Copyright (c) Vinícius Bastos da Silva 2025
* This file is part of ngx-show-form-errors.
* Licensed under the GNU Lesser General Public License v3 (LGPL v3).
* See the LICENSE file in the project root for full details.
*/
class ShowFormErrorsSingleControl extends ShowFormErrorsBase {
ngControl;
constructor(elementRef, ngControl, formWasSubmitted) {
super(elementRef, formWasSubmitted);
this.ngControl = ngControl;
}
loadControls() {
const htmlElement = this.elementRef.nativeElement;
this.formElement = HtmlHelper.getClosestFormParent(htmlElement);
const controlName = htmlElement.getAttribute('name') ??
htmlElement.getAttribute('formControlName');
this.controlsToValidate.push(new ControlToValidate(htmlElement, this.ngControl.control, controlName));
}
}
/*
* Copyright (c) Vinícius Bastos da Silva 2025
* This file is part of ngx-show-form-errors.
* Licensed under the GNU Lesser General Public License v3 (LGPL v3).
* See the LICENSE file in the project root for full details.
*/
class ShowFormErrorsForm extends ShowFormErrorsBase {
formGroup;
constructor(elementRef, formGroup, formWasSubmitted) {
super(elementRef, formWasSubmitted);
this.formGroup = formGroup;
}
loadControls() {
this.formElement = this.elementRef.nativeElement;
this.formGroup = this.formGroup;
const controls = this.formGroup.controls;
for (let controlName in controls) {
if (!HtmlHelper.formControlHasADataField(this.formElement, controlName))
continue;
const element = this.formElement.querySelector(`[name="${controlName}"], [formControlName="${controlName}"]`);
if (element == null)
throw new Error(`Control ${controlName} not found!`);
this.controlsToValidate.push(new ControlToValidate(element, controls[controlName], controlName));
}
}
}
/*
* Copyright (c) Vinícius Bastos da Silva 2025
* This file is part of ngx-show-form-errors.
* Licensed under the GNU Lesser General Public License v3 (LGPL v3).
* See the LICENSE file in the project root for full details.
*/
class NgxShowFormErrorsDirective {
elementRef;
ngControl;
ngForm;
formGroupDirective;
showFormErrors;
constructor(elementRef, ngControl, ngForm, formGroupDirective) {
this.elementRef = elementRef;
this.ngControl = ngControl;
this.ngForm = ngForm;
this.formGroupDirective = formGroupDirective;
}
ngAfterViewInit() {
const formWasSubmitted = () => (this.ngForm || this.formGroupDirective)?.submitted;
switch (true) {
case !!this.ngControl:
this.showFormErrors = new ShowFormErrorsSingleControl(this.elementRef, this.ngControl, formWasSubmitted);
this.showFormErrors.loadControls();
this.showFormErrors.transformTemplates();
this.showFormErrors.bindEventToRefreshValidationMessagesOnControls();
break;
case !!this.ngForm:
// That is necessary because the ngForm is async
Promise.resolve().then(() => {
this.showFormErrors = new ShowFormErrorsForm(this.elementRef, this.ngForm.form, formWasSubmitted);
this.showFormErrors.loadControls();
this.showFormErrors.transformTemplates();
this.showFormErrors.bindEventToRefreshValidationMessagesOnControls();
this.showFormErrors.refreshValidationMessagesStateForAllControls();
});
break;
case !!this.formGroupDirective:
const formGroupWasSubmitted = () => this.formGroupDirective.submitted;
this.showFormErrors = new ShowFormErrorsForm(this.elementRef, this.formGroupDirective.form, formGroupWasSubmitted);
this.showFormErrors.loadControls();
this.showFormErrors.transformTemplates();
this.showFormErrors.bindEventToRefreshValidationMessagesOnControls();
break;
default:
throw new Error('NgxShowFormErrorsDirective: No control or form found');
}
}
ngOnDestroy() {
this.showFormErrors?.subscriptionsToUnsubscribe.forEach(s => s.unsubscribe());
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgxShowFormErrorsDirective, deps: [{ token: i0.ElementRef }, { token: i1.NgControl, optional: true }, { token: i1.NgForm, optional: true }, { token: i1.FormGroupDirective, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.4", type: NgxShowFormErrorsDirective, isStandalone: true, selector: "[ngxShowFormErrors]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgxShowFormErrorsDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngxShowFormErrors]',
standalone: true,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i1.NgControl, decorators: [{
type: Optional
}] }, { type: i1.NgForm, decorators: [{
type: Optional
}] }, { type: i1.FormGroupDirective, decorators: [{
type: Optional
}] }] });
/*
* Public API Surface of ngx-show-form-errors
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxShowFormErrorsDirective };
//# sourceMappingURL=ngx-show-form-errors.mjs.map