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

news2024/11/18 0:49:23

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

之前的笔记:

  • [Angular 基础] - routing 路由(下)

  • [Angular 基础] - Observable

  • [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">Username</label>
              <input type="text" id="username" class="form-control" />
            </div>
            <div class="form-group">
              <label for="email">email</label>
              <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">Submit</button>
          </form>
        </div>
      </div>
    </div>
    
  • 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;
    }
    
  • 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 {}
    

    ⚠️:这里导入的是 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'),
    });
  }
}

其中 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">Username</label>
    <input
      type="text"
      id="username"
      class="form-control"
      formControlName="username"
    />
  </div>
  <div class="form-group">
    <label for="email">email</label>
    <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">Submit</button>
</form>

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

在这里插入图片描述

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

提交表单 AKA 获取提交的值

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

  • V 层修改

    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
      <!-- 其余不变 -->
    </form>
    
  • VM 层修改

    onSubmit() {
        console.log(this.signupForm);
    }
    

输出结果如下:

在这里插入图片描述

验证

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'),
    });
  }

展示报错信息

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

<p
  class="help-block"
  *ngIf="
             signupForm.get('username').invalid &&
             signupForm.get('username').touched
           "
>
  Please enter a valid username!
</p>

效果如下:

在这里插入图片描述

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

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

组合表单

这里使用 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'),
    });
  }

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

<div formGroupName="userData">
  <div class="form-group">
    <label for="username">Username</label>
    <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">email</label>
    <input
      type="text"
      id="email"
      class="form-control"
      [formControlName]="'email'"
    />
  </div>
</div>

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

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

Form Array

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

  • V 层

    <div formArrayName="hobbies">
      <h4>Your Hobbies</h4>
      <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>
    
  • 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);
      }
    }
    

渲染结果如下:

在这里插入图片描述

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

  1. getter

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

    看了下,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;
    }
    

自定义验证

本质上来说,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;
  }
}

效果如下:

在这里插入图片描述

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

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

异步自定义验证

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

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;
  }
}

效果如下:

在这里插入图片描述

可以看到,当输入为 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;
}

换言之,在 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>

效果展示:

在这里插入图片描述

状态追踪

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

this.signupForm.valueChanges.subscribe((val) => {
  console.log(val);
});
this.signupForm.statusChanges.subscribe((val) => {
  console.log(val);
});

效果如下:

在这里插入图片描述

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

更新数据

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

this.signupForm.setValue({});
this.signupForm.patchValue({});
this.signupForm.reset();

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1509482.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue-创建vue项目记录

安装node.js 先安装node.js的运行环境node.js的下载地址 安装后就可以使用npm命令 1、清除npm缓存&#xff1a;npm cache clean --force 2、禁用SSL&#xff1a;npm config set strict-ssl false 3、手动设置npm镜像源&#xff1a;npm config set registry https://registry.…

Python AI 之Stable-Diffusion-WebUI

Stable-Diffusion-WebUI简介 通过Gradio库&#xff0c;实现Stable Diffusion web 管理接口 Windows 11 安装Stable-Diffusion-WebUI 个人认为Stable-Diffusion-WebUI 官网提供的代码安装手册/自动安装不适合新手安装&#xff0c;我这边将一步步讲述我是如何搭建Python Conda…

centos 系统 yum 无法安装(换国内镜像地下)

centos 系统 yum 因为无法连接到国外的官网而无法安装&#xff0c;问题如下图&#xff1a; 更换阿里镜像&#xff0c;配置文件路径&#xff1a;/etc/yum.repos.d/CentOS-Base.repo&#xff08;如果目录有多余的文件可以移动到子目录&#xff0c;以免造成影响&#xff09; bas…

php CI框架异常报错通过钉钉自定义机器人发送

php CI框架异常报错通过钉钉自定义机器人发送 文章目录 php CI框架异常报错通过钉钉自定义机器人发送前言一、封装一个异常监测二、封装好钉钉信息发送总结 前言 我们在项目开发中&#xff0c;经常会遇到自己测试接口没问题&#xff0c;上线之后就会测出各种问题&#xff0c;主…

弹性盒子布局 Flexbox Layout

