• [Angular 基础] - 表单:响应式表单


    [Angular 基础] - 表单:响应式表单

    之前的笔记:


    开始

    其实这里的表单和之前 Template-Driven Forms 没差很多,不过 Template-Driven Forms 主要在 V 层实现,而这里之后的主要功能会在 VM 层实现。

    • V 层代码如下:

      <div class="container">
        <div class="row">
          <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
            <form>
              <div class="form-group">
                <label for="username">Usernamelabel>
                <input type="text" id="username" class="form-control" />
              div>
              <div class="form-group">
                <label for="email">emaillabel>
                <input type="text" id="email" class="form-control" />
              div>
              <div class="radio" *ngFor="let gender of genders">
                <label> <input type="radio" [value]="gender" />{{ gender }} label>
              div>
              <button class="btn btn-primary" type="submit">Submitbutton>
            form>
          div>
        div>
      div>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
    • VM 层代码

      import { Component } from '@angular/core';
      import { FormGroup } from '@angular/forms';
      
      @Component({
        selector: 'app-root',
        templateUrl: './app.component.html',
        styleUrls: ['./app.component.css'],
      })
      export class AppComponent {
        genders = ['male', 'female'];
        signupForm: FormGroup;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • app module 代码

      import { BrowserModule } from '@angular/platform-browser';
      import { NgModule } from '@angular/core';
      
      import { AppComponent } from './app.component';
      import { ReactiveFormsModule } from '@angular/forms';
      
      @NgModule({
        declarations: [AppComponent],
        imports: [BrowserModule, ReactiveFormsModule],
        providers: [],
        bootstrap: [AppComponent],
      })
      export class AppModule {}
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

      ⚠️:这里导入的是 ReactiveFormsModule

    创建表单

    这里会用 Angular 提供的类去实现:

    export class AppComponent implements OnInit {
      genders = ['male', 'female'];
      signupForm: FormGroup;
    
      ngOnInit(): void {
        this.signupForm = new FormGroup({
          username: new FormControl(null),
          email: new FormControl(null),
          gender: new FormControl('male'),
        });
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    其中 FormGroup 接受的是一个对象,对象中的 key 是当前 FormGroup 需要管理的 from control,value 则是 FormControl, FormGroupFormArray sange 三个中的一个

    FormControl 中接受的参数则是默认值

    到这一步,表单的创建就完成了,下一步需要将表单和 V 层进行同步——此时的 Angular 并不知道 VM 层中的 signupForm 会对 V 层中的表单进行管理,也无法将 ``FormControl` 中的属性与表单中的 input 建立关联

    同步 V 和 VM 层

    这里依然通过绑定 directive 实现,用到的 directive 包含 formGroupformControlName,如下:

    <form [formGroup]="signupForm">
      <div class="form-group">
        <label for="username">Usernamelabel>
        <input
          type="text"
          id="username"
          class="form-control"
          formControlName="username"
        />
      div>
      <div class="form-group">
        <label for="email">emaillabel>
        <input
          type="text"
          id="email"
          class="form-control"
          [formControlName]="'email'"
        />
      div>
      <div class="radio" *ngFor="let gender of genders">
        <label>
          <input type="radio" [value]="gender" formControlName="gender" />{{ gender
          }}
        label>
      div>
      <button class="btn btn-primary" type="submit">Submitbutton>
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    其中 formGroupFormGroup 对应,formControlNameFormControl 的名字对应,效果如下:

    在这里插入图片描述

    我把 gender 的默认值改成了 femail,这里可以看到两个 radio button 的状态都是 untouched,不过默认值被设置成了 female

    提交表单 AKA 获取提交的值

    这个实现比较简单,可以直接通过属性获取当前表单的值

    • V 层修改

      <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        
      form>
      
      • 1
      • 2
      • 3
    • VM 层修改

      onSubmit() {
          console.log(this.signupForm);
      }
      
      • 1
      • 2
      • 3

    输出结果如下:

    在这里插入图片描述

    验证

    Reactive Form 中的验证通常会通过编程实现,如:

      ngOnInit(): void {
        this.signupForm = new FormGroup({
          username: new FormControl(null, Validators.required),
          email: new FormControl(null, [Validators.required, Validators.email]),
          gender: new FormControl('female'),
        });
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    展示报错信息

    这里的报错信息也不是用 directive,而是直接访问 signupForm:

    <p
      class="help-block"
      *ngIf="
                 signupForm.get('username').invalid &&
                 signupForm.get('username').touched
               "
    >
      Please enter a valid username!
    p>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    效果如下:

    在这里插入图片描述

    ⚠️:CSS 还是可以用一样的方式实现:

    input.ng-invalid.ng-touched {
      border: 1px solid red;
    }
    
    • 1
    • 2
    • 3

    组合表单

    这里使用 FormGroup 去解决需要组合数据的情况,比如说地址的组合通常为省+市+具体地址+邮编才能组合成一个完整的地址,实现方法如下:

      ngOnInit(): void {
        this.signupForm = new FormGroup({
          userData: new FormGroup({
            username: new FormControl(null, Validators.required),
            email: new FormControl(null, [Validators.required, Validators.email]),
          }),
          gender: new FormControl('female'),
        });
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这个时候 VM 层和 V 层的同步又会失败——这时候 username 和 email 没办法通过 signupForm.attribute 进行获取。也因此,V 层需要进行对应的修改:

    <div formGroupName="userData">
      <div class="form-group">
        <label for="username">Usernamelabel>
        <input
          type="text"
          id="username"
          class="form-control"
          formControlName="username"
        />
        <p
          class="help-block"
          *ngIf="
                    signupForm.get('userData.username').invalid &&
                    signupForm.get('userData.username').touched
                  "
        >
          Please enter a valid username!
        p>
      div>
      <div class="form-group">
        <label for="email">emaillabel>
        <input
          type="text"
          id="email"
          class="form-control"
          [formControlName]="'email'"
        />
      div>
    div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    这里将 username 和 email 用一个 div formGroupName="userData" 进行绑定,去还原 VM 层的结构。属性的获取也从 signupForm.get('username') 改为了 signupForm.get('userData.username')

    通过这样的修改,就可以解决 console 出现的报错信息,当前表单的功能也可以正常运行

    Form Array

    FormArray 是一个比较方便接受数组数据的结构,实现如下:

    • V 层

      <div formArrayName="hobbies">
        <h4>Your Hobbiesh4>
        <button class="btn btn-default" type="button" (click)="onAddHobby()">
          Add Hoby
        button>
        <div
          class="form-group"
          *ngFor="let hobbieControl of hobbies.controls; let i = index"
        >
          <input type="text" class="form-control" [formControlName]="i" />
        div>
      div>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • VM 层

      export class AppComponent implements OnInit {
        ngOnInit(): void {
          this.signupForm = new FormGroup({
            userData: new FormGroup({
              username: new FormControl(null, Validators.required),
              email: new FormControl(null, [Validators.required, Validators.email]),
            }),
            gender: new FormControl('female'),
            hobbies: new FormArray([]),
          });
        }
      
        get hobbies() {
          return this.signupForm.get('hobbies') as FormArray;
        }
      
        onAddHobby() {
          const control = new FormControl(null, Validators.required);
          this.controls.push(control);
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

    渲染结果如下:

    在这里插入图片描述

    这里 VM 层有两个比较大的变动:

    1. getter

      getter 主要是为了 TS 验证方便,V 层直接调用 signupForm.get('hobbies') 会因为数据类型不明确而导致报错——报错信息大概是这样的: XXX does not exist on AbstractControl

      看了下,FormArray 没有 overload get(),因此调用的还是 AbstractControl 中的函数,所以需要做类型转换

      getter 可以有效的解决这个问题

    2. hobbies.controls

      FormArray。controls 是 Angular 用来提供循环的值,案例中多用来和 ngFor 搭配使用

      本质上 hobbies 是一个 FormArray,直接在 V 层调用会报错,可以使用 push 是因为 Angular 实现了 push

      export declare class FormArray<
        TControl extends AbstractControl<any> = any
      > extends AbstractControl<
        ɵTypedOrUntyped<TControl, ɵFormArrayValue<TControl>, any>,
        ɵTypedOrUntyped<TControl, ɵFormArrayRawValue<TControl>, any>
      > {
        push(
          control: TControl,
          options?: {
            emitEvent?: boolean;
          }
        ): void;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    自定义验证

    本质上来说,validator 就是一个需要传出去的函数,让 Angular 在每次状态变化时调用,因此实现自定义就是实现一个函数,实现如下:

    export class AppComponent implements OnInit {
      forbiddenUsernames = ['admin', 'super'];
    
      ngOnInit(): void {
        this.signupForm = new FormGroup({
          userData: new FormGroup({
            username: new FormControl(null, [
              Validators.required,
              this.forbiddenNames.bind(this),
            ]),
          }),
        });
      }
    
      forbiddenNames(control: FormControl): { [key: string]: boolean } {
        if (this.forbiddenUsernames.includes(control.value)) {
          return { nameIsForbidden: true };
        }
    
        return null;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    效果如下:

    在这里插入图片描述

    👀:注意这里自定义验证传送的方式:this.forbiddenNames.bind(this),其原因是因为在 forbiddenNames 调用了 this.forbiddenUsernames,因此需要绑定对应的 scope,否则 Angular 会因为 this 的指向变更而找不到 this.forbiddenUsernames

    ⚠️:这是 JavaScript 的问题,与框架无关

    异步自定义验证

    这个实现和自定义验证类似,不过返回的对象是 Promise | Observable,实现如下:

    export class AppComponent implements OnInit {
      ngOnInit(): void {
        this.signupForm = new FormGroup({
          userData: new FormGroup({
            email: new FormControl(
              null,
              [Validators.required, Validators.email],
              this.forbiddenEmails
            ),
          }),
        });
      }
    
      forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
        const promise = new Promise<any>((res, rej) => {
          setTimeout(() => {
            if (control.value === 'admin@test.com') {
              res({ emailIsForbidden: true });
            } else {
              res(null);
            }
          }, 1500);
        });
    
        return promise;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    效果如下:

    在这里插入图片描述

    可以看到,当输入为 admin@test.com 时,又过了大概 1.5s 之后,输入框才跳为红色——这是之前加的出现 error 的 CSS

    有一点我截图的时候没截到,就是当等待验证的过程中,Angular 会自动为当前 class 添加一个 ng-pending 的类名:

    在这里插入图片描述

    使用 errors

    上面根据自定义验证可以得知,Validators 返回的是一个 { [key: string]: boolean },或者准确的说是这个结构:

    export declare type ValidationErrors = {
      [key: string]: any;
    };
    
    export declare interface ValidatorFn {
      (control: AbstractControl): ValidationErrors | null;
    }
    
    export declare class Validators {
      static min(min: number): ValidatorFn;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    换言之,在 errors 中,要么是 null,要么应该会返回一个 {[errorName: string]: any} 的类型:

    在这里插入图片描述

    也就是说,如果想要细化报错信息,那么就可以通过 errors 这个对象去实现,V 层修改代码如下:

    <p
      class="help-block"
      *ngIf="
                    signupForm.get('userData.username').invalid &&
                    signupForm.get('userData.username').touched
                  "
    >
      <span
        *ngIf="
                      signupForm.get('userData.username').errors['nameIsForbidden']
                    "
        >This username is invalid!span
      >
      <span *ngIf="signupForm.get('userData.username').errors['required']"
        >This field is required!span
      >
    p>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    效果展示:

    在这里插入图片描述

    状态追踪

    之前提到了 async validator 的使用,这里补充一下怎么追踪类似的变化。这里依旧通过 subscribe 两个 Observable 时实现:

    this.signupForm.valueChanges.subscribe((val) => {
      console.log(val);
    });
    this.signupForm.statusChanges.subscribe((val) => {
      console.log(val);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    效果如下:

    在这里插入图片描述

    如果需要 track 状态的变化,从而进行更细致的处理,就可以通过 valueChangesstatusChanges 进行

    更新数据

    这里的用法和 Template-Driven Form 一致,我也就不多提了,具体调用的函数如下:

    this.signupForm.setValue({});
    this.signupForm.patchValue({});
    this.signupForm.reset();
    
    • 1
    • 2
    • 3

    总结

    Reactive Form 相对于 Template-Driven Form 灵活性更高一些,不过为了保持 V 层和 VM 层的同步,二者的结构需要保持一致。

    V 层中的每一层结构依旧需要使用对应的 directive 与 VM 层中的数据进行绑定:

    • 最外层的 form 需要通过 FormGroup 对应 formGroup
    • form 里的结构都以对应的类型+Name 的方式绑定,主要包含: FormGroupName, FormArrayNameformControlName

    Reactive Form 主要功能实现都在 VM 层,操作的对象时 VM 中创建的 FromGroup 对象,对比 Template-Driven Form 是在 V 层自动对表单进行管理,操作时需要将 local reference 传到对应的函数中去,或是使用 @ViewChild 获得对应的 ElementRef

    Reactive Form 的 directive 通过 ReactiveFormsModule 引入,而 Template-Driven Form 通过 FormsMorule

    Reactive FormTemplate-Driven Form
    管理VM 层V 层
    对应模块ReactiveFormsModuleFormsMorule
    绑定方法[formGroup]="", formGroupName="", etclocal reference
    #form="ngForm", #name="ngModel"
    优点控制灵活更少的 boilerplate code
  • 相关阅读:
    java计算机毕业设计Vue潍坊学院宿舍管理系统设计与实现MyBatis+系统+LW文档+源码+调试部署
    磨金石教育摄影技能干货分享|古风人像修图与调色技巧
    第二证券:长期停牌一般是多久?
    user-agent怎么获取
    启动Tomcat报:Failed to initialize connector [Connector[HTTP/1.1-80]
    位图——使用long类型表示0~63数字
    支持向量机----核技巧(Kernel Trick)
    上个厕所的时间了解链路追踪基本概念
    AI领域里违规话术如何检测?如何避免直播时违规
    Information Sciences 2022 | 利用图嵌入和图神经网络实现社交网络中的影响力最大化
  • 原文地址:https://blog.csdn.net/weixin_42938619/article/details/136617077