在 Angular 的世界中,无论您是在制作简单的登录页面还是更复杂的用户配置文件界面,表单对于用户交互都是至关重要的。 Angular 传统上提供两种主要方法:模板驱动表单和反应式表单。在我之前的 Angular 反应式表单系列中,我探索了如何利用反应式表单的强大功能来管理复杂逻辑、创建动态表单以及构建自定义表单控件。
用于管理反应性的新工具 - 信号 - 已在 Angular 版本 16 中引入,此后一直是 Angular 维护人员关注的焦点,并在版本 17 中变得稳定。信号允许您处理状态更改声明性地,提供了一个令人兴奋的替代方案,将模板驱动表单的简单性与反应表单的强大反应性结合起来。本文将研究信号如何为 Angular 中的简单和复杂形式添加反应性。
在深入探讨使用信号增强模板驱动表单的主题之前,让我们快速回顾一下 Angular 的传统表单方法:
模板驱动表单:使用 ngModel 等指令直接在 HTML 模板中定义,这些表单易于设置,非常适合简单表单。但是,它们可能无法提供更复杂场景所需的细粒度控制。
这是模板驱动表单的最小示例:
<form (ngSubmit)="onSubmit()"> <label for="name">Name:</label> <input> </li> </ol> <pre class="brush:php;toolbar:false">```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent { name = ''; onSubmit() { console.log(this.name); } } ```
反应式表单:使用 Angular 的 FormGroup、FormControl 和 FormArray 类在组件类中以编程方式进行管理;反应式表单提供对表单状态和验证的精细控制。正如我之前关于 Angular Reactive Forms 的文章所讨论的那样,这种方法非常适合复杂的表单。
这是一个反应式形式的最小示例:
import { Component } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent { form = new FormGroup({ name: new FormControl('') }); onSubmit() { console.log(this.form.value); } }
```html <form [formGroup]="form" (ngSubmit)="onSubmit()"> <label for="name">Name:</label> <input> <h2> Introducing Signals as a New Way to Handle Form Reactivity </h2> <p>With the release of Angular 16, signals have emerged as a new way to manage reactivity. Signals provide a declarative approach to state management, making your code more predictable and easier to understand. When applied to forms, signals can enhance the simplicity of template-driven forms while offering the reactivity and control typically associated with reactive forms.</p> <p>Let’s explore how signals can be used in both simple and complex form scenarios.</p> <h3> Example 1: A Simple Template-Driven Form with Signals </h3> <p>Consider a basic login form. Typically, this would be implemented using template-driven forms like this:<br> </p> <pre class="brush:php;toolbar:false"><!-- login.component.html --> <form name="form" (ngSubmit)="onSubmit()"> <label for="email">E-mail</label> <input type="email"> <pre class="brush:php;toolbar:false">// login.component.ts import { Component } from "@angular/core"; @Component({ selector: "app-login", templateUrl: "./login.component.html", }) export class LoginComponent { public email: string = ""; public password: string = ""; onSubmit() { console.log("Form submitted", { email: this.email, password: this.password }); } }
这种方法适用于简单的表单,但是通过引入信号,我们可以在添加反应功能的同时保持简单性:
// login.component.ts import { Component, computed, signal } from "@angular/core"; import { FormsModule } from "@angular/forms"; @Component({ selector: "app-login", standalone: true, templateUrl: "./login.component.html", imports: [FormsModule], }) export class LoginComponent { // Define signals for form fields public email = signal(""); public password = signal(""); // Define a computed signal for the form value public formValue = computed(() => { return { email: this.email(), password: this.password(), }; }); public isFormValid = computed(() => { return this.email().length > 0 && this.password().length > 0; }); onSubmit() { console.log("Form submitted", this.formValue()); } }
<!-- login.component.html --> <form name="form" (ngSubmit)="onSubmit()"> <label for="email">E-mail</label> <input type="email"> <p>In this example, the form fields are defined as signals, allowing for reactive updates whenever the form state changes. The formValue signal provides a computed value that reflects the current state of the form. This approach offers a more declarative way to manage form state and reactivity, combining the simplicity of template-driven forms with the power of signals.</p> <p>You may be tempted to define the form directly as an object inside a signal. While such an approach may seem more concise, typing into the individual fields does not dispatch reactivity updates, which is usually a deal breaker. Here’s an example StackBlitz with a component suffering from such an issue:</p> <p>Therefore, if you'd like to react to changes in the form fields, it's better to define each field as a separate signal. By defining each form field as a separate signal, you ensure that changes to individual fields trigger reactivity updates correctly. </p> <h3> Example 2: A Complex Form with Signals </h3> <p>You may see little benefit in using signals for simple forms like the login form above, but they truly shine when handling more complex forms. Let's explore a more intricate scenario - a user profile form that includes fields like firstName, lastName, email, phoneNumbers, and address. The phoneNumbers field is dynamic, allowing users to add or remove phone numbers as needed.</p> <p>Here's how this form might be defined using signals:<br> </p> <pre class="brush:php;toolbar:false">// user-profile.component.ts import { JsonPipe } from "@angular/common"; import { Component, computed, signal } from "@angular/core"; import { FormsModule, Validators } from "@angular/forms"; @Component({ standalone: true, selector: "app-user-profile", templateUrl: "./user-profile.component.html", styleUrls: ["./user-profile.component.scss"], imports: [FormsModule, JsonPipe], }) export class UserProfileComponent { public firstName = signal(""); public lastName = signal(""); public email = signal(""); // We need to use a signal for the phone numbers, so we get reactivity when typing in the input fields public phoneNumbers = signal([signal("")]); public street = signal(""); public city = signal(""); public state = signal(""); public zip = signal(""); public formValue = computed(() => { return { firstName: this.firstName(), lastName: this.lastName(), email: this.email(), // We need to do a little mapping here, so we get the actual value for the phone numbers phoneNumbers: this.phoneNumbers().map((phoneNumber) => phoneNumber()), address: { street: this.street(), city: this.city(), state: this.state(), zip: this.zip(), }, }; }); public formValid = computed(() => { const { firstName, lastName, email, phoneNumbers, address } = this.formValue(); // Regex taken from the Angular email validator const EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; const isEmailFormatValid = EMAIL_REGEXP.test(email); return ( firstName.length > 0 && lastName.length > 0 && email.length > 0 && isEmailFormatValid && phoneNumbers.length > 0 && // Check if all phone numbers are valid phoneNumbers.every((phoneNumber) => phoneNumber.length > 0) && address.street.length > 0 && address.city.length > 0 && address.state.length > 0 && address.zip.length > 0 ); }); addPhoneNumber() { this.phoneNumbers.update((phoneNumbers) => { phoneNumbers.push(signal("")); return [...phoneNumbers]; }); } removePhoneNumber(index: number) { this.phoneNumbers.update((phoneNumbers) => { phoneNumbers.splice(index, 1); return [...phoneNumbers]; }); } }
请注意,phoneNumbers 字段被定义为信号数组中的一个信号。这种结构使我们能够跟踪各个电话号码的更改并反应性地更新表单状态。 addPhoneNumber 和removePhoneNumber 方法更新phoneNumbers 信号数组,触发表单中的反应性更新。
<!-- user-profile.component.html --> <blockquote> <p>在模板中,我们使用phoneNumbers信号数组来动态渲染电话号码输入字段。 addPhoneNumber 和removePhoneNumber 方法允许用户反应性地添加或删除电话号码,从而更新表单状态。请注意 track 函数的用法,这是确保 ngFor 指令正确跟踪phoneNumbers 数组更改所必需的。</p> </blockquote> <p>这是复杂表单示例的 StackBlitz 演示,供您试用:</p> <h3> 使用信号验证表单 </h3> <p>验证对于任何表单都至关重要,确保用户输入在提交之前符合所需的标准。使用信号,可以以反应性和声明性的方式处理验证。在上面的复杂表单示例中,我们实现了一个名为 formValid 的计算信号,它检查所有字段是否满足特定的验证标准。</p> <p>可以轻松自定义验证逻辑以适应不同的规则,例如检查有效的电子邮件格式或确保填写所有必填字段。使用信号进行验证可以让您创建更多可维护和可测试的代码,因为验证规则被明确定义并自动对表单字段中的更改做出反应。它甚至可以被抽象为一个单独的实用程序,以使其可以在不同形式中重用。</p> <p>在复杂表单示例中,formValid 信号可确保填写所有必填字段并验证电子邮件和电话号码格式。</p> <p>这种验证方法有点简单,需要更好地连接到实际的表单字段。虽然它适用于许多用例,但在某些情况下,您可能需要等到 Angular 中添加显式“信号形式”支持。 Tim Deschryver 开始实现一些围绕信号形式的抽象,包括验证,并写了一篇关于它的文章。让我们看看将来 Angular 中是否会添加这样的东西。</p> <h3> 为什么使用角度形式的信号? </h3> <p>Angular 中信号的采用提供了一种强大的新方法来管理表单状态和反应性。信号提供了一种灵活的声明性方法,可以通过结合模板驱动表单和反应式表单的优势来简化复杂的表单处理。以下是使用 Angular 形式的信号的一些主要好处:</p> <ol> <li><p><strong>声明式状态管理</strong>:信号允许您以声明方式定义表单字段和计算值,使您的代码更可预测且更易于理解。</p></li> <li><p><strong>反应性</strong>:信号为表单字段提供反应性更新,确保表单状态的更改自动触发反应性更新。</p></li> <li><p><strong>粒度控制</strong>:信号允许您在粒度级别定义表单字段,从而实现对表单状态和验证的细粒度控制。</p></li> <li><p><strong>动态表单</strong>:信号可用于创建带有可动态添加或删除字段的动态表单,提供灵活的方式来处理复杂的表单场景。</p></li> <li><p><strong>简单性</strong>:与传统的反应式表单相比,信号可以提供更简单、更简洁的方式来管理表单状态,使构建和维护复杂表单变得更加容易。</p></li> </ol> <h3> 结论 </h3> <p>在我之前的文章中,我们探索了 Angular 反应式表单的强大功能,从动态表单构建到自定义表单控件。随着信号的引入,Angular 开发人员拥有了一种新工具,它将模板驱动表单的简单性与反应式表单的反应性融为一体。</p> <p>虽然许多用例都需要反应式表单,但信号为需要更直接、声明性方法的 Angular 应用程序中的表单状态管理提供了一种全新、强大的替代方案。随着 Angular 的不断发展,尝试这些新功能将帮助您构建更易于维护、性能更高的应用程序。</p> <p>编码愉快!</p>
以上是探索角度形式:信号的新替代方案的详细内容。更多信息请关注PHP中文网其他相关文章!