Angular 深入浅出三十天:表单与测试 Day23 - Reactive Forms 进阶技巧 - 栏位连动检核逻辑

Day23

大家在日常生活中,应该看过满多表单的某个栏位会随着另个栏位的改变,而造成该栏位的验证逻辑需要改变的情况吧?

举例来说,可能会有个栏位叫做联络资讯,使用者可以选择要填入手机号码或者是 E-mail ,该栏位再根据使用所选择的类型来检核该栏位的值。

今天,我们就来用 Reactive Forms 实作这个栏位,而这个栏位我会实作在我们的被保人表单上,各位就随意吧!

如果已经忘记被保人表单长怎麽样的话,可以先回头复习一下第十一天的文章:Reactive Forms 实作 - 动态表单初体验

实作开始

首先,我们需要在原本的被保人表单里新增一个栏位:联络资讯。

HTML 的部份大概会长这样:

<p>
  <label>联络资讯:</label>
</p>
<p>
  <select>
    <option value="">请选择</option>
    <option value="mobile">手机</option>
    <option value="email">E-Mail</option>
  </select>
  <input type="text">
</p>

画面看起来会像这样:

Insured View

虽然联络资讯是一个栏位,但其实我们需要两个 FormControl ,一个给下拉选单,一个给实际填值的 input 元素。

因此,我们要在原本的 createInsuredFormGroup 里多加两个栏位,像是这样:

private createInsuredFormGroup(): FormGroup {
  return this.formBuilder.group({
    name: [
      '',
      [Validators.required, Validators.minLength(2), Validators.maxLength(10)]
    ],
    gender: ['', Validators.required],
    age: ['', Validators.required],
    contactInfoType: ['', Validators.required],
    contactInfo: ['', Validators.required]
  });
}

然後将刚刚新增的栏位与画面的元素绑定:

<p>
  <select formControlName="contactInfoType">
    <option value="">请选择</option>
    <option value="mobile">手机</option>
    <option value="email">E-Mail</option>
  </select>
  <input type="text" formControlName="contactInfo">
</p>

接着我们透过把资料印在画面上的方式来检查是否已正确绑定,像这样:

<pre>{{ formGroup?.getRawValue() | json }}</pre>

结果:

Insured View

看起来已经有正确跟画面上的元素绑定了,那接下来要怎麽做才好呢?

valueChanges

FormControl 的父类别 AbstractControl 有个属性叫做 valueChanges ,它是一个 Observable

我们可以透过订阅某个 AbstractControlvalueChanges 这个 Observable 来知道该栏位是否已经发生变化,并且做出相应的处理。

因此,我们可以这样调整 createInsuredFormGroup 里的实作:

private createInsuredFormGroup(): FormGroup {
  const contactInfoTypeControl = this.formBuilder.control('', Validators.required);
  const contactInfoControl = this.formBuilder.control('', Validators.required);
  contactInfoTypeControl.valueChanges.subscribe((value) => {
    switch (value) {
      case 'mobile':
        contactInfoControl.setValidators([Validators.required, Validators.pattern(/^09\d{8}$/)]);  
        break;
      case 'email':
        contactInfoControl.setValidators([Validators.required, Validators.email]);  
        break;
      default:
        contactInfoControl.setValidators([Validators.required]);  
        break;
    }
    contactInfoControl.updateValueAndValidity();
  });

  return this.formBuilder.group({
    name: [
      '',
      [Validators.required, Validators.minLength(2), Validators.maxLength(10)]
    ],
    gender: ['', Validators.required],
    age: ['', Validators.required],
    contactInfoType: contactInfoTypeControl,
    contactInfo: contactInfoControl
  });
}

上述程序码中有以下三个要点:

  1. 建立 FormControl 的时候可以藉由 this.formBuilder.control() 的方式建立,也可以直接使用 new FormControl() 建立,这点在前面的文章已经有提过,不过我在这边再提醒大家一次。

  2. setValidators() 执行完後,记得一定要使用 updateValueAndValidity() 来更新当前栏位的验证,不然就要等到该栏位的值有改变时才会以新的验证器来验证。

  3. 由於 contactInfoType 允许使用者选择 请选择 的选项,因此记得在 default 的区块里,将 Validators.required 给加回去。

