Angular 深入浅出三十天:表单与测试 Day03 - Reactive Forms 实作 - 以登入为例

Day3

今天要来用 Reactive Forms 的方式实作一个简单的登入系统,撇开 UI 不谈,具体的功能需求规格跟昨天差不多,如下所示:

  • 帐号
    • 格式为 Email Address,相关规则请参考维基百科,此处则直接使用正规表示法 /^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi 来检验,验证有误时需在栏位後方显示错误讯息:格式有误,请重新输入
    • 此栏位必填,验证有误时需需在栏位後方显示错误讯息:此栏位必填
  • 密码
    • 长度最短不得低於 8 码,验证有误时需需在栏位後方显示错误讯息:密码长度最短不得低於8码
    • 长度最长不得超过 16码,验证有误时需需在栏位後方显示错误讯息:密码长度最长不得超过16码
    • 此栏位必填,验证有误时需需在栏位後方显示错误讯息:此栏位必填
  • 以上验证皆需在使用者输入时动态检查
  • 任一验证有误时,登入按钮皆呈现不可被点选之状态。

规格需求看清楚之後,我们就来开始实作吧!

实作时大家可以自己开一个专案来练习,抑或是用 Stackblitz 开一个 Angular 的专案来练习,我就不再赘述罗!

如果正在阅读此篇文章的你还不知道要怎麽开始一个 Angular 专案的话,请先阅读我的 Angular 深入浅出三十天後再来阅读此系列文章会比较恰当噢!

实作开始

首先我们先准备好基本的 HTML :

<form>
  <p>
    <label for="account">帐号:</label>
    <input type="email" id="account">
  </p>
  <p>
    <label for="password">密码:</label>
    <input type="password" id="password">
  </p>
  <p>
    <button type="submit">登入</button>
  </p>
</form>

未经美化的画面应该会长这样:

Template view

