[Angular 基础] - 表单:模板驱动表单
之前的笔记:
-
[Angular 基础] - routing 路由(上)
-
[Angular 基础] - routing 路由(下)
-
[Angular 基础] - Observable
Angular 内置两种表单的支持,这篇写的就是第一种,即模板驱动表单 (Template-Driven Form)
Template-Driven Form 的实现比较简单,Angular 自身会生成和提供对应的表单控制和管理状态,对于开发者来说,实现相对而言更加简单,因此更加适合简单的表单实现
做一个平行对比,React 有 uncontrolled form & controlled form,Angular 有一个 Template-Driven Form 和 Reactive form,虽然这么看起来两个实现似乎挺像的,不过本质上还是有些区别的
对于 React 的 uncontrolled form,它本质上是让 HTML 去进行处理,然后可以通过 refs 去获得 HTML 的值,React 不会对其进行太多的干预;对比 Angular 的 Template-Driven Form,Angular 本身通过 绑定 对状态进行了接管
开始
下面是一个表单:
<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 id="user-data">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" class="form-control" />
</div>
<button class="btn btn-default" type="button">
Suggest an Username
</button>
<div class="form-group">
<label for="email">Mail</label>
<input type="email" id="email" class="form-control" />
</div>
</div>
<div class="form-group">
<label for="secret">Secret Questions</label>
<select id="secret" class="form-control">
<option value="pet">Your first Pet?</option>
<option value="teacher">Your first teacher?</option>
</select>
</div>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
</div>
</div>
</div>
表单渲染如下:
相别于直接使用 JS+HTML 的方式实现表单,使用 Angular 这里没有添加 action
这一 action,这一步会让 Angular 去安排。
这一点在之前也提过,需要实现这一点,也需要在 app.module.ts
中导入 FormsMorule
绑定控制
这里主要使用 ngModel
和 name=""
绑定所有输入:
<label for="username">Username</label>
<input type="text" id="username" class="form-control" ngModel name="username" />
<label for="email">Mail</label>
<input type="email" id="email" class="form-control" ngModel name="email" />
<label for="secret">Secret Questions</label>
<select id="secret" class="form-control" ngModel name="secret">
<option value="pet">Your first Pet?</option>
<option value="teacher">Your first teacher?</option>
</select>
我这里省略了其他部分的代码,只留下了对应的 label
和 input
⚠️:name
是一个 HTML attribute,这个实现本质上跟 angular 没什么关系。但是在 Template-Driven Form 中它的作用非常的重要。它会用来创建一个 FormControl
的实例对象,对 ngModel
进行自动绑定,这也是为什么这里的 ngModel
使用语法和 2 way binding 里的不一样。可以说,没有 name
,那么 Angular 就无法正确识别对应的 ``FormControl`,也无法接管对应的 onChange 和 validation
提交表单 AKA 获取提交的值
这里提交表单的做法不是之前实现的那样,在 submit
button 上绑定 onclick 事件,而是使用 angular 提供的 directive 去实现——这也是为什么这种实现叫 Template-Driven Form,代码修改如下:
-
V 层
<form (ngSubmit)="onSubmit(f)" #f="ngForm"> <!-- 其余不变 --> </form>
-
VM 层
onSubmit(form: NgForm) { console.log('submitted'); console.log(form); }
输出结果如下:
注意这里的 local reference 用法,它绑定了一个特殊的 ngForm
directive,因此传到 onSubmit
中的值就是 NgForm
。默认情况下它的值是当前 HTML 元素类型,表单对应的元素类型是 HTMLFormElement
通过 NgForm
就可以轻松的获取当前表单的值、控制(即 FormControl
)、验证等。这些值可以杯统称为当前表单的 状态
验证
这里主要是通过 angular 提供的验证搭配 HTML5 的验证进行实现,代码修改如下:
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
ngModel
name="username"
required
/>
</div>
<input
type="email"
id="email"
class="form-control"
ngModel
name="email"
required
email
/>
这里主要用了 angular 提供的 required
和 email
这两个 validators。这个时候表单还是可以继续提交的,输出结果如下:
可以看到,当前 NgForm
的 submitted
是 true
,invalid
也是 true
这个时候就可以利用一下这个 invalid
属性:
<button class="btn btn-primary" type="submit" [disabled]="f.invalid">
Submit
</button>
这样可以阻止用户在表单验证未通过的情况下,点击 submit
事件:
修改报错样式
这里主要通过 CSS 实现,修改代码如下:
input.ng-invalid.ng-touched,
select.ng-invalid.ng-touched {
border: 1px solid red;
}
实现效果如下:
这里也是通过 angular 提供的 class
进行的实现。angular 会在对应的元素上添加对应的状态,touched
指的是用户是否触碰(focus)过当前元素。
报错信息
这里使用 ngIf
搭配对应的 directive 进行报错信息的渲染,代码修改如下:
<div class="form-group">
<label for="email">Mail</label>
<input
type="email"
id="email"
class="form-control"
ngModel
name="email"
autocomplete="off"
required
email
#email="ngModel"
/>
<span class="help-block" *ngIf="email.invalid && email.touched"
>Please enter a valid email!</span
>
</div>
效果如下:
⚠️:这里依旧使用 local reference+绑定 directive 的方法实现,不过这里绑定的是 ngModel
而不是 ``NgForm`
设置默认值
这里主要就是设置一下默认值,使用的是 property binding,代码如下:
<div class="form-group">
<label for="secret">Secret Questions</label>
<select id="secret" class="form-control" [ngModel]="'pet'" name="secret">
<option value="pet">Your first Pet?</option>
<option value="teacher">Your first teacher?</option>
</select>
</div>
实现效果如下:
⚠️:这里使用的是 property binding,即 [ngModel]
提供默认值,而不是提供 2-way binding——在有需求的情况下还是可以使用 2-way binding 的。使用 property binding 并不会修改 VM 层中的数据。
👀:也可以在 VM 层实现一个变量,而不是直接将值写到 V 层中
组合 form control
这可以将一些数据组合在一起,比如说地址的组合通常为省+市+具体地址+邮编才能组合成一个完整的地址,实现方法如下:
<div id="user-data" ngModelGroup="userData">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
ngModel
name="username"
required
/>
</div>
<button class="btn btn-default" type="button">Suggest an Username</button>
<div class="form-group">
<label for="email">Mail</label>
<input
type="email"
id="email"
class="form-control"
ngModel
name="email"
autocomplete="off"
required
email
#email="ngModel"
/>
<span class="help-block" *ngIf="email.invalid && email.touched"
>Please enter a valid email!</span
>
</div>
</div>
效果如下:
在这里插入图片描述
⚠️:这里的语法为 ngModelGroup="string-value"
👀:这里同样可以添加 local reference,如: #userData="ngModelGroup"
这样就可以对整个 group 进行提示,如:
<p *ngIf="userData.invalid && userData.touched">User Data is invalid!</p>
这个报错信息只有在用户提供了正确的用户名和邮箱之后才会消失:
radio button
这里没有什么特别的地方,不过可以搭配 ngFor
让整个实现变得简单一些。
假设 VM 层有一个数组包含 [male, female]
这两个数据,V 层渲染如下:
<div class="form-group" *ngFor="let gender of genders">
<label for=""></label>
<input type="radio" name="gender" ngModel [value]="gender" />
{{ gender }}
</div>
结果如下:
使用 @ViewChild
获取表单
这里补充另一种可以获取表单的方式,就是通过 @ViewChild
进行绑定
@ViewChild
的使用在之前的笔记提过: [Angular 基础] - 视图封装 & 局部引用 & 父子组件中内容传递
使用方法如下:
export class AppComponent {
@ViewChild('f') signupForm: NgForm;
suggestUserName() {
const suggestedName = 'Superuser';
}
// onSubmit(form: NgForm) {
// console.log('submitted');
// console.log(form);
// }
onSubmit(f: NgForm) {
console.log(this.signupForm);
}
}
输出结果如下:
使用 @ViewChild
使用 @ViewChild
可以提供更加灵活的使用,比如说 UI 上的一个 Suggest an Username
按钮,我想要点一下这里就生成一个用户名时,就可以使用 @ViewChild
去实现。
这里会提供两种写法,根据业务需求去使用,具体使用方式在注释里:
-
VM 层
suggestUserName() { const suggestedName = 'Superuser'; // 这里必须提供所有的值,否则会报错,因此就有可能会重写所有值的问题 this.signupForm.setValue({ userData: { username: suggestedName, email: '', }, secret: 'pet', gender: 'male', }); // 这里就是打个补丁,不会重写所有的值 this.signupForm.form.patchValue({ userData: { username: suggestedName, }, }); }
这里直接使用
setValue
去设置一个与提交格式对应的表单 -
V 层
<button class="btn btn-default" type="button" (click)="suggestUserName()"> Suggest an Username </button>
这里绑定 click 事件
这里的效果是重写所有值,动图如下:
⚠️:还是可以使用 event handler+2-way data binding 去实现的,只是对于 Template-Driven Form 来说,这种实现方法最方便
使用表单数据
这里还是需要创建一个新的对象用来 match 提交的表单数据,从而完成渲染,实现如下:
-
V 层
<div class="row" *ngIf="f.submitted"> <div class="col-xs-12"> <h3>Your Data</h3> <p>Username: {{ user.username }}</p> <p>Email: {{ user.email }}</p> <p>Secret Question: {{ user.secret }}</p> <p>Gender: {{ user.gender }}</p> </div> </div>
-
VM 层
onSubmit(f: NgForm) { this.user.username = f.value.userData.username; this.user.email = f.value.userData.email; this.user.secret = f.value.secret; this.user.gender = f.value.gender; }
渲染如下:
这里的逻辑是只有在提交的时候会对数据进行赋值,所以直接检查 f.submitted
也不会造成修改表单数据就让下面的数据区域重新渲染的结果。
重置表单
这里就一行代码:
this.signupForm.reset();
效果如下:
⚠️:reset
中也可以传值,用法和 setValue
类似
reference
-
Validators
angular 的所有 validators