这边改好之後,我们也顺便调整一下 getErrorMessage 的实作,让使用者可以知道该栏位的验证有误:

getErrorMessage(key: string, index: number): string {
  const formGroup = this.formArray.controls[index];
  const formControl = formGroup.get(key);
  let errorMessage: string;
  if (!formControl || !formControl.errors || formControl.pristine) {
    errorMessage = '';
  } else if (formControl.errors.required) {
    errorMessage = '此栏位必填';
  } else if (formControl.errors.minlength) {
    errorMessage = '姓名至少需两个字以上';
  } else if (formControl.errors.maxlength) {
    errorMessage = '姓名至多只能输入十个字';

  // 增加以下两个判断  
  } else if (formControl.errors.pattern) {
    errorMessage = '手机号码格式错误';
  } else if (formControl.errors.email) {
    errorMessage = 'E-mail 格式错误';
  }

  return errorMessage!;
}

这边要提醒大家的是,由於验证 E-mail 格式的方式我今天是用 Validators.email 的验证器来验,不是之前的 Validators.pattern() ,所以我可以直接用 formControl.errors.email 来判断。

如果实作时,手机号码跟 E-mail 都是用 Validators.pattern() 的验证器来验的话,就需要进一步去比对 formControl.errors.pattern 里的 Regular Expression 来分辨究竟是手机号码的格式错误还是 E-mail 的格式错误了。

像是这样:

} else if (formControl.errors.pattern) {
  const requiredPattern = formControl.errors.pattern.requiredPattern;
  if (requiredPattern === '/A Regular Expression/') {
    errorMessage = '手机号码格式错误';
  } else if (requiredPattern === '/B Regular Expression/') {
    errorMessage = 'E-mail 格式错误';
  }
}

如此一来,我们就完成这个栏位的功能罗!

结果:

Insured View

本日小结

今天的重点是学会如何使用 valueChanges 来动态调整相关栏位的验证逻辑。

虽然是 Observable 是 RxJS 的东西,但今天并没有太艰难或太复杂的运用,使用上的感觉会跟使用 Promise 的感觉类似,不过我个人认为 RxJS 好玩且强大许多。

关於 RxJS ,如果大家想知道更多资讯,我推荐大家去看 Mike 的打通 RxJS 任督二脉系列文,或者是直接买实体书也行。

虽然今天的实作已经完成了,但因为有调整程序码的关系,测试程序码其实也需要相应的调整才不会出错,此部份就交给大家实作我就不再用篇幅分享实作罗!

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

如果有任何的问题或是回馈,还请麻烦留言给我让我知道!


<<:  Day 23 Flask-Login

>>:  Day26 vue.js功能展示ep2之有"大麻"烦(cros跨域)

【这些年我似是非懂的 Javascript】那些年我睡掉的物件导向 #浅谈 #Part 2

嗨各位好久不见, 今天要来分享上次的续集 第 2 part , 上篇讲到建构器 今天要来分享关於继...

Day 3 Capsule Network

Day 3 Capsule Network 前言 昨天讲到CNN的限制,那今天就要开始介绍甚麽是胶囊...

Day 30 [分享] 学习 JavaScript 的优秀资源

底下为一些资源 JavaScript 标准参考教程(alpha) ECMAScript 6 入门 现...

[Day29]FFmpeg切割影片

未来社会中,文盲并非不识字的人,而是不能再学习的人。铁人赛就是强迫自己学习的好机会。 大家好今天我要...

Day 16 AWS云端实作起手式第六弹 串接两大网路流量导流服务Route53和ELB

今天我们会把Route 53串接到昨天建置的ELB上。但开始之前,我们先问自己一个问题,Route ...