UNPKG

ngx-show-form-errors

Version:

An Angular library providing a directive to automatically display form error messages.

261 lines (250 loc) 11.7 kB
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