如何在Angular Signals表单和Angular Material中显示服务器端错误?
我们想在结合Angular Material的客户端Angular信号表单中展示服务器端验证错误。
We have installed the following packages (package.json):
{
[...]
"dependencies": {
[...]
"@angular/core": "^21.1.0",
"@angular/forms": "^21.1.0",
[...]
},
"devDependencies": {
[...]
"@angular/material": "^21.1.3",
[...]
}
}
We have a simple model which describes a ticket and should be bound to the template:
interface Ticket {
id: number,
title: string,
repro: string
}
The server-side errors will be parsed into the following structure:
export interface IServerValidationResponse {
errors: IServerValidationError[];
}
export interface IServerValidationError {
propertyName: string;
errorMessage: string;
}
Our Angular component looks like this:
import { Component, computed, effect, signal } from '@angular/core';
import { IServerValidationResponse } from '../../../core/server-validation-response';
import { form, FormField, required, submit } from '@angular/forms/signals';
import { MatFormField, MatHint, MatLabel } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
@Component({
selector: 'app-ticket-create-serverside-validation',
imports: [MatFormField, MatLabel, MatInput, FormField],
templateUrl: './ticket-create-serverside-validation.html',
styleUrl: './ticket-create-serverside-validation.css',
})
export class TicketCreateServersideValidation {
// simulated server response with validation errors
errorResponse = signal<IServerValidationResponse>({
"errors": [
{
"propertyName": "Title",
"errorMessage": "Please enter a valid ticket title!"
},
{
"propertyName": "Repro",
"errorMessage": "Please enter a valid ticket repro!"
}
]
});
ticketModel = signal<Ticket>({
id: 0,
title: '',
repro: ''
});
ticketForm = form(this.ticketModel);
fieldErrors = effect(() => {
const errors = this.errorResponse().errors;
// match server validation errors to form fields and set errors on the form fields here...
});
}
This is our template (HTML):
<form>
<mat-form-field appearance="outline">
<mat-label>Ticket</mat-label>
<input matInput [formField]="ticketForm.title">
</mat-form-field>
<br/>
<mat-form-field appearance="outline">
<mat-label>Repro</mat-label>
<input matInput [formField]="ticketForm.repro">
</mat-form-field>
</form>
How can we pass the server side errors (in this example simulated by errorResponse) to the form so that the respective fields will be colored red?
解决方案
We can use the validateTree method from @angular/forms/signals to validate the entire form tree and load the errors into the form.
Below are the resources to refer on:
According to the docs the validateTree does the following:
The validateTree() function creates custom validation rules that can target multiple fields or provide complex validation logic for a whole subtree.
Which is exactly matching the use case of your problem.
ticketForm = form(this.ticketModel, (schemaPath) => {
validateTree<Ticket>(schemaPath, ({ value, fieldTreeOf }) => {
const errors: any[] = [];
const fieldsToCheck: Array<keyof Ticket> = ['title', 'repro'];
const errorResponseData = this.errorResponse();
// server errors check
fieldsToCheck.forEach((key: keyof Ticket) => {
const foundError = errorResponseData.errors.find(
(x) => x.propertyName.toLowerCase() === key.toLowerCase()
);
if (foundError) {
errors.push({
kind: `${key.toLowerCase()}-server-error`,
message: foundError.errorMessage,
fieldTree: fieldTreeOf(schemaPath)[key], // Targets a specific field
});
}
});
// Return the array of errors, or null if empty
return errors.length > 0 ? errors : null;
});
});
So we implement the validateTree where we loop through the fields. We initialize an errors array to store the errors.
Then we loop through the fields, and check if the errors are present inside errorResponse.errors, if present add the error.
The most important line is below, where we must ensure the fieldTree property is specified where we use the method fieldTreeOf to set the field where the error message should be shown.
errors.push({
kind: `${key.toLowerCase()}-server-error`,
message: foundError.errorMessage,
fieldTree: fieldTreeOf(schemaPath)[key], // Targets a specific field
});
Full Code:
TS:
import {
ChangeDetectionStrategy,
Component,
signal,
effect,
} from '@angular/core';
import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormField, form, validateTree } from '@angular/forms/signals';
interface Ticket {
id: number;
title: string;
repro: string;
}
// The server-side errors will be parsed into the following structure:
export interface IServerValidationResponse {
errors: IServerValidationError[];
}
export interface IServerValidationError {
propertyName: string;
errorMessage: string;
}
/** @title Simple form field */
@Component({
selector: 'form-field-overview-example',
templateUrl: 'form-field-overview-example.html',
styleUrl: 'form-field-overview-example.css',
imports: [MatFormFieldModule, MatInputModule, MatSelectModule, FormField],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormFieldOverviewExample {
// simulated server response with validation errors
errorResponse = signal<IServerValidationResponse>({
errors: [
{
propertyName: 'Title',
errorMessage: 'Please enter a valid ticket title!',
},
{
propertyName: 'Repro',
errorMessage: 'Please enter a valid ticket repro!',
},
],
});
ticketModel = signal<Ticket>({
id: 0,
title: '',
repro: '',
});
ticketForm = form(this.ticketModel, (schemaPath) => {
validateTree<Ticket>(schemaPath, ({ value, fieldTreeOf }) => {
const errors: any[] = [];
const errorResponseData = this.errorResponse();
const fieldsToCheck: Array<keyof Ticket> = ['title', 'repro'];
// server errors check
fieldsToCheck.forEach((key: keyof Ticket) => {
const foundError = errorResponseData.errors.find(
(x) => x.propertyName.toLowerCase() === key.toLowerCase()
);
if (foundError) {
errors.push({
kind: `${key.toLowerCase()}-server-error`,
message: foundError.errorMessage,
fieldTree: fieldTreeOf(schemaPath)[key], // Targets a specific field
});
}
});
// Return the array of errors, or null if empty
return errors.length > 0 ? errors : null;
});
});
fieldErrors = effect(() => {
const errors = this.errorResponse().errors;
// match server validation errors to form fields and set errors on the form fields here...
});
onSubmit(event: any) {}
}
HTML:
<form (submit)="onSubmit($event)" novalidate>
<mat-form-field>
<mat-label>Ticket</mat-label>
<input matInput [formField]="ticketForm.title" />
@for (error of ticketForm.title().errors(); track error) {
<mat-error>{{error.message}}</mat-error>
}
</mat-form-field>
<br />
<mat-form-field>
<mat-label>Repro</mat-label>
<input matInput [formField]="ticketForm.repro" />
@for (error of ticketForm.repro().errors(); track error) {
<mat-error>{{error.message}}</mat-error>
}
</mat-form-field>
<br />
<br />
<button type="submit">Log In</button>
</form>