接着到 app.module.ts 里 import FormsModuleReactiveFormsModule

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  imports: [
    BrowserModule, 
    FormsModule,
    ReactiveFormsModule
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

然後将要绑在 Template 的属性跟方法都准备好:

export class LoginComponent implements OnInit {
  
  // 绑定在表单上
  formGroup: FormGroup;
  
  /**
   * 用以取得帐号栏位的表单控制项
   */
  get accountControl(): FormControl {
    return this.formGroup.get('account') as FormControl;
  }

  /**
   * 用以取得密码栏位的表单控制项
   */
  get passwordControl(): FormControl {
    return this.formGroup.get('password') as FormControl;
  }

  /**
   * 透过 DI 取得 FromBuilder 物件,用以建立表单
   */
  constructor(private formBuilder: FormBuilder) {}

  /**
   * 当 Component 初始化的时候初始化表单
   */
  ngOnInit(): void {
    this.formGroup = this.formBuilder.group({
      account: [
        '', 
        [
          Validators.required,
          Validators.pattern(/^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi)
        ]
      ],
      password: [
        '',
        [
          Validators.required, 
          Validators.minLength(8), 
          Validators.maxLength(16)
        ]
      ]
    });
  }

  // 绑定在表单上,当使用者按下登入按钮时会触发此函式
  login(): void {
    // do login...
  }

  /**
   * 透过该栏位的表单控制项来取得该栏位的错误讯息
   * 
   * @param {FormControl} formControl 欲取得错误讯息的栏位的表单控制项 (by Angular)
   */
  getErrorMessage(formControl: FormControl): string {
    let errorMessage: string;
    if (!formControl.errors || formControl.pristine) {
      errorMessage = '';
    } else if (formControl.errors.required) {
      errorMessage = '此栏位必填';
    } else if (formControl.errors.pattern) {
      errorMessage = '格式有误,请重新输入';
    } else if (formControl.errors.minlength) {
      errorMessage = '密码长度最短不得低於8码';
    } else if (formControl.errors.maxlength) {
      errorMessage = '密码长度最长不得超过16码';
    }
    return errorMessage;
  }

}

就可以将这些属性和方法跟 Template 绑定在一起:

<form [formGroup]="formGroup" (ngSubmit)="login()">
  <p>
    <label for="account">帐号:</label>
    <input
      type="email"
      id="account"
      [formControl]="accountControl"
    />
    <span class="error-message">{{ getErrorMessage(accountControl) }}</span>
  </p>
  <p>
    <label for="password">密码:</label>
    <input
      type="password"
      id="password"
      [formControl]="passwordControl"
    />
    <span class="error-message">{{ getErrorMessage(passwordControl) }}</span>
  </p>
  <p>
    <button type="submit" [disabled]="formGroup.invalid">登入</button>
  </p>
</form>

到目前为止的程序码你看懂了多少呢?对於刚接触 Angular 的表单的朋友来说,今天的资讯量可能会比较大,容我稍微说明一下:

Reactive Forms 的概念是将表单程序的方式产生。以这个需求来说,这个表单底下会有两个栏位 accountpassword ,如果将其用 JSON 来表示的话,应该会长这样:

{ 
  "account": "", 
  "password": "" 
}

从资料面来看, {} 代表表单, "account": """password": "" 则是里面的两个栏位。

而再将其转换成 Reactive Forms 的概念的话, {} 代表的是 FormGroup"account": """password": "" 则代表的是 FormControl

所以在程序码中我们可以看到我们宣告 formGroup: FromGroup; 并且在 template 中将其绑定在表单上:

<form [formGroup]="formGroup">
  <!-- ... -->
</form>

并且把表单控制项绑定在对应的 input 栏位上:

<!-- 帐号栏位 -->
<input
  type="email"
  id="account"
  [formControl]="accountControl"
/>

<!-- 密码栏位 -->
<input
  type="password"
  id="password"
  [formControl]="passwordControl"
/>

然後在 ngOnInit 里透过 FormBuilder 来初始化表单:

ngOnInit(): void {
  this.formGroup = this.formBuilder.group({
    account: '我是该栏位的初始值',
    password: '我是该栏位的初始值'
  });
}

如此一来,就可以在初始化过後,跟我们的 template 正确绑定了。

而如果当该栏位需要验证时,就要在初始化时将格式调整成:

ngOnInit(): void {
  this.formGroup = this.formBuilder.group({
    account: ['我是该栏位的初始值', /* 验证器的摆放位置 */],
    password: ['我是该栏位的初始值', /* 验证器的摆放位置 */],
  });
}

如果只有一个要验证的项目则可以直接放入:

ngOnInit(): void {
  this.formGroup = this.formBuilder.group({
    account: ['我是该栏位的初始值', Validators.required],
    password: ['我是该栏位的初始值', Validators.required],
  });
}

如果有多个要验证的项目,就用 [] 将多个验证项包起来再放入:

ngOnInit(): void {
  this.formGroup = this.formBuilder.group({
    account: [
      '我是该栏位的初始值', 
      [
        Validators.required,
        Validators.pattern(/^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi)
      ]
    ],
    password: [
      '我是该栏位的初始值', 
      [
        Validators.required, 
        Validators.minLength(8), 
        Validators.maxLength(16)
      ]
    ],
  });
}

在这里我们可以发现,上一篇使用 Template Driven Forms 实作时,是用 HTML 原生的属性来验证,而今天使用 Reactive Forms 实作时,则是用程序来验证,如此一来,可以降低表单与 template 之间的依赖性,使得其更易於维护、重用与测试。

Validators 是 Angular 帮我们制作的验证器,里面有很多常用验证器,详细请参考官方文件

当然我们也可以自己客制验证器,只要符合 ValidatorFn 的类型即可

关於错误讯息基本上可以沿用上一篇的程序,只不过原本是传入 FormControlerrors 来判断,但现在是传入整个 FormControl ,为什麽呢?

因为如果只有传入 FormControlerrors 的话,你会发现表单初始化完之後,就会有错误讯息显示在画面上:

img

这是因为当我们的表单初始化完之後,验证器就会开始运作,所以的确那个两个栏位是有那个错误没错,但其实这不是我们想要的行为,因为使用者根本就还没有开始填表单,我们想要的是当使用者开始填表单之後,才会显示对应的错误讯息,所以我们改传入整个 FormControl ,它其中有几个很好用的属性可以使用:

  • pristine ─ 如果此属性为 true ,代表该栏位是乾净,没有被输入过值;反之则代表有被输入过值,与 dirty 成反比。
  • touched ─ 如果此属性为 true,代表该栏位曾经被碰(该栏位曾经被使用滑鼠 focus 过);反之则代表该栏位完全没被碰过。
  • dirty ─ 如果此属性为 true ,代表该栏位曾经被输入过值,已经脏掉了;反之则代表该栏位是乾净,没有被输入过值,与 pristine 成反比。

想知道更多可以参考官方文件: FormControl 与其抽象类别 AbstractControl

所以我们只要加上当该栏位是乾净的,就不回传错误讯息的判断就可以了,像是这样:

getErrorMessage(formControl: FormControl): string {
  let errorMessage: string;
  if (!formControl.errors || formControl.pristine) {
    errorMessage = '';
  }
  // 其他省略...
}

最终结果:

complete gif

本日小结

对於第一次接触 Reactive Forms 的朋友们,今天的资讯量会比较多,但重点大致上可归纳成以下四点:

  1. 学习如何将表单程序的方式写出来,心法:「资料即表单,表单即资料」
  2. 学习如何使用表单物件 FormBuilderFormGroupFormControl
  3. 学习如何使用 Validators 来验证使用者所输入的值。
  4. 学习如何将表单物件与 Template 绑定。

此外,千万记得要 import FormsModuleReactiveFormsModule 才可以使用噢!

我一样会将今日的实作程序码放在 Stackblitz 上供大家参考,建议大家在看我的实作之前,先按照需求规格自己做一遍,之後再跟我的对照,看看自己的实作跟我的实作不同的地方在哪里、有什麽好处与坏处,如此反覆咀嚼消化後,我相信你一定可以进步地非常快!

如果有任何的问题或是回馈,也都非常欢迎留言给我让我知道噢!


<<:  Day 18:Kotlin 过滤(filter)集合资料用法

>>:  [Day-18] R语言 - 分群应用(一) k - prototype类别补值 - 上 ( Fill.NA with k - prototype in R.Studio )

Day.4 针对使用者做管理 - 权限管理&资安 (Power)

在资料库管理上,root 相当於拥有所有权限的最大管理者,针对不同使用者规划给予相应的权限是很重要的...

Day12-Express 的部署

Express 利用 pm2 做管理(因为 docker 坑很深 加上来的话会写不完) Expres...

[第二十六天]从0开始的UnityAR手机游戏开发-输出64位元的APP

点开Project Settings的other,把Scripting Backend改为IL2CP...

Day 12 让你的广告活动可以超乎预料的好

就像昨天提到的,我们设定好广告活动和群组之後,当然 Google 会按照你期待的方式,将广告费用投放...

认识强大的Python套件:Pandas(下)

今天我们接着继续和DataFrame继续奋斗!先把套件和档案载入: import pandas as...