+
diff --git a/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/demos/ngxerrors/form-accessor.component.ts b/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/demos/ngxerrors/form-accessor.component.ts
index ac4b07cc..99524e84 100644
--- a/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/demos/ngxerrors/form-accessor.component.ts
+++ b/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/demos/ngxerrors/form-accessor.component.ts
@@ -14,7 +14,11 @@ export class FormAccessorComponent extends FormAccessor {
initForm() {
return new FormGroup({
hello: new FormControl(null, [Validators.required]),
- world: new FormControl(null, [Validators.required, Validators.minLength(3)]),
+ world: new FormControl(null, [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.pattern(/^[A-Z].*/),
+ ]),
date: new FormControl(null, Validators.required),
});
}
diff --git a/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/demos/ngxerrors/ngxerrors.demo.component.ts b/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/demos/ngxerrors/ngxerrors.demo.component.ts
index 305fff4c..c2c0f064 100644
--- a/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/demos/ngxerrors/ngxerrors.demo.component.ts
+++ b/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/demos/ngxerrors/ngxerrors.demo.component.ts
@@ -15,13 +15,17 @@ import { FormAccessorContainer, NgxFormsErrorsConfigurationToken } from '@ngx/fo
{
provide: NgxFormsErrorsConfigurationToken,
useValue: {
+ // Global (default) error messages. Individual controls can override these.
errors: {
required: 'This field is required',
- minlength: 'Too short',
+ minlength: 'Value is too short (min 3 chars)',
+ pattern: 'Must start with an uppercase letter',
dependedDates: 'Something broke',
},
component: FormErrorComponent,
showWhen: 'touched',
+ // Showcase multiple errors rendering
+ show: 'all',
},
},
],
diff --git a/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/index.md b/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/index.md
index 11de3575..502aa29a 100644
--- a/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/index.md
+++ b/apps/docs/src/app/pages/docs/angular/forms/implementation/ngxerrors/index.md
@@ -8,6 +8,17 @@ Intended to be used in projects that require consistent error messages throughou
The error message is always rendered right below the element the `ngxErrors` directive is placed on.
+---
+
+## Why use `NgxFormsErrorsDirective`?
+
+- **Consistency** – Define your application's error messages once at a root level.
+- **Flexibility** – Render errors using the default DOM element or your own custom component.
+- **Context-aware** – Override default error messages for specific form controls without changing the global configuration.
+- **Integration-friendly** – Works with both _structural directive_ syntax (`*ngxFormsErrors`) and _attribute syntax_ (`[ngxFormsErrors]`).
+
+---
+
## Configuration
To implement the `ngxErrors` directive, we have to provide the necessary configuration on root level and import the `NgxFormsErrorsDirective` where used.
@@ -15,7 +26,7 @@ To implement the `ngxErrors` directive, we have to provide the necessary configu
A simple example is shown below.
```ts
- // Root
+ // Root module or standalone bootstrap provide
providers: [
{
provide: NgxFormsErrorsConfigurationToken,
@@ -24,7 +35,7 @@ A simple example is shown below.
required: 'This is a required field.',
email: 'This field is not a valid email address.'
},
- showWhen: 'touched',
+ showWhen: 'touched', // or 'dirty'
}
},
]
@@ -37,6 +48,13 @@ A simple example is shown below.
})
```
+- `errors`: A mapping between Angular validation error keys and your display messages.
+- `showWhen`: Determines when errors are displayed (`'touched'` or `'dirty'`).
+- `component` _(optional)_: Provide a custom component to render errors instead of the default `
` element.
+- `show` _(optional)_: Number of errors to display or `'all'` to show all.
+
+---
+
## Basic implementation
By default, only two properties are required when setting up the `NgxFormsErrorsDirective`.
@@ -47,6 +65,10 @@ The `showWhen` property will determine when an error message becomes visible. Yo
Once configured, all we need to do is attach the directive where we wish to render the error. We suggest attaching this directly to the input or your custom input component.
+You can attach the directive to an element in two ways:
+
+**Structural syntax** (renders the input inside the directive’s view):
+
```html
Hello
@@ -54,6 +76,17 @@ Once configured, all we need to do is attach the directive where we wish to rend
```
+**Attribute syntax** (applies directive directly to an existing element):
+
+```html
+
+```
+
+In both cases, `ngxFormsErrors` accepts either:
+
+- A **string** key that matches the control name in the parent `FormGroup`.
+- An **`AbstractControl`** instance directly.
+
The `ngxFormsErrors` directive allows for a string value that matches with the provided control in a `FormGroup`. Alternatively, you can also pass the `AbstractControl` directly.
By using this approach, when the control is invalid and in our case `touched`, the directive will render a `p` element with the `ngx-forms-error` class underneath the input.
@@ -89,12 +122,49 @@ The second Input is the `errorKeys` input, which provides us with an array of ke
On top of that, the `data` input provides us with the actual `ValidationErrors` on the control.
+The `customErrorMessages` input will contain the per-control overrides if they were provided via `ngxFormsErrorsCustomErrorMessages`.
+
+## Custom error messages per control
+
+In addition to global messages configured at root level, you can now override them **per control** using the `ngxFormsErrorsCustomErrorMessages` input.
+
+This is useful when a certain form field requires a different tone, extra context, or localized phrasing without affecting other fields.
+
+```html
+
+```
+
+If a key is provided in `ngxFormsErrorsCustomErrorMessages`, it will take priority over the global configuration for that control. Any keys not overridden will still fall back to the global `errors` record.
+
+This works for both the default `
` element output and custom components.
+
## Multiple errors
By default, the directive only renders a single error, the first one that gets provided in the validation errors object. If we wish to show more errors, we can provide the `show` property in the configuration.
We can either provide a specific number of errors we wish to see or provide the option `all` to see all errors.
+```ts
+{
+ provide: NgxFormsErrorsConfigurationToken,
+ useValue: {
+ errors,
+ showWhen: 'touched',
+ show: 'all' // or a number
+ }
+}
+```
+
+---
+
## Example
{{ NgDocActions.demo("NgxerrorsDemoComponent", { expanded: true }) }}
diff --git a/libs/angular/forms/src/lib/abstracts/error/error.component.abstract.ts b/libs/angular/forms/src/lib/abstracts/error/error.component.abstract.ts
index 765c3cc4..93dfdff8 100644
--- a/libs/angular/forms/src/lib/abstracts/error/error.component.abstract.ts
+++ b/libs/angular/forms/src/lib/abstracts/error/error.component.abstract.ts
@@ -15,4 +15,8 @@ export class NgxFormsErrorAbstractComponent {
* The error object provided by the control
*/
@Input({ required: true }) public data: ValidationErrors;
+ /**
+ * An object containing custom error messages
+ */
+ @Input() public customErrorMessages: Record;
}
diff --git a/libs/angular/forms/src/lib/directives/errors/errors.directive.spec.ts b/libs/angular/forms/src/lib/directives/errors/errors.directive.spec.ts
index 87f6d453..55f23d0e 100644
--- a/libs/angular/forms/src/lib/directives/errors/errors.directive.spec.ts
+++ b/libs/angular/forms/src/lib/directives/errors/errors.directive.spec.ts
@@ -58,10 +58,85 @@ export class FormErrorComponent extends NgxFormsErrorAbstractComponent {}
describe('NgxFormsErrorsDirective', () => {
const errors = {
- required: 'Dit veld is verplicht',
- email: 'Dit veld is geen e-mail',
- minlength: 'Dit veld moet minstens 3 lang zijn',
+ required: 'This field is required',
+ email: 'This field is not a valid email',
+ minlength: 'This field must be at least 3 characters long',
};
+
+ @Component({
+ selector: 'kp-attr-usage',
+ template: `
+
+
+ `,
+ imports: [ReactiveFormsModule, NgxFormsErrorsDirective],
+ })
+ class AttributeUsageComponent {
+ public control = new FormControl('', [Validators.email, Validators.minLength(10)]);
+ }
+
+ @Component({
+ selector: 'kp-multi-errors',
+ template: `
+
+
+
+
+ `,
+ imports: [ReactiveFormsModule, NgxFormsErrorsDirective],
+ })
+ class MultiErrorsComponent extends FormAccessor {
+ initForm() {
+ return new FormGroup({
+ multi: new FormControl('', [Validators.email, Validators.minLength(10)]),
+ });
+ }
+ }
+
+ @Component({
+ selector: 'kp-multi-errors-component',
+ template: `
+
+
+
+ `,
+ imports: [ReactiveFormsModule, NgxFormsErrorsDirective, FormErrorComponent],
+ })
+ class MultiErrorsWithComponent extends FormAccessor {
+ initForm() {
+ return new FormGroup({
+ multi: new FormControl('', [Validators.email, Validators.minLength(10)]),
+ });
+ }
+ }
+
+ @Component({
+ selector: 'kp-no-control',
+ template: `
+
+
+ `,
+ imports: [ReactiveFormsModule, NgxFormsErrorsDirective],
+ })
+ class NoControlProvidedComponent {
+ public dummy = new FormControl('');
+ }
+
+ @Component({
+ selector: 'kp-invalid-string',
+ template: `
+
+
+
+
+ `,
+ imports: [ReactiveFormsModule, NgxFormsErrorsDirective],
+ })
+ class InvalidControlStringComponent extends FormAccessor {
+ initForm() {
+ return new FormGroup({ exists: new FormControl('') });
+ }
+ }
describe('Without component', () => {
let fixture: ComponentFixture;
@@ -167,4 +242,176 @@ describe('NgxFormsErrorsDirective', () => {
expect(errorElements.length).toBe(0);
});
});
+
+ describe('Attribute usage with multiple errors (show variations)', () => {
+ const multiErrors = {
+ email: 'Email invalid',
+ minlength: 'Minimum length not reached',
+ };
+
+ describe('show = default (1)', () => {
+ let fixture: ComponentFixture;
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ReactiveFormsModule, MultiErrorsComponent],
+ providers: [
+ {
+ provide: NgxFormsErrorsConfigurationToken,
+ useValue: { showWhen: 'dirty', errors: multiErrors },
+ },
+ ],
+ });
+ fixture = TestBed.createComponent(MultiErrorsComponent);
+ fixture.detectChanges();
+ });
+
+ it('should show only the first error when multiple are present by default', () => {
+ const control = fixture.componentRef.instance.form.get('multi');
+ control.setValue('abc'); // triggers email + minlength
+ control.markAsDirty();
+ control.updateValueAndValidity();
+ fixture.detectChanges();
+ const error = fixture.nativeElement.querySelector('.ngx-forms-error');
+ expect(error.textContent).toBe(multiErrors.email); // first validator supplied
+ });
+ });
+
+ describe('show = 2', () => {
+ let fixture: ComponentFixture;
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ReactiveFormsModule, MultiErrorsComponent],
+ providers: [
+ {
+ provide: NgxFormsErrorsConfigurationToken,
+ useValue: { showWhen: 'dirty', show: 2, errors: multiErrors },
+ },
+ ],
+ });
+ fixture = TestBed.createComponent(MultiErrorsComponent);
+ fixture.detectChanges();
+ });
+
+ it('should show both errors when show = 2', () => {
+ const control = fixture.componentRef.instance.form.get('multi');
+ control.setValue('abc');
+ control.markAsDirty();
+ control.updateValueAndValidity();
+ fixture.detectChanges();
+ const error = fixture.nativeElement.querySelector('.ngx-forms-error');
+ expect(error.textContent).toBe(`${multiErrors.email}, ${multiErrors.minlength}`);
+ });
+ });
+
+ describe("show = 'all'", () => {
+ let fixture: ComponentFixture;
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ReactiveFormsModule, MultiErrorsComponent],
+ providers: [
+ {
+ provide: NgxFormsErrorsConfigurationToken,
+ useValue: { showWhen: 'dirty', show: 'all', errors: multiErrors },
+ },
+ ],
+ });
+ fixture = TestBed.createComponent(MultiErrorsComponent);
+ fixture.detectChanges();
+ });
+
+ it('should show all errors when show = all', () => {
+ const control = fixture.componentRef.instance.form.get('multi');
+ control.setValue('abc');
+ control.markAsDirty();
+ control.updateValueAndValidity();
+ fixture.detectChanges();
+ const error = fixture.nativeElement.querySelector('.ngx-forms-error');
+ expect(error.textContent).toBe(`${multiErrors.email}, ${multiErrors.minlength}`);
+ });
+ });
+ });
+
+ describe('Custom error messages override', () => {
+ const baseErrors = {
+ email: 'Base email',
+ minlength: 'Base minlength',
+ };
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ReactiveFormsModule, MultiErrorsWithComponent, FormErrorComponent],
+ providers: [
+ {
+ provide: NgxFormsErrorsConfigurationToken,
+ useValue: {
+ showWhen: 'dirty',
+ show: 'all',
+ errors: baseErrors,
+ component: FormErrorComponent,
+ },
+ },
+ ],
+ });
+ fixture = TestBed.createComponent(MultiErrorsWithComponent);
+ // Provide custom overrides dynamically
+ const inputEl: HTMLInputElement = fixture.nativeElement.querySelector('input');
+ // Patch the directive instance to set customErrorMessages input
+ const directiveInstance: any = (fixture.debugElement.childNodes as any[])
+ .map((n) => n.injector?.get?.(NgxFormsErrorsDirective, null))
+ .filter(Boolean)[0];
+ if (directiveInstance) {
+ directiveInstance.customErrorMessages = { email: 'Custom email override' };
+ }
+ fixture.detectChanges();
+ });
+
+ it('should use custom message overrides when provided (component flow)', () => {
+ const control = fixture.componentRef.instance.form.get('multi');
+ control.setValue('abc');
+ control.markAsDirty();
+ control.updateValueAndValidity();
+ fixture.detectChanges();
+ const errorCmp = fixture.nativeElement.querySelector('.kp-error');
+ expect(errorCmp.textContent).toContain('Custom email override');
+ });
+ });
+
+ describe('Early exit & error logging scenarios', () => {
+ it('logs an error when no control input is provided', () => {
+ spyOn(console, 'error');
+ TestBed.configureTestingModule({
+ imports: [ReactiveFormsModule, NoControlProvidedComponent],
+ providers: [
+ {
+ provide: NgxFormsErrorsConfigurationToken,
+ useValue: { showWhen: 'dirty', errors },
+ },
+ ],
+ });
+ const fixture = TestBed.createComponent(NoControlProvidedComponent);
+ fixture.detectChanges();
+ expect(console.error).toHaveBeenCalled();
+ const errEl = fixture.nativeElement.querySelector('.ngx-forms-error');
+ expect(errEl).toBeNull();
+ });
+
+ it('logs an error when provided control string does not resolve', () => {
+ spyOn(console, 'error');
+ TestBed.configureTestingModule({
+ imports: [ReactiveFormsModule, InvalidControlStringComponent],
+ providers: [
+ {
+ provide: NgxFormsErrorsConfigurationToken,
+ useValue: { showWhen: 'dirty', errors },
+ },
+ ],
+ });
+ const fixture = TestBed.createComponent(InvalidControlStringComponent);
+ fixture.detectChanges();
+ expect(console.error).toHaveBeenCalled();
+ const errEl = fixture.nativeElement.querySelector('.ngx-forms-error');
+ expect(errEl).toBeNull();
+ });
+ });
});
diff --git a/libs/angular/forms/src/lib/directives/errors/errors.directive.ts b/libs/angular/forms/src/lib/directives/errors/errors.directive.ts
index 382f5d9a..f7a39f10 100644
--- a/libs/angular/forms/src/lib/directives/errors/errors.directive.ts
+++ b/libs/angular/forms/src/lib/directives/errors/errors.directive.ts
@@ -19,7 +19,7 @@ import {
ValidationErrors,
} from '@angular/forms';
-import { Observable, Subject, combineLatest, startWith, takeUntil, tap } from 'rxjs';
+import { Subject, combineLatest, startWith, takeUntil, tap } from 'rxjs';
import { NgxFormsErrorsConfigurationToken } from '../../tokens';
import { NgxFormsErrorConfigurationOptions } from '../../interfaces';
import { NgxFormsErrorAbstractComponent } from '../../abstracts';
@@ -32,7 +32,7 @@ import { touchedEventListener } from '../../utils';
export class NgxFormsErrorsDirective implements AfterViewInit, OnDestroy {
// Iben: Handle the OnDestroy flow
private readonly onDestroySubject$ = new Subject();
- private readonly onDestroy$ = new Observable();
+ private readonly onDestroy$ = this.onDestroySubject$.asObservable();
/**
* The actual template of the input element
@@ -59,25 +59,39 @@ export class NgxFormsErrorsDirective implements AfterViewInit, OnDestroy {
*/
private componentRef: ComponentRef;
+ /**
+ * Custom error messages to override default ones
+ */
+ private customMessages: Record;
+
/**
* A reference to a control or a string reference to the control
*/
@Input('ngxFormsErrors') public control: AbstractControl | string;
+ /**
+ * Custom error messages to override default ones
+ */
+ @Input('ngxFormsErrorsCustomErrorMessages')
+ public set customErrorMessages(value: Record) {
+ this.customMessages = value ?? {};
+ }
constructor(
@Optional() private readonly formGroupDirective: FormGroupDirective,
@Optional() private readonly formNameDirective: FormGroupName,
+ @Optional() private readonly templateRef: TemplateRef,
@Optional()
@Inject(NgxFormsErrorsConfigurationToken)
private readonly config: NgxFormsErrorConfigurationOptions,
private readonly viewContainer: ViewContainerRef,
private readonly elementRef: ElementRef,
private readonly renderer: Renderer2,
- private readonly templateRef: TemplateRef,
private readonly cdRef: ChangeDetectorRef
) {
// Iben: Set the current template ref at constructor time so we actually have the provided template (as done in the *ngIf directive)
- this.template = this.templateRef;
+ if (this.templateRef) {
+ this.template = this.templateRef;
+ }
}
public ngOnDestroy(): void {
@@ -89,7 +103,11 @@ export class NgxFormsErrorsDirective implements AfterViewInit, OnDestroy {
public ngAfterViewInit(): void {
// Iben: Render the actual input so that it is always visible
this.viewContainer.clear();
- this.viewContainer.createEmbeddedView(this.template);
+
+ // Abdurrahman: Only render template if this directive is used in structural form
+ if (this.template) {
+ this.viewContainer.createEmbeddedView(this.template);
+ }
// Iben: If no control was provided, we early exit and log an error
if (!this.control) {
@@ -173,11 +191,17 @@ export class NgxFormsErrorsDirective implements AfterViewInit, OnDestroy {
this.errorComponent = this.componentRef.instance;
// Iben: Set the data of the error component
- const { errors, errorKeys, data } = this.getErrors(this.abstractControl.errors);
+ const { errorKeys, data } = this.getErrors(this.abstractControl.errors);
+
+ // Abdurrahman: Merge defaults with custom overrides if provided
+ const errors = errorKeys.map(
+ (key) => this.customMessages?.[key] || this.config.errors[key]
+ );
this.errorComponent.errors = errors;
this.errorComponent.errorKeys = errorKeys;
this.errorComponent.data = data;
+ this.errorComponent.customErrorMessages = this.customMessages;
}
/**
@@ -203,12 +227,15 @@ export class NgxFormsErrorsDirective implements AfterViewInit, OnDestroy {
this.renderer.setAttribute(this.errorsElement, 'class', 'ngx-forms-error');
// Iben: Set the errors based on the keys
- this.renderer.setProperty(
- this.errorsElement,
- 'innerHTML',
- this.getErrors(this.abstractControl.errors).errors.join(', ')
+ const { errorKeys } = this.getErrors(this.abstractControl.errors);
+
+ // Abdurrahman: Merge defaults with custom overrides if provided
+ const errors = errorKeys.map(
+ (key) => this.customMessages?.[key] || this.config.errors[key]
);
+ this.renderer.setProperty(this.errorsElement, 'textContent', errors.join(', '));
+
// Iben: insert the paragraph underneath the input component
this.renderer.insertBefore(
this.elementRef.nativeElement.parentNode,