UNPKG

ngx-valdemort

Version:

Simple, consistent validation error messages for your Angular forms

687 lines (675 loc) 42.2 kB
import * as i0 from '@angular/core'; import { signal, Injectable, inject, TemplateRef, input, Directive, contentChildren, contentChild, computed, ChangeDetectionStrategy, Component, NgModule } from '@angular/core'; import { ControlContainer } from '@angular/forms'; import { NgTemplateOutlet } from '@angular/common'; /** * Service used by the default validation errors directive to store the default error template references. This * service is injected in the validation errors component which displays the appropriate templates and provides their context. */ class DefaultValidationErrors { directives = signal([], { ...(ngDevMode ? { debugName: "directives" } : {}) }); fallback = signal(undefined, { ...(ngDevMode ? { debugName: "fallback" } : {}) }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: DefaultValidationErrors, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: DefaultValidationErrors, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: DefaultValidationErrors, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Directive allowing to define the template for an error of a given type (using the `valError` input), using an ng-template. * It's used inside the body of the validation errors component, or inside the body of the default validation errors directive. * See the documentation of these two for example usages. */ class ValidationErrorDirective { templateRef = inject(TemplateRef); /** * The type of the error that the content of the template must display. */ type = input.required({ ...(ngDevMode ? { debugName: "type" } : {}), alias: 'valError' }); static ngTemplateContextGuard(_directive, context) { return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValidationErrorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.2", type: ValidationErrorDirective, isStandalone: true, selector: "ng-template[valError]", inputs: { type: { classPropertyName: "type", publicName: "valError", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValidationErrorDirective, decorators: [{ type: Directive, args: [{ selector: 'ng-template[valError]' }] }], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "valError", required: true }] }] } }); /** * Directive allowing to define a fallback template for an error of a type that is not handled by any validation error directive. * It's used inside the body of the validation errors component, or inside the body of the default validation errors directive. * See the documentation of these two for example usages. * * This is useful to handle forgotten errors instead of displaying no error at all, or to handle all or several error types in the same way, * for example by relying on the error key to choose an internationalized message. */ class ValidationFallbackDirective { templateRef = inject(TemplateRef); static ngTemplateContextGuard(_directive, context) { return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValidationFallbackDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.2", type: ValidationFallbackDirective, isStandalone: true, selector: "ng-template[valFallback]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValidationFallbackDirective, decorators: [{ type: Directive, args: [{ selector: 'ng-template[valFallback]' }] }] }); /** * Directive allowing to register default templates for validation error messages. It's supposed to be used once, * typically in the root component. By using templates to do that, error messages can * - easily be i18ned * - easily use pipes * - easily use HTML * - easily be ordered * * Example usage: * ``` * <val-default-errors> * <ng-template valError="required">This field is mandatory</ng-template> * <ng-template valError="max" let-error="error">This field must be at most {{ error.max | number }}</ng-template> * </val-default-errors> * ``` * * Example usage where a label is used to make the messages less generic: * ``` * <val-default-errors> * <ng-template valError="required" let-label>{{ label }} is mandatory</ng-template> * <ng-template valError="max" let-error="error" let-label>{{ label }} must be at most {{ error.max | number }}</ng-template> * </val-default-errors> * ``` * * A fallback template can also be provided. This fallback template is used for all the errors that exist on the form control * but are not handled by any of the specific error templates: * ``` * <val-default-errors> * <ng-template valError="required" let-label>{{ label }} is mandatory</ng-template> * <ng-template valError="max" let-error="error" let-label>{{ label }} must be at most {{ error.max | number }}</ng-template> * <ng-template valFallback let-label let-type="type" let-error="error">{{ label }} has an unhandled error of type {{ type }}: {{ error | json }}</ng-template> * </val-default-errors> * ``` * Using the fallback can also be used to handle all the errors the same way, for example by using the error type as an i18n key * to display the appropriate error message. * * This directive stores the default template references in a service, that is then injected in the validation errors components * to be reused. */ class DefaultValidationErrorsDirective { defaultValidationErrors = inject(DefaultValidationErrors); /** * The list of validation error directives (i.e. <ng-template valError="...">) * contained inside the directive element. */ errorDirectives = contentChildren(ValidationErrorDirective, { ...(ngDevMode ? { debugName: "errorDirectives" } : {}) }); /** * The validation fallback directive (i.e. <ng-template valFallback>) contained inside the directive element. */ fallbackDirective = contentChild(ValidationFallbackDirective, { ...(ngDevMode ? { debugName: "fallbackDirective" } : {}) }); ngAfterContentInit() { this.defaultValidationErrors.directives.set(this.errorDirectives()); this.defaultValidationErrors.fallback.set(this.fallbackDirective()); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: DefaultValidationErrorsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.0.2", type: DefaultValidationErrorsDirective, isStandalone: true, selector: "val-default-errors", queries: [{ propertyName: "errorDirectives", predicate: ValidationErrorDirective, isSignal: true }, { propertyName: "fallbackDirective", first: true, predicate: ValidationFallbackDirective, descendants: true, isSignal: true }], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: DefaultValidationErrorsDirective, decorators: [{ type: Directive, args: [{ // eslint-disable-next-line @angular-eslint/directive-selector selector: 'val-default-errors' }] }], propDecorators: { errorDirectives: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ValidationErrorDirective), { isSignal: true }] }], fallbackDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ValidationFallbackDirective), { isSignal: true }] }] } }); /** * The display mode of the validation errors. For a given control, either all the validation errors * are displayed, or only the first one. */ var DisplayMode; (function (DisplayMode) { DisplayMode[DisplayMode["ALL"] = 0] = "ALL"; DisplayMode[DisplayMode["ONE"] = 1] = "ONE"; })(DisplayMode || (DisplayMode = {})); /** * The configuration service used by the validation errors component to apply common rules for all * form controls. * * To change its default behavior, you can either inject this service in your root module or component and mutate it, * or define your own implementation and provide it. */ class ValdemortConfig { /** * The display mode of the errors. The default value is ALL, meaning that all the errors existing on a control * (and which have an error template defined) are displayed. */ displayMode = DisplayMode.ALL; /** * Specifies one or several CSS classes (separated by a white space) that are automatically added to the * validation errors element. This can be useful to reuse a standard CSS class of your CSS framework (like * .invalid-feedback in BootStrap), rather than styling the val-errors element itself. * * The default value is null (no class is added). */ errorsClasses = null; /** * Specifies one or several CSS classes (separated by a white space) that are automatically added to the * each validation error message element. This can be useful to reuse a standard CSS class of your CSS framework * rather than styling the div element itself. * * The default value is null (no class is added). */ errorClasses = null; /** * Specifies when error messages should be displayed based on the state of the control itself (touched, dirty, etc.) * and on the state of the form directive containing it (if any). This function is only called if the control is invalid * in the first place: if it's valid, errors are never displayed. * * The default value of this function returns true if the control is touched, or if the form (if any) is submitted. */ shouldDisplayErrors = (control, form) => control.touched || (!!form && form.submitted); /** * **Experimental** * * Specifies when error messages should be displayed based on the state of the field (touched, dirty, etc.). * This function must be reactive (i.e. it must return its value by reading signals). * Note that if the field is valid, errors are never displayed, whatever ths function returns. * * The default value of this function returns true if the field is touched. */ shouldDisplayFieldErrors = (fieldState) => fieldState.touched(); /** * Specifies if the library should throw an error when a control is not found. * For example, this can happen if a typo was made in the `controlName`. * If the check is enabled, then an error will be thrown in such a case. * Otherwise, the error is silently ignored. * * The default value of this function returns false, thus disabling the check. * * You can enable the check by giving it a function that returns true, * or you can enable it only in development for example with: * `config.shouldThrowOnMissingControl = () => !environment.production` */ shouldThrowOnMissingControl = () => false; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValdemortConfig, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValdemortConfig, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValdemortConfig, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const NO_ERRORS$1 = { shouldDisplayErrors: false }; const NO_VALIDATION_STATE = { control: null, errorsDisplayed: null, errors: null }; function areValidationStatesEqual(previous, current) { return previous.control === current.control && previous.errorsDisplayed === current.errorsDisplayed && previous.errors === current.errors; } /** * Component allowing to display validation error messages associated to a given form control, form group or form array. * The control is provided using the `control` input of the component. If it's used inside an enclosing form group or * form array, it can instead be provided using the `controlName` input of the component. * * Example usage where the control itself is being passed as input: * ``` * <val-errors [control]="form.controls.birthDate"> * <ng-template valError="required">The birth date is mandatory</ng-template> * <ng-template valError="max" let-error="error">The max value for the birth date is {{ error.max | number }}</ng-template> * </val-errors> * ``` * * Example usage where the control name is being passed as input: * ``` * <val-errors controlName="birthDate"> * <ng-template valError="required">The birth date is mandatory</ng-template> * <ng-template valError="max" let-error="error">The max value for the birth date is {{ error.max | number }}</ng-template> * </val-errors> * ``` * * This component, if the control is invalid, displays its validation errors using the provided templates. * The templates, as shown in the above example, have access to the validation error itself. * * The label of the control can also be provided as input, and then used in the templates: * ``` * <val-errors controlName="birthDate" label="the birth date"> * <ng-template valError="required" let-label>{{ label }} is mandatory</ng-template> * <ng-template valError="max" let-error="error" let-label>The max value for {{ label }} is {{ error.max | number }}</ng-template> * </val-errors> * ``` * * The component‘s behavior is configured globally by the Config service (see its documentation for more details). It can * - display the first error, or all the errors * - add CSS classes to its host `<val-errors>` element * - add CSS classes to each error message element being displayed * - choose when to display the errors (dirty, touched, touched and submitted, etc.) * * Global, default templates can be defined (and used by this component) using the default validation errors directive * (see its documentation for details). So, if the default error messages are defined and sufficient for a given control, all you * need is * * ``` * <val-errors controlName="birthDate"></val-errors> * ``` * * or, if the default templates expect a label: * * ``` * <val-errors controlName="birthDate" label="the birth date"></val-errors> * ``` * * If, however, you want to override one or several error messages by custom ones, you can do so by simply defining them inside the * component: * * ``` * <val-errors controlName="birthDate" label="the birth date"> * <ng-template valError="max">You're too young, sorry</ng-template> * </val-errors> * ``` * * A fallback template can also be provided. This fallback template is used for all the errors that exist on the form control * but are not handled by any of the specific error templates: * ``` * <val-errors controlName="birthDate" label="the birth date"> * <ng-template valError="max">You're too young, sorry</ng-template> * <ng-template valFallback let-label let-type="type" let-error="error">{{ label }} has an unhandled error of type {{ type }}: {{ error | json }}</ng-template> * </val-errors> * ``` * Note that, the fallback template can also be defined in the default validation errors directive (see its documentation for details). * If a fallback template is defined inside `val-errors`, it overrides the default fallback. * * If an error is present on the control, but doesn't have any template, default template or fallback template defined for its type, * then it's not displayed. If the control is valid, or if none of the errors of the component has a matching template or default template, * then this component itself is hidden. */ class ValidationErrorsComponent { /** * The FormControl, FormGroup or FormArray containing the validation errors. * If set, the controlName input is ignored */ control = input(null, { ...(ngDevMode ? { debugName: "control" } : {}) }); /** * The name (or the index, in case it's contained in a FormArray) of the FormControl, FormGroup or FormArray containing the validation * errors. * Ignored if the control input is set, and only usable if the control to validate is part of a control container */ controlName = input(null, { ...(ngDevMode ? { debugName: "controlName" } : {}) }); /** * The label of the field, exposed to templates so they can use it in the error message. */ label = input(null, { ...(ngDevMode ? { debugName: "label" } : {}) }); /** * The list of validation error directives (i.e. <ng-template valError="...">) contained inside the component element. */ errorDirectives = contentChildren(ValidationErrorDirective, { ...(ngDevMode ? { debugName: "errorDirectives" } : {}) }); /** * The validation fallback directive (i.e. <ng-template valFallback>) contained inside the component element. */ fallbackDirective = contentChild(ValidationFallbackDirective, { ...(ngDevMode ? { debugName: "fallbackDirective" } : {}) }); /** * The Config service instance, defining the behavior of this component */ config = inject(ValdemortConfig); errorsClasses = this.config.errorsClasses || ''; errorClasses = this.config.errorClasses || ''; validationState = signal(NO_VALIDATION_STATE, { ...(ngDevMode ? { debugName: "validationState" } : {}), equal: areValidationStatesEqual }); /** * The DefaultValidationErrors service instance, holding the default error templates, * optionally defined by using the default validation errors directive */ defaultValidationErrors = inject(DefaultValidationErrors); /** * The control container, if it exists, as one of the 4 form group or form array directives that can "wrap" the control. * It's injected so that we can know if it exists and, if it does, if its form directive has been submitted or not: * the config service shouldDisplayErrors function can choose (and does by default) to use that information. */ controlContainer = inject(ControlContainer, { optional: true }); vm = computed(() => { const ctrl = this.validationState().control; if (this.shouldDisplayErrors(ctrl)) { const errorsToDisplay = this.findErrorsToDisplay(ctrl); return { shouldDisplayErrors: true, control: ctrl, errorsToDisplay }; } else { return NO_ERRORS$1; } }, { ...(ngDevMode ? { debugName: "vm" } : {}) }); ngDoCheck() { const ctrl = this.findActualControl(); if (ctrl) { const formDirective = this.controlContainer?.formDirective; const errorsDisplayed = this.config.shouldDisplayErrors(ctrl, formDirective); this.validationState.set({ control: ctrl, errorsDisplayed, errors: ctrl.errors }); } else { this.validationState.set(NO_VALIDATION_STATE); } } shouldDisplayErrors(ctrl) { if (!ctrl || !ctrl.invalid || !this.hasDisplayableError(ctrl)) { return false; } const form = this.controlContainer && this.controlContainer.formDirective; return this.config.shouldDisplayErrors(ctrl, form ?? undefined); } findErrorsToDisplay(ctrl) { const mergedDirectives = []; const fallbackErrors = []; const alreadyMetTypes = new Set(); const shouldContinue = () => this.config.displayMode === DisplayMode.ALL || (mergedDirectives.length === 0 && fallbackErrors.length === 0); const defaultValidationErrorDirectives = this.defaultValidationErrors.directives(); for (let i = 0; i < defaultValidationErrorDirectives.length && shouldContinue(); i++) { const defaultDirective = defaultValidationErrorDirectives[i]; if (ctrl.hasError(defaultDirective.type())) { const customDirectiveOfSameType = this.errorDirectives().find(dir => dir.type() === defaultDirective.type()); mergedDirectives.push(customDirectiveOfSameType || defaultDirective); } alreadyMetTypes.add(defaultDirective.type()); } if (shouldContinue()) { const customDirectives = this.errorDirectives(); for (let i = 0; i < customDirectives.length && shouldContinue(); i++) { const customDirective = customDirectives[i]; if (!alreadyMetTypes.has(customDirective.type()) && ctrl.hasError(customDirective.type())) { mergedDirectives.push(customDirective); } alreadyMetTypes.add(customDirective.type()); } } if (shouldContinue() && (this.fallbackDirective() || this.defaultValidationErrors.fallback())) { const allErrors = Object.entries(ctrl.errors ?? []); for (let i = 0; i < allErrors.length && shouldContinue(); i++) { const [type, value] = allErrors[i]; if (!alreadyMetTypes.has(type)) { fallbackErrors.push({ type, value }); } } } return { errors: mergedDirectives, fallback: this.fallbackDirective() ?? this.defaultValidationErrors.fallback(), fallbackErrors }; } findActualControl() { const ctrl = this.control(); const ctrlName = this.controlName(); if (ctrl) { return ctrl; } else if (ctrlName != null && this.controlContainer?.control?.controls) { // whether the control is a FormGroup or a FormArray, we must use .control[ctrlName] to get it const control = (this.controlContainer?.control).controls[ctrlName]; if (this.config.shouldThrowOnMissingControl()) { // if the control is null, then there are two cases: // - we are in a template driven form, and the controls might not be initialized yet // - there was an error in the control name. If so, let's throw an error to help developers // to avoid false positive in template driven forms, we check if the controls are initialized // by checking if the `controls` object or array has any element if (!control && Object.keys(this.controlContainer?.control?.controls).length > 0) { throw new Error(`ngx-valdemort: no control found for controlName: '${ctrlName}'.`); } } return control; } return null; } hasDisplayableError(ctrl) { return (ctrl.errors && (this.fallbackDirective() || this.defaultValidationErrors.fallback() || Object.keys(ctrl.errors).some(type => this.defaultValidationErrors.directives().some(dir => dir.type() === type) || this.errorDirectives().some(dir => dir.type() === type)))); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValidationErrorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.2", type: ValidationErrorsComponent, isStandalone: true, selector: "val-errors", inputs: { control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, controlName: { classPropertyName: "controlName", publicName: "controlName", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "errorsClasses", "style.display": "vm().shouldDisplayErrors ? '' : 'none'" } }, queries: [{ propertyName: "errorDirectives", predicate: ValidationErrorDirective, isSignal: true }, { propertyName: "fallbackDirective", first: true, predicate: ValidationFallbackDirective, descendants: true, isSignal: true }], ngImport: i0, template: "@let vm = this.vm();\n@if (vm.shouldDisplayErrors) {\n @for (errorDirective of vm.errorsToDisplay.errors; track $index) {\n <div [class]=\"errorClasses\">\n <ng-container\n *ngTemplateOutlet=\"\n errorDirective!.templateRef;\n context: {\n $implicit: label(),\n error: vm.control.errors![errorDirective.type()]\n }\n \"\n />\n </div>\n }\n @for (error of vm.errorsToDisplay.fallbackErrors; track $index) {\n <div [class]=\"errorClasses\">\n <ng-container\n *ngTemplateOutlet=\"\n vm.errorsToDisplay.fallback!.templateRef;\n context: {\n $implicit: label(),\n type: error.type,\n error: error.value\n }\n \"\n />\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValidationErrorsComponent, decorators: [{ type: Component, args: [{ selector: 'val-errors', host: { '[class]': 'errorsClasses', '[style.display]': `vm().shouldDisplayErrors ? '' : 'none'` }, imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let vm = this.vm();\n@if (vm.shouldDisplayErrors) {\n @for (errorDirective of vm.errorsToDisplay.errors; track $index) {\n <div [class]=\"errorClasses\">\n <ng-container\n *ngTemplateOutlet=\"\n errorDirective!.templateRef;\n context: {\n $implicit: label(),\n error: vm.control.errors![errorDirective.type()]\n }\n \"\n />\n </div>\n }\n @for (error of vm.errorsToDisplay.fallbackErrors; track $index) {\n <div [class]=\"errorClasses\">\n <ng-container\n *ngTemplateOutlet=\"\n vm.errorsToDisplay.fallback!.templateRef;\n context: {\n $implicit: label(),\n type: error.type,\n error: error.value\n }\n \"\n />\n </div>\n }\n}\n" }] }], propDecorators: { control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], controlName: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlName", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], errorDirectives: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ValidationErrorDirective), { isSignal: true }] }], fallbackDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ValidationFallbackDirective), { isSignal: true }] }] } }); const NO_ERRORS = { shouldDisplayErrors: false }; /** * **Experimental** * * Component allowing to display validation error messages associated to a given field of a signal form. * The control is provided using the `forField` input of the component. * * Example usage: * ``` * <val-signal-errors [forField]="form.birthYear"> * <ng-template valError="required">The birth year is mandatory</ng-template> * <ng-template valError="max" let-error="error">The max value for the birth year is {{ error.max | number }}</ng-template> * </val-errors> * ``` * * This component, if the control is invalid, displays its validation errors using the provided templates. * The templates, as shown in the above example, have access to the validation error itself. * * The label of the control can also be provided as input, and then used in the templates: * ``` * <val-signal-errors [forField]="form.birthYear" label="the birth year"> * <ng-template valError="required" let-label>{{ label }} is mandatory</ng-template> * <ng-template valError="max" let-error="error" let-label>The max value for {{ label }} is {{ error.max | number }}</ng-template> * </val-signal-errors> * ``` * * The component‘s behavior is configured globally by the Config service (see its documentation for more details). It can * - display the first error, or all the errors * - add CSS classes to its host `<val-errors>` element * - add CSS classes to each error message element being displayed * - choose when to display the errors (dirty, touched, etc.) * * Global, default templates can be defined (and used by this component) using the default validation errors directive * (see its documentation for details). So, if the default error messages are defined and sufficient for a given control, all you * need is * * ``` * <val-signal-errors [forField]="form.birthYear" /> * ``` * * or, if the default templates expect a label: * * ``` * <val-signal-errors [forField]="form.birthYear" label="the birth year" /> * ``` * * If, however, you want to override one or several error messages by custom ones, you can do so by simply defining them inside the * component: * * ``` * <val-signal-errors [forField]="form.birthYear" label="the birth year"> * <ng-template valError="max">You're too young, sorry</ng-template> * </val-signal-errors> * ``` * * A fallback template can also be provided. This fallback template is used for all the errors that exist on the form control * but are not handled by any of the specific error templates: * ``` * <val-signal-errors [forField]="form.birthYear" label="the birth year"> * <ng-template valError="max">You're too young, sorry</ng-template> * <ng-template valFallback let-label let-type="type" let-error="error">{{ label }} has an unhandled error of kind {{ type }}: {{ error | json }}</ng-template> * </val-signal-errors> * ``` * Note that, the fallback template can also be defined in the default validation errors directive (see its documentation for details). * If a fallback template is defined inside `val-signal-errors`, it overrides the default fallback. * * If an error is present on the field, but doesn't have any template, default template or fallback template defined for its type, * then it's not displayed. If the field is valid, or if none of the errors of the component has a matching template or default template, * then this component itself is hidden. */ class ValidationSignalErrorsComponent { /** * The FieldTree containing the validation errors. */ // eslint-disable-next-line @angular-eslint/no-input-rename field = input.required({ ...(ngDevMode ? { debugName: "field" } : {}), alias: 'forField' }); /** * The label of the field, exposed to templates so they can use it in the error message. */ label = input(null, { ...(ngDevMode ? { debugName: "label" } : {}) }); /** * The list of validation error directives (i.e. <ng-template valError="...">) contained inside the component element. */ errorDirectives = contentChildren(ValidationErrorDirective, { ...(ngDevMode ? { debugName: "errorDirectives" } : {}) }); /** * The validation fallback directive (i.e. <ng-template valFallback>) contained inside the component element. */ fallbackDirective = contentChild(ValidationFallbackDirective, { ...(ngDevMode ? { debugName: "fallbackDirective" } : {}) }); /** * The Config service instance, defining the behavior of this component */ config = inject(ValdemortConfig); errorsClasses = this.config.errorsClasses || ''; errorClasses = this.config.errorClasses || ''; /** * The DefaultValidationErrors service instance, holding the default error templates, * optionally defined by using the default validation errors directive */ defaultValidationErrors = inject(DefaultValidationErrors); hasDisplayableError = computed(() => { const field = this.field(); const errors = field().errors(); return (errors.length > 0 && (!!this.fallbackDirective() || !!this.defaultValidationErrors.fallback() || errors.some(error => this.defaultValidationErrors.directives().some(dir => dir.type() === error.kind) || this.errorDirectives().some(dir => dir.type() === error.kind)))); }, { ...(ngDevMode ? { debugName: "hasDisplayableError" } : {}) }); shouldDisplayErrors = computed(() => { const field = this.field(); const fieldState = field(); if (!fieldState.invalid() || !this.hasDisplayableError()) { return false; } return this.config.shouldDisplayFieldErrors(fieldState); }, { ...(ngDevMode ? { debugName: "shouldDisplayErrors" } : {}) }); vm = computed(() => { if (this.shouldDisplayErrors()) { const errorsToDisplay = this.findErrorsToDisplay(); return { shouldDisplayErrors: true, errorsToDisplay }; } else { return NO_ERRORS; } }, { ...(ngDevMode ? { debugName: "vm" } : {}) }); findErrorsToDisplay() { const field = this.field(); const fieldErrors = field().errors(); const mergedErrors = []; const fallbackErrors = []; const alreadyMetTypes = new Set(); const shouldContinue = () => this.config.displayMode === DisplayMode.ALL || (mergedErrors.length === 0 && fallbackErrors.length === 0); const defaultValidationErrorDirectives = this.defaultValidationErrors.directives(); for (let i = 0; i < defaultValidationErrorDirectives.length && shouldContinue(); i++) { const defaultDirective = defaultValidationErrorDirectives[i]; const error = fieldErrors.find(error => error.kind === defaultDirective.type()); if (error) { const customDirectiveOfSameType = this.errorDirectives().find(dir => dir.type() === defaultDirective.type()); mergedErrors.push({ directive: customDirectiveOfSameType ?? defaultDirective, error }); } alreadyMetTypes.add(defaultDirective.type()); } if (shouldContinue()) { const customDirectives = this.errorDirectives(); for (let i = 0; i < customDirectives.length && shouldContinue(); i++) { const customDirective = customDirectives[i]; if (!alreadyMetTypes.has(customDirective.type())) { const error = fieldErrors.find(error => error.kind === customDirective.type()); if (error) { mergedErrors.push({ directive: customDirective, error }); } } alreadyMetTypes.add(customDirective.type()); } } if (shouldContinue() && (this.fallbackDirective() || this.defaultValidationErrors.fallback())) { for (let i = 0; i < fieldErrors.length && shouldContinue(); i++) { const error = fieldErrors[i]; if (!alreadyMetTypes.has(error.kind)) { fallbackErrors.push(error); } } } return { errors: mergedErrors, fallback: this.fallbackDirective() ?? this.defaultValidationErrors.fallback(), fallbackErrors }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValidationSignalErrorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.2", type: ValidationSignalErrorsComponent, isStandalone: true, selector: "val-signal-errors", inputs: { field: { classPropertyName: "field", publicName: "forField", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "errorsClasses", "style.display": "vm().shouldDisplayErrors ? '' : 'none'" } }, queries: [{ propertyName: "errorDirectives", predicate: ValidationErrorDirective, isSignal: true }, { propertyName: "fallbackDirective", first: true, predicate: ValidationFallbackDirective, descendants: true, isSignal: true }], ngImport: i0, template: "@let vm = this.vm();\n@if (vm.shouldDisplayErrors) {\n @for (errorWithDirective of vm.errorsToDisplay.errors; track $index) {\n <div [class]=\"errorClasses\">\n <ng-container\n *ngTemplateOutlet=\"\n errorWithDirective.directive.templateRef;\n context: {\n $implicit: label(),\n error: errorWithDirective.error\n }\n \"\n />\n </div>\n }\n @for (error of vm.errorsToDisplay.fallbackErrors; track $index) {\n <div [class]=\"errorClasses\">\n <ng-container\n *ngTemplateOutlet=\"\n vm.errorsToDisplay.fallback!.templateRef;\n context: {\n $implicit: label(),\n type: error.kind,\n error: error\n }\n \"\n />\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValidationSignalErrorsComponent, decorators: [{ type: Component, args: [{ selector: 'val-signal-errors', host: { '[class]': 'errorsClasses', '[style.display]': `vm().shouldDisplayErrors ? '' : 'none'` }, imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let vm = this.vm();\n@if (vm.shouldDisplayErrors) {\n @for (errorWithDirective of vm.errorsToDisplay.errors; track $index) {\n <div [class]=\"errorClasses\">\n <ng-container\n *ngTemplateOutlet=\"\n errorWithDirective.directive.templateRef;\n context: {\n $implicit: label(),\n error: errorWithDirective.error\n }\n \"\n />\n </div>\n }\n @for (error of vm.errorsToDisplay.fallbackErrors; track $index) {\n <div [class]=\"errorClasses\">\n <ng-container\n *ngTemplateOutlet=\"\n vm.errorsToDisplay.fallback!.templateRef;\n context: {\n $implicit: label(),\n type: error.kind,\n error: error\n }\n \"\n />\n </div>\n }\n}\n" }] }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "forField", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], errorDirectives: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ValidationErrorDirective), { isSignal: true }] }], fallbackDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ValidationFallbackDirective), { isSignal: true }] }] } }); class ValdemortModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValdemortModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.2", ngImport: i0, type: ValdemortModule, imports: [ValidationErrorsComponent, ValidationSignalErrorsComponent, ValidationErrorDirective, ValidationFallbackDirective, DefaultValidationErrorsDirective], exports: [ValidationErrorsComponent, ValidationSignalErrorsComponent, ValidationErrorDirective, ValidationFallbackDirective, DefaultValidationErrorsDirective] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValdemortModule }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.2", ngImport: i0, type: ValdemortModule, decorators: [{ type: NgModule, args: [{ imports: [ ValidationErrorsComponent, ValidationSignalErrorsComponent, ValidationErrorDirective, ValidationFallbackDirective, DefaultValidationErrorsDirective ], exports: [ ValidationErrorsComponent, ValidationSignalErrorsComponent, ValidationErrorDirective, ValidationFallbackDirective, DefaultValidationErrorsDirective ] }] }] }); /* * Public API Surface of ngx-valdemort */ /** * Generated bundle index. Do not edit. */ export { DefaultValidationErrorsDirective, DisplayMode, ValdemortConfig, ValdemortModule, ValidationErrorDirective, ValidationErrorsComponent, ValidationFallbackDirective, ValidationSignalErrorsComponent }; //# sourceMappingURL=ngx-valdemort.mjs.map