在 Angular Signal Forms 中以现代方式提交表单

发布: (2026年1月2日 GMT+8 16:00)
17 min read
原文: Dev.to

Source: Dev.to

用 Angular Signal Forms 以现代方式提交表单

在 Angular 16 中引入了 Signal Forms,它们提供了一种更具响应式的方式来管理表单状态。与传统的 FormGroup/FormControl API 相比,Signal Forms 让我们能够直接使用 signals(信号)来读取和写入表单值,从而简化代码并提升可读性。本文将展示如何使用 Signal Forms 实现表单提交,并对比传统做法的差异。


目录

  1. 为什么选择 Signal Forms?
  2. 创建一个 Signal Form
  3. 处理表单提交
  4. 完整示例代码
  5. 小结

为什么选择 Signal Forms?

  • 更直观的 API:使用 signal 来获取和设置值,而不必调用 getValue()setValue() 等方法。
  • 自动变更检测:Signal 本身会在值变化时触发更新,无需手动调用 markAsDirty()updateValueAndValidity() 等。
  • 更好的 TypeScript 支持:表单结构可以直接映射为 TypeScript 接口,IDE 能提供更精准的类型提示。

注意:Signal Forms 仍然兼容 Angular 的传统表单指令(如 ngModelformControlName),所以可以逐步迁移。


创建一个 Signal Form

首先,定义表单模型的接口:

export interface LoginForm {
  email: string;
  password: string;
}

然后,在组件中使用 signalForm 创建表单:

import { Component, signal } from '@angular/core';
import { signalForm } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent {
  // 使用 signalForm 创建一个强类型的表单
  loginForm = signalForm<LoginForm>({
    email: '',
    password: ''
  });

  // 读取表单值的 signal(只读)
  readonly formValue = this.loginForm.value;
}

在模板中,直接绑定 formControlName

<form (ngSubmit)="onSubmit()">
  <label>
    Email
    <input type="email" formControlName="email" />
  </label>

  <label>
    Password
    <input type="password" formControlName="password" />
  </label>

  <button type="submit" [disabled]="loginForm.invalid()">Login</button>
</form>

loginForm.invalid() 是 Signal Forms 为我们自动生成的 getter,用来检查表单整体的有效性。


处理表单提交

Signal Forms 让提交逻辑变得非常简洁。我们只需要读取 loginForm.value()(注意这里是函数调用,因为 value 本身是一个 signal),然后将数据发送到后端:

import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';

export class LoginComponent {
  private http = inject(HttpClient);

  // 省略前面的代码 ...

  async onSubmit() {
    // 读取当前表单值
    const payload = this.loginForm.value();

    // 发送请求(这里使用 async/await 只是示例,实际项目可自行决定)
    try {
      const response = await this.http.post('/api/login', payload).toPromise();
      console.log('登录成功', response);
    } catch (error) {
      console.error('登录失败', error);
    }
  }
}

与传统方式的对比

传统 FormGroupSignal Forms
this.form.get('email')?.valuethis.loginForm.value().email
this.form.validthis.loginForm.valid()
需要手动调用 markAsTouched()updateValueAndValidity()自动处理,无需额外调用
代码冗长,类型推断有限简洁、强类型,IDE 自动补全更友好

完整示例代码

下面给出一个可直接运行的完整示例(包括组件、模板和样式):

// login.component.ts
import { Component, signal } from '@angular/core';
import { signalForm } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';

export interface LoginForm {
  email: string;
  password: string;
}

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent {
  private http = inject(HttpClient);

  // 创建 Signal Form
  loginForm = signalForm<LoginForm>({
    email: '',
    password: ''
  });

  // 提交处理
  async onSubmit() {
    const payload = this.loginForm.value();

    try {
      const result = await this.http.post('/api/login', payload).toPromise();
      console.log('登录成功', result);
    } catch (err) {
      console.error('登录失败', err);
    }
  }
}
<!-- login.component.html -->
<form (ngSubmit)="onSubmit()">
  <div class="field">
    <label for="email">Email</label>
    <input id="email" type="email" formControlName="email" />
  </div>

  <div class="field">
    <label for="password">Password</label>
    <input id="password" type="password" formControlName="password" />
  </div>

  <button type="submit" [disabled]="loginForm.invalid()">登录</button>
</form>
/* login.component.scss */
.field {
  margin-bottom: 1rem;
  label {
    display: block;
    margin-bottom: 0.5rem;
  }
  input {
    width: 100%;
    padding: 0.5rem;
    box-sizing: border-box;
  }
}
button {
  padding: 0.75rem 1.5rem;
}

