如何在Angular Signals表单和Angular Material中显示服务器端错误?

前端开发 2026-07-11

我们想在结合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:

验证与状态

Using validateTree()

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>

Stackblitz Demo

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章