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);