小结

  • Signal Forms 为 Angular 表单提供了更现代、更响应式的编程模型。
  • 通过 signalForm<T>() 可以一次性获得强类型的表单对象,省去繁琐的 FormControl 手动创建。
  • 表单状态(如 validinvaliddirty)均以 signal 形式暴露,直接在模板中使用函数调用即可。
  • 迁移成本低,兼容旧的指令和 API,适合作为新项目的默认表单实现。

如果你还在使用传统的 FormGroup,不妨尝试在一个小模块中引入 Signal Forms,感受一下代码简洁度和类型安全的提升。祝编码愉快!

Signal Forms 如何处理客户端验证

完全使用 Signal Forms API 构建的简易注册表单

<!-- Username field -->
<div>
  Username
</div>

@if (form.username().touched() && form.username().invalid()) {
  <ul>
    @for (err of form.username().errors(); track $index) {
      <li>- {{ err.message }}</li>
    }
  </ul>
}

<!-- Email field -->
<div>
  Email
</div>

@if (form.email().touched() && form.email().invalid()) {
  <ul>
    @for (err of form.email().errors(); track $index) {
      <li>- {{ err.message }}</li>
    }
  </ul>
}

<!-- Submit button -->
<button type="submit" [disabled]="form.invalid()">Create account</button>

初始状态 – “Create account” 按钮被禁用,因为表单无效(未输入用户名或电子邮件)。

客户端验证 – 当你聚焦一个字段并失去焦点时,验证错误会立即出现:

  • 用户名字段:必填 → “Please enter a username”
  • 电子邮件字段:必填 → “Please enter an email address”

输入有效值后,错误消失,提交按钮变为可用。到目前为止,客户端验证一切如预期工作。

问题:没有提交逻辑

点击 Create account 时,浏览器会刷新页面。没有异步处理,无法显示服务器端验证错误,也没有加载状态。

模板片段(没有提交处理器)

<form>

</form>

<form> 元素没有绑定 (ngSubmit)submit() 处理器,因此会执行默认的浏览器行为(页面重新加载)。

字段绑定

<input type="text" field="username" />
<input type="email" field="email" />

两个输入都使用 field 指令连接到 Signal Form。

条件错误渲染

@if (form.username().touched() && form.username().invalid()) {
  @for (err of form.username().errors(); track $index) {
    - {{ err.message }}
  }
}

电子邮件字段使用相同的模式。

禁用的提交按钮

<button type="submit" [disabled]="form.invalid()">Create account</button>

上述所有内容在客户端验证方面都运行良好;我们只需要添加提交逻辑。

组件 TypeScript

模型信号

protected readonly model = signal({
  username: '',
  email: '',
});

基于 Signal 的表单定义

protected readonly form = form(this.model, s => {
  required(s.username, { message: 'Please enter a username' });
  minLength(s.username, 3, { message: 'Your username must be at least 3 characters' });

  required(s.email, { message: 'Please enter an email address' });
});

所有这些都是客户端验证:快速、同步,并在任何提交尝试之前运行。

模拟注册服务(模拟后端调用)

import { Injectable } from '@angular/core';

export interface SignupModel {
  username: string;
  email: string;
}

export type SignupResult =
  | { status: 'ok' }
  | {
      status: 'error';
      fieldErrors: Partial<SignupModel>;
    };

@Injectable({ providedIn: 'root' })
export class SignupService {
  async signup(value: SignupModel): Promise<SignupResult> {
    await new Promise(r => setTimeout(r, 700));

    const fieldErrors: Partial<SignupModel> = {};

    // Username rules
    if (value.username.trim().toLowerCase() === 'brian') {
      fieldErrors.username = 'That username is already taken.';
    }

    // Email rules
    if (value.email.trim().toLowerCase() === 'brian@test.com') {
      fieldErrors.email = 'That email is already taken.';
    }

    return Object.keys(fieldErrors).length
      ? { status: 'error', fieldErrors }
      : { status: 'ok' };
  }
}

该服务返回成功结果,或返回一个包含字段特定服务器错误的对象。

  • 客户端验证 – 检查形状/格式(必填、电子邮件格式、最小长度)。
  • 服务器端验证 – 强制业务规则(保留的用户名、受限域名、唯一性)。

Source:

实现正确的表单提交

