探索角度形式:信号的新替代方案

探索角度形式:信号的新替代方案

探索角度形式:信号的新替代方案

angular 的世界中,无论您是在制作简单的登录页面还是更复杂的用户配置文件界面,表单对于用户交互都是至关重要的。 angular 传统上提供两种主要方法:模板驱动表单反应式表单。在我之前的 angular 反应式表单系列中,我探索了如何利用反应式表单的强大功能来管理复杂逻辑、创建动态表单以及构建自定义表单控件。

用于管理反应性的新工具 - 信号 - 已在 angular 版本 16 中引入,此后一直是 angular 维护人员关注的焦点,并在版本 17 中变得稳定。信号允许您处理状态更改声明性地,提供了一个令人兴奋的替代方案,将模板驱动表单的简单性与反应表单的强大反应性结合起来。本文将研究信号如何为 angular 中的简单和复杂形式添加反应性。

回顾:角度形式方法

在深入探讨使用信号增强模板驱动表单的主题之前,让我们快速回顾一下 angular 的传统表单方法:

  1. 模板驱动表单:使用 ngmodel 等指令直接在 html 模板中定义,这些表单易于设置,非常适合简单表单。但是,它们可能无法提供更复杂场景所需的细粒度控制。

    这是模板驱动表单的最小示例:

    
    
```typescript
import { component } from '@angular/core';

@component({
  selector: 'app-root',
  templateurl: './app.component.html'
})
export class appcomponent {
  name = '';

  onsubmit() {
    console.log(this.name);
  }
}
```
  1. 反应式表单:使用 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 id="name" formcontrolname="name">
  <button type="submit">submit</button>
