当前位置: 首页 > news >正文

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

[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><inputtype="text"id="username"class="form-control"formControlName="username"/></div><div class="form-group"><label for="email">email</label><inputtype="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:

<pclass="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><inputtype="text"id="username"class="form-control"formControlName="username"/><pclass="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><inputtype="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><divclass="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 层修改代码如下:

<pclass="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

相关文章:

  • 【Java】获取手机文件名称补充
  • NLP:bert下载与使用
  • 使用 ChatGPT 写高考作文
  • Spring Boot Configuration Processor使用
  • java学习(集合)
  • 开发跨平台 App 推荐 React Native 还是 Flutter?
  • Linux:kubernetes(k8s)Deployment的操作(12)
  • 小米消金加大科技赋能,打造特色金融服务体系
  • 6.S081的Lab学习——Lab1: Xv6 and Unix utilities
  • 若依 启动!(手把手配置好Java若依,运行起来)
  • docker 部署prometheus+grafana
  • FPGA TestBench编写学习
  • 如果需要在Log4j中记录特定的异常信息,应该如何实现?如何动态地更改Log4j的日志级别?
  • C#游戏开发
  • torch 手动计算BatchNorm, 手动计算LayerNorm, 手动计算GroupNorm, 手动计算InstanceNorm
  • IE9 : DOM Exception: INVALID_CHARACTER_ERR (5)
  • SegmentFault for Android 3.0 发布
  • 【从零开始安装kubernetes-1.7.3】2.flannel、docker以及Harbor的配置以及作用
  • 11111111
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • Github访问慢解决办法
  • isset在php5.6-和php7.0+的一些差异
  • Java 最常见的 200+ 面试题:面试必备
  • JavaScript设计模式之工厂模式
  • jquery ajax学习笔记
  • underscore源码剖析之整体架构
  • Web设计流程优化:网页效果图设计新思路
  • 当SetTimeout遇到了字符串
  • 海量大数据大屏分析展示一步到位:DataWorks数据服务+MaxCompute Lightning对接DataV最佳实践...
  • 入手阿里云新服务器的部署NODE
  • 在Unity中实现一个简单的消息管理器
  • Hibernate主键生成策略及选择
  • ​LeetCode解法汇总2808. 使循环数组所有元素相等的最少秒数
  • ​插件化DPI在商用WIFI中的价值
  • ​力扣解法汇总946-验证栈序列
  • #{}和${}的区别?
  • #ifdef 的技巧用法
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • #我与Java虚拟机的故事#连载03:面试过的百度,滴滴,快手都问了这些问题
  • (1)常见O(n^2)排序算法解析
  • (30)数组元素和与数字和的绝对差
  • (arch)linux 转换文件编码格式
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (LeetCode) T14. Longest Common Prefix
  • (保姆级教程)Mysql中索引、触发器、存储过程、存储函数的概念、作用,以及如何使用索引、存储过程,代码操作演示
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (转)Unity3DUnity3D在android下调试
  • .jks文件(JAVA KeyStore)
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .NET DataGridView数据绑定说明
  • .NET 使用 XPath 来读写 XML 文件
  • .NET/C# 将一个命令行参数字符串转换为命令行参数数组 args
  • @NestedConfigurationProperty 注解用法
  • [ 蓝桥杯Web真题 ]-布局切换
  • [04]Web前端进阶—JS伪数组