可以嵌套下去 1.display 属性 默认行排列 <style>.flex-item{ height: 20px;width: 10px;background-color: #f1f1f1;margin: 10px;}</style> </head> <body> <div class"flex-container"><div class"flex-item">1&l…

ajax异步访问及跨域处理

文章目录 1 认识同步和异步1.1 什么是同步交互1.2 什么是异步交互 2 AJAX介绍3 案例开发之验证用户名4 JSON格式4.1 响应普通文本数据4.2 JSON的介绍和应用4.3 JSON 与 JS 对象的关系4.4 JSON 和 JS 对象互转4.5 GSON工具类的使用 5 AJAX结合jQuery实现5.1 jQuery.ajax()的简单…

问题解决:NPM 安装 TypeScript出现“sill IdealTree buildDeps”

一、原因&#xff1a; 使用了其他镜像&#xff08;例如我使用了淘宝镜像 npm config set registry https://registry.npm.taobao.org/ &#xff09; 二、解决方法&#xff1a; 1.切换为原镜像 npm config set registry https://registry.npmjs.org 安装typescript npm i …

vscode设置setting.json

{ // vscode默认启用了根据文件类型自动设置tabsize的选项 "editor.detectIndentation": false, // 重新设定tabsize "editor.tabSize": 2, // #每次保存的时候自动格式化 // "editor.formatOnSave": true, // #每次保存的时候将代码按eslint格式…

java学习(集合)

一.集合(主要是单列集合和双列集合) 1.集合的框架体系&#xff08;两大类&#xff09; 2.collection接口是实现类的特点&#xff1a; 1)collection实现子类可以存放多个元素&#xff0c;每个元素可以是Object 2)有效Collection的实现类&#xff0c;可以存放重复的元素&#…

交叉编译x264 zlib ffmpeg以及OpenCV等 以及解决交叉编译OpenCV时ffmpeg始终为NO的问题

文章目录 环境编译流程nasm编译x264编译zlib编译libJPEG编译libPNG编译libtiff编译 FFmpeg编译OpenCV编译问题1解决方案 问题2解决方案 总结 环境 系统&#xff1a;Ubutu 18.04交叉编译链&#xff1a;gcc-arm-10.2-2020.11-x86_64-aarch64-none-linux-gnu 我的路径/opt/toolch…

解释“RNN encode-decode”

“RNN encode-decode” 涉及使用循环神经网络&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;来执行编码和解码操作。这种结构常用于处理序列数据&#xff0c;例如自然语言处理、语音识别和时间序列预测等任务。 以下是 “RNN encode-decode” 的一般概念&a…

week07day01(窗口函数)

一. 窗口函数的定义和一些规范&#xff1a; 对数据进行分区&#xff0c;数据的样式是不改变的&#xff0c;但是会多添加一列。窗口函数只能写在"结果集"中。 二. 排名函数 1. rank() over() 例题&#xff1a;对每个人的消费金额进行排名&#xff1a; rank() …

基于Android的教学课程系统设计与开发

摘 要 移动应用已经成为人们生活必不可缺的一部分&#xff0c;大学生身为移动应用的最大用户群体&#xff0c;在生活学习娱乐各个方面都与移动应用有着紧密联系&#xff0c;然而针对大学生校园学习的移动应用却寥寥无几&#xff0c;因为不同的学校&#xff0c;甚至不同的院系&…

unity显示当前时间

1建立文本组件和一个空对象 2创建一个脚本并复制下面代码 using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine;public class showtime: MonoBehaviour {public TextMeshProUGUI time;private void Update(){string currentTime Sy…

抖音开放平台第三方开发,实现代小程序备案申请

大家好&#xff0c;我是小悟 抖音小程序备案整体流程总共分为五个环节&#xff1a;备案信息填写、平台初审、工信部短信核验、通管局审核和备案成功。 服务商可以代小程序发起备案申请。在申请小程序备案之前&#xff0c;需要确保小程序基本信息已填写完成、小程序至少存在一个…

Clearview X for mac v3.5.0 电子书阅读器 兼容 M1/M2/M3

应用介绍 Clearview X 是 macOS 上的一款简洁易用且美观大方的电子书阅读器。直观好用的图书管理功能&#xff0c;支持 PDF, Epub, MOBI, CHM, FB2, CBR, CBZ 等流行的电子书格式&#xff0c;可以方便地添加注解&#xff0c;插入书签&#xff0c;及迅速的搜索查找。支持在不同…

基于Springboot的高校竞赛管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的高校竞赛管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构…

C#/WPF 清理任务栏托盘图标缓存

在我们开发Windows客户端程序时&#xff0c;往往会出现程序退出后&#xff0c;任务还保留之前程序的缓存图标。每打开关闭一次程序&#xff0c;图标会一直增加&#xff0c;导致托盘存放大量缓存图标。为了解决这个问题&#xff0c;我们可以通过下面的程序清理任务栏托盘图标缓存…

深度学习_AlexNet_2

目标 知道AlexNet网络结构能够利用AlexNet完成图像分类 2012年&#xff0c;AlexNet横空出世&#xff0c;该模型的名字源于论文第一作者的姓名Alex Krizhevsky 。AlexNet使用了8层卷积神经网络&#xff0c;以很大的优势赢得了ImageNet 2012图像识别挑战赛。它首次证明了学习到…

安卓studio安装(从安装到配置到helloworld)

安卓studio安装 2024.3.11官网的版本&#xff08;有些翻墙步骤下载东西也解决了&#xff09; 这次写的略有草率&#xff0c;后面会更新布局的&#xff0c;因为截图量太大了&#xff0c;有需要的小伙伴可以试着接受一下哈哈哈哈 !(https://gitee.com/jiuzheyangbawjf/img/raw/ma…