Signal Forms 提供了一个新的 submit() API,帮助我们处理许多繁琐的工作。

1. 注入注册服务

import { inject } from '@angular/core';
import { SignupService } from './signup.service';

export class SignupComponent {
  // …
  private readonly signupService = inject(SignupService);
}

2. 添加 onSubmit 方法

protected onSubmit(event: Event) {
  // 阻止浏览器默认的表单提交(页面刷新)
  event.preventDefault();

  // 使用新的 submit() API
  this.form.submit(async () => {
    // 将所有字段标记为已触摸,以便在需要时显示验证信息
    this.form.markAllAsTouched();

    // 如果在客户端检查后表单仍然无效,则中止
    if (this.form.invalid()) return;

    // 调用模拟服务
    const result = await this.signupService.signup(this.model());

    // 处理服务器端错误
    if (result.status === 'error') {
      // 将每个字段错误映射到对应的 Signal Form 字段
      for (const [field, message] of Object.entries(result.fieldErrors)) {
        // `field` 是 SignupModel 的键(例如 'username' 或 'email')
        // 强制转换为 any 以满足索引签名
        (this.form as any)[field].setServerError(message);
      }
      return;
    }

    // 成功路径 – 你可以导航、显示 toast 等
    console.log('Signup successful!');
  });
}

3. 在模板中绑定该方法

<form (ngSubmit)="onSubmit($event)">

  <button type="submit" [disabled]="form.submitting()">
    {{ form.submitting() ? 'Creating…' : 'Create account' }}
  </button>
</form>
  • form.submitting()submit() 提供的内置加载状态信号。
  • 当异步操作进行时,按钮文字会自动切换为加载状态。

submit() API 为你提供的功能

功能工作原理
异步处理接受一个异步回调;表单会保持在“提交中”状态,直到 Promise 解析完成。
加载状态form.submitting() 是一个信号,你可以绑定到 UI(例如,禁用按钮或显示加载指示器)。
已触摸字段处理markAllAsTouched() 确保在用户尝试提交时,未触摸字段也会显示验证信息。
服务器错误映射在字段信号上调用 setServerError(message) 会添加服务器端错误,并与现有错误 UI 集成。
阻止默认行为当通过 (ngSubmit) 使用时,API 会自动调用 event.preventDefault()

完整组件示例

import { Component, signal, inject } from '@angular/core';
import { form, required, minLength } from '@angular/forms';
import { SignupService, SignupModel } from './signup.service';

@Component({
  selector: 'app-signup',
  templateUrl: './signup.component.html',
  standalone: true,
})
export class SignupComponent {
  // Model signal
  protected readonly model = signal({ username: '', email: '' });

  // Signal‑based form
  protected readonly form = form(this.model, s => {
    required(s.username, { message: 'Please enter a username' });
    minLength(s.username, 3, { message: 'Your username must be at least 3 characters' });
    required(s.email, { message: 'Please enter an email address' });
  });

  // Service injection
  private readonly signupService = inject(SignupService);

  // Submit handler
  protected onSubmit(event: Event) {
    this.form.submit(async () => {
      this.form.markAllAsTouched();

      if (this.form.invalid()) return;

      const result = await this.signupService.signup(this.model());

      if (result.status === 'error') {
        for (const [field, message] of Object.entries(result.fieldErrors)) {
          (this.form as any)[field].setServerError(message);
        }
        return;
      }

      console.log('Signup successful!');
    });
  }
}

回顾

  1. 客户端验证 开箱即用,适用于 Signal Forms。
  2. 提交 需要使用新的 submit() API,以防止页面刷新、管理加载状态并处理服务器端错误。
  3. 实现步骤 – 注入服务,添加调用 form.submit(...)onSubmit 方法,将字段标记为已触摸,在客户端错误时中止,调用后端,映射服务器错误,并处理成功。

有了这些要素,您的 Angular Signal Forms 将实现完全响应式、用户友好,并准备好进行真实场景的服务器交互。祝编码愉快!

防止浏览器执行完整页面刷新

protected onSubmit(event: Event) {
  event.preventDefault();
}

使用新的 submit() 函数

import { ..., submit } from '@angular/forms/signals';

protected onSubmit(event: Event) {

  submit(this.form);
}

异步回调(第二个参数)

protected onSubmit(event: Event) {

  submit(this.form, async f => {
    // …
  });
}

Angular 为您做的事

  • 仅在表单有效时才调用回调。
  • 自动将所有字段标记为已触摸
  • 为您跟踪提交状态