</form>
```

引入信号作为处理表单反应性的新方法

随着 angular 16 的发布,信号已经成为管理反应性的新方式。信号提供了一种声明式的状态管理方法,使您的代码更可预测且更易于理解。当应用于表单时,信号可以增强模板驱动表单的简单性,同时提供通常与反应性表单相关的反应性和控制。

让我们探索如何在简单和复杂的形式场景中使用信号。

示例 1:带有信号的简单模板驱动表单

考虑一个基本的登录表单。通常,这将使用模板驱动的表单来实现,如下所示:

<!-- login.component.html -->
<form name="form" (ngsubmit)="onsubmit()">
  <label for="email">e-mail</label>
  <input type="email" id="email" [(ngmodel)]="email" required email />
  <label for="password">password</label>
  <input type="password" id="password" [(ngmodel)]="password" required />
  <button type="submit">login!</button>
</form>
// 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" id="email" name="email" [(ngmodel)]="email" required email />
  <label for="password">password</label>
  <input type="password" name="password" id="password" [(ngmodel)]="password" required />
  <button type="submit">login!</button>
</form>

在此示例中,表单字段被定义为信号,允许在表单状态发生变化时进行反应式更新。 formvalue 信号提供反映表单当前状态的计算值。这种方法提供了一种更具声明性的方式来管理表单状态和反应性,将模板驱动表单的简单性与信号的强大功能结合起来。

您可能会想将表单直接定义为信号内的对象。虽然这种方法可能看起来更简洁,但在各个字段中输入内容并不会调度反应性更新,这通常会破坏交易。下面是一个 stackblitz 示例,其中一个组件遇到了此类问题:

因此,如果您想对表单字段中的更改做出反应,最好将每个字段定义为单独的信号。通过将每个表单字段定义为单独的信号,您可以确保对各个字段的更改正确触发反应性更新。

示例 2:带有信号的复杂形式

您可能看不到对简单表单(如上面的登录表单)使用信号的好处,但在处理更复杂的表单时它们真正发挥作用。让我们探讨一个更复杂的场景 - 一个用户个人资料表单,其中包含名字、姓氏、电子邮件、电话号码和地址等字段。 phonenumbers 字段是动态的,允许用户根据需要添加或删除电话号码。

以下是如何使用信号定义此形式:

// 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 -->
<form class="form">
  <label for="firstName">First Name</label> <input type="text" id="firstName" name="firstName" [(ngModel)]="firstName" required />

  <label for="lastName">Last Name</label> <input type="text" id="lastName" name="lastName" [(ngModel)]="lastName" required />

  <label for="email">Email</label> <input type="email" id="email" name="emailAddress" [(ngModel)]="email" required email />

  <div id="phoneNumbers">
    <label>Phone Numbers</label> @for (phone of phoneNumbers(); track i; let i = $index) {
    <div><input type="tel" name="phoneNumbers-{{ i }}" [(ngModel)]="phone" required /> <button type="button" (click)="removePhoneNumber(i)">Remove</button></div>
    } <button type="button" (click)="addPhoneNumber()">Add Phone Number</button>
  </div>

  <label for="street">Street</label> <input type="text" id="street" name="street" [(ngModel)]="street" required />

  <label for="city">City</label> <input type="text" id="city" name="city" [(ngModel)]="city" required />

  <label for="state">State</label> <input type="text" id="state" name="state" [(ngModel)]="state" required />

  <label for="zip">ZIP Code</label> <input type="text" id="zip" name="zip" [(ngModel)]="zip" required />

  @if(!formValid()) {
  <div class="message message--error">Form is invalid!</div>
  } @else {
  <div class="message message--success">Form is valid!</div>
  }
  
    {{ formValue() | json }}
  

在模板中,我们使用phonenumbers信号数组来动态渲染电话号码输入字段。 addphonenumber 和removephonenumber 方法允许用户反应性地添加或删除电话号码,从而更新表单状态。请注意 track 函数的用法,这是确保 ngfor 指令正确跟踪phonenumbers 数组更改所必需的。

这是复杂表单示例的 stackblitz 演示,供您试用:

使用信号验证表单

验证对于任何表单都至关重要,确保用户输入在提交之前满足所需的标准。使用信号,可以以反应性和声明性的方式处理验证。在上面的复杂表单示例中,我们实现了一个名为 formvalid 的计算信号,它检查所有字段是否满足特定的验证标准。

可以轻松自定义验证逻辑以适应不同的规则,例如检查有效的电子邮件格式或确保填写所有必填字段。使用信号进行验证可以让您创建更多可维护和可测试的代码,因为验证规则被明确定义并自动对表单字段中的更改做出反应。它甚至可以被抽象为一个单独的实用程序,以使其可以在不同形式中重用。

在复杂表单示例中,formvalid 信号可确保填写所有必填字段并验证电子邮件和电话号码格式。

这种验证方法有点简单,需要更好地连接到实际的表单字段。虽然它适用于许多用例,但在某些情况下,您可能需要等到 angular 中添加显式“信号形式”支持。 tim deschryver 开始实现一些围绕信号形式的抽象,包括验证,并写了一篇关于它的文章。让我们看看将来 angular 中是否会添加这样的东西。

为什么使用角度形式的信号?

angular 中信号的采用提供了一种强大的新方法来管理表单状态和反应性。信号提供了一种灵活的声明性方法,可以通过结合模板驱动表单和反应式表单的优势来简化复杂的表单处理。以下是使用 angular 形式的信号的一些主要好处:

  1. 声明式状态管理:信号允许您以声明方式定义表单字段和计算值,使您的代码更可预测且更易于理解。

  2. 反应性:信号为表单字段提供反应性更新,确保表单状态的更改自动触发反应性更新。

  3. 粒度控制:信号允许您在粒度级别定义表单字段,从而实现对表单状态和验证的细粒度控制。

  4. 动态表单:信号可用于创建带有可动态添加或删除字段的动态表单,提供灵活的方式来处理复杂的表单场景。

  5. 简单性:与传统的反应式表单相比,信号可以提供更简单、更简洁的方式来管理表单状态,使构建和维护复杂表单变得更容易。

结论

在我之前的文章中,我们探索了 angular 反应式表单的强大功能,从动态表单构建到自定义表单控件。随着信号的引入,angular 开发人员拥有了一种新工具,它将模板驱动表单的简单性与反应式表单的反应性融为一体。

虽然许多用例都需要反应式表单,但信号为需要更直接、声明性方法的 angular 应用程序中的表单状态管理提供了一种全新、强大的替代方案。随着 angular 的不断发展,尝试这些新功能将帮助您构建更易于维护、性能更高的应用程序。

编码愉快!

以上就是探索角度形式:信号的新替代方案的详细内容,更多请关注其它相关文章!