PAYATHON 2026

Angular Signal Forms 如何在 Material 中显示服务端错误?

数据小许

可以用 validate() 将 errorResponse 中的错误接入 Signal Forms,再用 Angular Material 的 ErrorStateMatcher 控制红色错误状态,并在 <mat-error> 中显示错误消息。

题目中的 errorResponse 是一个独立的 signal。声明式校验可以直接响应服务端错误的变化,不必在 effect() 中逐个写入字段错误。

将服务端错误注册为字段校验结果

validate() 接收字段的 schema 路径和校验函数。让校验函数读取 errorResponse(),再将错误转换为带有 kind 和 message 的对象。该 API 用于声明字段的校验逻辑,详见 Angular v21 的 validate API。

保留现有的 errorResponse、ticketModel 和接口定义,删除原来的 fieldErrors = effect(...),将表单初始化改为:

import { form, FormField, validate } from '@angular/forms/signals';

// 以下成员放在原组件类中,位于 errorResponse、ticketModel 之后。
private serverErrors(propertyName: string) {
  return this.errorResponse().errors
    .filter(error => error.propertyName === propertyName)
    .map(error => ({
      kind: 'server',
      message: error.errorMessage,
    }));
}

ticketForm = form(this.ticketModel, path => {
  validate(path.title, () => this.serverErrors('Title'));
  validate(path.repro, () => this.serverErrors('Repro'));
});

代码中显式处理了字段名映射:服务端使用 Title、Repro,客户端模型使用 title、repro。有多个同名字段错误时,返回的数组会保留全部消息;没有匹配错误时返回空数组。

接入后,可以通过以下状态检查映射结果:

this.ticketForm.title().errors();
this.ticketForm.title().invalid();

this.ticketForm.repro().errors();
this.ticketForm.repro().invalid();

让 Material 根据字段状态显示红框

字段无效后,界面不一定马上显示红框。Angular Material 使用 ErrorStateMatcher 决定是否进入错误状态,默认显示策略还与用户交互或表单提交有关。可以通过 errorStateMatcher 自定义显示条件,参见 Material Input 的错误显示说明。

题目中的错误在初始化时已存在。如果需要立即显示,可以让每个 matcher 直接读取对应字段的 invalid():

import { ErrorStateMatcher } from '@angular/material/core';

// 放在组件类中,位于 ticketForm 初始化之后。
readonly titleErrorMatcher: ErrorStateMatcher = {
  isErrorState: () => this.ticketForm.title().invalid(),
};

readonly reproErrorMatcher: ErrorStateMatcher = {
  isErrorState: () => this.ticketForm.repro().invalid(),
};

这里的 isErrorState 读取闭包中的字段状态,不依赖 matcher 接收的传统 FormControl 参数。

题目使用的是 21.1 系列。为避免显示刷新依赖具体版本的表单互操作实现,可以增加一个小指令,显式调用 MatInput.updateErrorState(),重新计算输入框的错误状态。参见 MatInput 源码。

import { Directive, DoCheck, inject } from '@angular/core';
import { MatInput } from '@angular/material/input';

@Directive({
  selector: 'input[matInput][refreshSignalErrorState]',
  standalone: true,
})
export class RefreshSignalErrorState implements DoCheck {
  private readonly matInput = inject(MatInput, { self: true });

  ngDoCheck(): void {
    this.matInput.updateErrorState();
  }
}

这个指令负责刷新显示状态,校验结果仍由 validate() 提供。如果实际安装的版本已经能自动刷新 matcher,可以省略该指令。

在模板中显示错误消息

组件需要导入 MatError 和上面定义的兼容指令:

import {
  MatError,
  MatFormField,
  MatLabel,
} from '@angular/material/form-field';

// 更新原 @Component 装饰器的 imports:
imports: [
  MatFormField,
  MatLabel,
  MatError,
  MatInput,
  FormField,
  RefreshSignalErrorState,
],

将模板改为:

<form novalidate>
  <mat-form-field appearance="outline">
    <mat-label>Ticket</mat-label>
    <input
      matInput
      refreshSignalErrorState
      [formField]="ticketForm.title"
      [errorStateMatcher]="titleErrorMatcher"
    >

    @for (error of ticketForm.title().errors(); track $index) {
      <mat-error>{{ error.message }}</mat-error>
    }
  </mat-form-field>

  <br/>

  <mat-form-field appearance="outline">
    <mat-label>Repro</mat-label>
    <input
      matInput
      refreshSignalErrorState
      [formField]="ticketForm.repro"
      [errorStateMatcher]="reproErrorMatcher"
    >

    @for (error of ticketForm.repro().errors(); track $index) {
      <mat-error>{{ error.message }}</mat-error>
    }
  </mat-form-field>
</form>

[formField] 绑定字段,matcher 决定 Material 是否进入错误状态,<mat-error> 展示消息。只添加错误文本不会自动创建字段校验结果。

明确服务端错误何时清除

上述校验器依赖 errorResponse()。用户修改输入值后,旧的服务端错误仍然存在,直到应用更新或清除相应错误。

如果产品要求用户编辑某个字段时清除该字段的服务端错误,可以在组件中加入:

clearServerErrors(propertyName: string): void {
  this.errorResponse.update(response => ({
    ...response,
    errors: response.errors.filter(
      error => error.propertyName !== propertyName,
    ),
  }));
}

然后分别在对应输入框上增加:

(input)="clearServerErrors('Title')"
(input)="clearServerErrors('Repro')"

清除错误只表示旧错误不再用于当前输入,最终结果仍需在下次请求时由服务端校验。真实请求返回后,通过 this.errorResponse.set(response) 更新错误即可。

对于无法映射到字段的错误,应在表单级区域显示,不要直接丢弃,也不要通过类型断言把任意服务端属性名当作表单字段。

^21.1.0 和 ^21.1.3 允许安装一定范围内的更新,不能确定实际安装版本。Angular v21 文档仍将 Signal Forms 标记为实验性 API。排查兼容问题时,应以 lockfile 和实际安装包为准,不能直接假定最新文档中的接口都适用于 21.1。参见 Angular v21 的 FormField API。

备注:内容仅供参考。