在回调内部,您可以通过 f 获取表单的字段树。

访问当前表单值

submit(this.form, async f => {
  const value = f().value();   // validated client‑side value
});

调用注册服务

submit(this.form, async f => {

  const result = await this.signupService.signup(value);
});

这模拟了一个后端调用。

Source:

处理服务器端验证错误

如果服务器拒绝提交,返回验证错误 而不是抛出异常或手动设置状态。

submit(this.form, async f => {

  if (result.status === 'error') {
    // …
  }
});

创建错误数组

import { ..., ValidationError } from '@angular/forms/signals';

submit(this.form, async f => {

  if (result.status === 'error') {
    const errors: ValidationError.WithOptionalField[] = [];
  }
});

ValidationError.WithOptionalField 表示一种可以(可选)针对特定字段的验证错误。

为特定字段添加错误

submit(this.form, async f => {

  if (result.status === 'error') {
    if (result.fieldErrors.username) {
      errors.push({
        field: f.username,
        kind: 'server',
        message: result.fieldErrors.username,
      });
    }
  }
});

邮箱错误(相同模式)

submit(this.form, async f => {

  if (result.status === 'error') {
    if (result.fieldErrors.email) {
      errors.push({
        field: f.email,
        kind: 'server',
        message: result.fieldErrors.email,
      });
    }
  }
});

返回错误(或 undefined

submit(this.form, async f => {

  if (result.status === 'error') {

    return errors.length ? errors : undefined;
  }
});
  • 返回 errors 告诉 Angular:“不要提交表单;显示这些错误。”
  • 返回 undefined 告诉 Angular:“一切正常——表单已成功提交。”

在模板中绑定 onSubmit 方法

<form (ngSubmit)="onSubmit($event)">

</form>

在提交时禁用提交按钮

<button type="submit" [disabled]="form.submitting()">
  {{ form.submitting() ? 'Creating…' : 'Create account' }}
</button>

测试完整流程

  1. 客户端验证 在字段失去焦点时工作。
  2. 输入能够 通过客户端验证但未通过服务器验证 的值。
    • 客户端错误消失(表单在技术上是有效的)。
    • 按钮变为可用;点击它。
    • 按钮被禁用并显示 “Creating…”,期间模拟服务器响应。
    • 服务器验证错误以与客户端错误相同的 UI 显示。
    • 没有控制台日志出现,因为我们返回了错误,所以表单 提交。
  3. 修复用户名和电子邮件后,再次提交。
    • 表单现在成功提交,数据被记录/处理。

完整组件代码

import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
  Field,
  form,
  minLength,
  required,
  submit,
  ValidationError
} from '@angular/forms/signals';
import { SignupModel, SignupService } from './signup.service';

@Component({
  selector: 'app-form',
  imports: [CommonModule, Field],
  templateUrl: './form.component.html',
  styleUrl: './form.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormComponent {
  protected readonly model = signal({
    // … (model properties)
  });

  // … (rest of the component: form definition, onSubmit implementation, etc.)
}

上面的代码片段展示了完整的组件结构;请使用前面章节中展示的代码填充省略的部分(模型字段、表单定义、onSubmit 逻辑)。

组件 (TypeScript)

username: '',
email: '',
});

protected readonly form = form(this.model, s => {
  required(s.username, { message: 'Please enter a username' });
  minLength(s.username, 3, { message: 'Your username must be at least 3 characters' });
  required(s.email, { message: 'Please enter an email address' });
});

private readonly signupService = inject(SignupService);

protected onSubmit(event: Event) {
  event.preventDefault();

  submit(this.form, async f => {
    const value = f().value();
    const result = await this.signupService.signup(value);

    if (result.status === 'error') {
      const errors: ValidationError.WithOptionalField[] = [];

      if (result.fieldErrors.username) {
        errors.push({
          field: f.username,
          kind: 'server',
          message: result.fieldErrors.username,
        });
      }

      if (result.fieldErrors.email) {
        errors.push({
          field: f.email,
          kind: 'server',
          message: result.fieldErrors.email,
        });
      }

      return errors.length ? errors : undefined;
    }

    console.log('Submitted:', value);
    return undefined;
  });
}

模板 (HTML)

Back to Blog

相关文章

阅读更多 »

Angular 21 — 新功能与变更

Angular 21 专注于简化、性能和现代响应式模式。它不是添加花哨的 API,而是强化 Angular 开发者已经使用的……