For display validation messages of reactive form control i started to develop structural directive in Angular 18 with source code below
import { Directive, effect, input, InputSignal, TemplateRef, ViewContainerRef } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
@Directive({
selector: '[appValidationMessages]',
standalone: true
})
export class ValidationMessagesDirective {
constructor(
private templateReference: TemplateRef<{
$implicit: {
key: string;
message: string}
}>,
private viewContainerReference : ViewContainerRef) {
effect(() => {
const controlReference = this.appValidationMessagesOf().get(this.appValidationMessagesControlName());
if (!controlReference) {
throw new Error(`Control with name ${this.appValidationMessagesControlName} not found in form group`);
}
this.control = controlReference;
this.control.statusChanges.subscribe(() => {
this.doValidation();
});
});
}
public appValidationMessagesControlName : InputSignal<string> = input<string>('');
public appValidationMessagesOf : InputSignal<FormGroup> = input.required<FormGroup>();
private control: AbstractControl<any, any> ;
private doValidation() : void {
if(!this.control || !this.control.touched || !this.control.errors) {
this.viewContainerReference.clear();
return;
}
this.viewContainerReference.clear();
for(const key in this.control.errors) {
let error = 'This field is invalid';
switch(key) {
case 'required':
error = 'This field is required';
break;
case 'email':
error ='This field must be a valid email address';
break;
case 'minlength':
error = `This field must be at least ${this.control.errors['minlength'].requiredLength} characters long`;
break;
case 'emailIsTaken':
error = 'This email is already taken';
break;
}
const viewData = {$implicit : {key: key, message: error}};
this.viewContainerReference.createEmbeddedView(this.templateReference, viewData );
}
}
}
When i use it with ng-template, it works like expected.
<section>
<header>Validation messages complete</header>
<ng-template appValidationMessages let-valMessages [appValidationMessagesOf]="loginForm" appValidationMessagesControlName="email">
<p class="text-danger">{{valMessages.message}}</p>
</ng-template>
</section>
But, when i try to use it with * syntax
<section>
<header>Validation messages shorthand</header>
<p class="text-danger" *appValidationMessages="let valMessages of loginForm" appValidationMessagesControlName="email" >{{valMessages.message}}</p>
</section>
I receive a message
ERROR Error: Control with name function inputValueFn() {
producerAccessed(node);
if (node.value === REQUIRED_UNSET_VALUE) {
throw new RuntimeError(-950, ngDevMode && "Input is required but no value is available yet.");
}
return node.value;
} not found in form group
at EffectHandle.effectFn (validation-mesages.directive.ts:19:25)
at EffectHandle.runEffect (core.mjs:14336:18)
at Object.fn (core.mjs:14331:58)
at Object.run (signals.mjs:514:18)
at EffectHandle.run (core.mjs:14346:22)
at ZoneAwareEffectScheduler.flushQueue (core.mjs:14314:20)
at core.mjs:14304:41
at _ZoneDelegate.invoke (zone.js:368:26)
at Object.onInvoke (core.mjs:14832:33)
I suppose i missed a convention for shorthand template, but what exactly and how to fix it? Thank you.
The input appValidationMessagesControlName should be set, minus the directive name (appValidationMessages), so it will be controlName, in a key value syntax after the semi colon of the let valMessages of loginForm;.
<section>
<form [formGroup]="loginForm">
<input formControlName="email"/>
<p class="text-danger" *appValidationMessages="let valMessages of loginForm;controlName: 'email'" >{{valMessages.message}}</p>
</form>
<header>Validation messages complete</header>
</section>
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
Directive,
effect,
input,
InputSignal,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import {
AbstractControl,
FormGroup,
ReactiveFormsModule,
FormControl,
Validators,
} from '@angular/forms';
import { CommonModule } from '@angular/common';
@Directive({
selector: '[appValidationMessages]',
standalone: true,
})
export class ValidationMessagesDirective {
constructor(
private templateReference: TemplateRef<{
$implicit: {
key: string;
message: string;
};
}>,
private viewContainerReference: ViewContainerRef
) {
effect(() => {
const controlReference = this.appValidationMessagesOf().get(
this.appValidationMessagesControlName()
);
if (!controlReference) {
throw new Error(
`Control with name ${this.appValidationMessagesControlName} not found in form group`
);
}
this.control = controlReference;
this.control.statusChanges.subscribe(() => {
this.doValidation();
});
});
}
public appValidationMessagesControlName: InputSignal<string> =
input<string>('');
public appValidationMessagesOf: InputSignal<FormGroup> =
input.required<FormGroup>();
private control!: AbstractControl<any, any>;
private doValidation(): void {
if (!this.control || !this.control.touched || !this.control.errors) {
this.viewContainerReference.clear();
return;
}
this.viewContainerReference.clear();
for (const key in this.control.errors) {
let error = 'This field is invalid';
switch (key) {
case 'required':
error = 'This field is required';
break;
case 'email':
error = 'This field must be a valid email address';
break;
case 'minlength':
error = `This field must be at least ${this.control.errors['minlength'].requiredLength} characters long`;
break;
case 'emailIsTaken':
error = 'This email is already taken';
break;
}
const viewData = { $implicit: { key: key, message: error } };
this.viewContainerReference.createEmbeddedView(
this.templateReference,
viewData
);
}
}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, ValidationMessagesDirective],
template: `
<section>
<form [formGroup]="loginForm">
<input formControlName="email"/>
<p class="text-danger" *appValidationMessages="let valMessages of loginForm;controlName: 'email'" >{{valMessages.message}}</p>
</form>
<header>Validation messages complete</header>
</section>
`,
})
export class App {
loginForm = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
});
}
bootstrapApplication(App);