本节我们完成最核心、最简单的更新功能。
## MockApi
更新功能需要调用后台更新班级的接口,该接口信息如下:
```
PUT /clazz/{id}
```
| **类型Type** | **名称Name** | **描述Description** | **类型Schema** |
| :----------- | :----------- | :------------------ | :----------------------------------------------------------- |
| Body | | 班级 | `{name: string, teacher: {id: number}}` |
| Response | 成功 | Status Code: 204 | `{id: number, name: string, teacher: {id: number, name: string}}` |
依此建立MockApi信息:
```typescript
+++ b/first-app/src/app/mock-api/clazz.mock.api.ts
@@ -92,6 +92,22 @@ export class ClazzMockApi implements MockApiInterface {
}
} as Clazz;
}
+ },
+ {
+ method: 'PUT',
+ url: `/clazz/(\\d+)`,
+ result: (urlMatches: string[], options: RequestOptions) => {
+ const id = +urlMatches[1];
+ const clazz = options.body as Clazz;
+ return {
+ id,
+ name: clazz.name,
+ teacher: {
+ id: clazz.teacher.id,
+ name: randomString('测试教师')
+ } as Teacher
+ } as Clazz;
+ }
}
];
}
```
## onSubmit
先V层:
```html
+++ b/first-app/src/app/clazz/edit/edit.component.html
@@ -1,4 +1,4 @@
-<form class="container-sm">
+<form class="container-sm" (ngSubmit)="onSubmit()">
```
在C层:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -43,4 +43,8 @@ export class EditComponent implements OnInit {
console.log('接收到了选择的teacherId', $event);
this.teacherId = $event;
}
+
+ onSubmit(): void {
+ console.log('点击了提交按钮');
+ }
}
```
测试:
![image-20210402164059299](https://img.kancloud.cn/e5/c5/e5c501fda786a5000dfb9d2b15c57573_1352x388.png)
此时点击保存按钮时,页面会执行刷新操作。这是由于我们未对V层中的`form`元素加以控制的原因。在默认情况下,`form`中的提交按钮并点击时,该表单会依照其`action`属性的值发送数据。在默认情况下`action`的值为空,则当点击保存按钮时,数据会发送至当前页面,即在当前页的情况下重新打开当前页,所以点击保存按钮时就像是刷新了一样。
解决的方法有两种,第一种方法我们已经学习过:加入`FormsModule`。`FormsModule`会禁止表单的默认提交行为,此时再点击保存按钮,则将触发`onSubmit()`方法:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.spec.ts
@@ -17,7 +17,8 @@ describe('EditComponent', () => {
imports: [
MockApiTestingModule,
ReactiveFormsModule,
- RouterTestingModule
+ RouterTestingModule,
+ FormsModule
]
```
![image-20210402165008228](https://img.kancloud.cn/94/78/947871d26b497a61f4a75c9fdbd81ba5_530x64.png)
第二种方案是使用`FormGroup`。
在继续以前,我们删除刚刚引入的`FormModule`:
```typescript
@@ -17,8 +17,7 @@ describe('EditComponent', () => {
imports: [
MockApiTestingModule,
ReactiveFormsModule,
- RouterTestingModule,
- FormsModule
+ RouterTestingModule
]
})
.compileComponents();
```
## FormGroup
与`FormControl`类似Angular也提供了用于绑定`form` 元素的`FormGroup`。Group译为**组**,在使用过程中,会在`FormGroup`中加入多个`FormControl`,以使多个`FormControl`组合在一起。`FormGroup`与其它的对象的初始化方式完全相同:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {HttpClient} from '@angular/common/http';
import {Clazz} from '../../entity/clazz';
-import {FormControl, Validators} from '@angular/forms';
+import {FormControl, FormGroup, Validators} from '@angular/forms';
@Component({
selector: 'app-edit',
@@ -15,6 +15,10 @@ export class EditComponent implements OnInit {
*/
nameFormControl = new FormControl('', Validators.required);
teacherId: number | undefined;
+ /**
+ * 表单组,用于存放多个formControl
+ */
+ formGroup = new FormGroup({});
```
初始过程中`FormGroup`接收一个对象做为参数,在该对象中可以定义多个`FormControl`,比如将`nameFormControl`添加到`FormGroup`中。
```typescript
- formGroup = new FormGroup({});
+ formGroup = new FormGroup({
+ name: this.nameFormControl
+ });
```
### 绑定FormGroup
在V层中对FormGroup的绑定也与FormControl相似:
```html
+++ b/first-app/src/app/clazz/edit/edit.component.html
@@ -1,4 +1,4 @@
-<form class="container-sm" (ngSubmit)="onSubmit()">
+<form class="container-sm" (ngSubmit)="onSubmit()" [formGroup]="formGroup">
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">名称</label>
<div class="col-sm-10">
```
此时即使我们没有引入`FormsModule`,在点击提交按钮时,`Form`也不会刷新了:
![image-20210402165008228](https://img.kancloud.cn/94/78/947871d26b497a61f4a75c9fdbd81ba5_530x64.png)
## 校验
使用`FormGroup`会带来很多好处,比如某个表单有10个字段,每个字段的验证方式都不一样。如果不使用`FormGroup`,则在处理是否禁用保存按钮时,代码则会又臭又长:
```html
*ngIf="a === undefined || b === undefined || c === '' || d < 10"
```
而有了`FormGroup`以后,无论有多少个验证的字段,只要有一个字段不符合条件,则都将使`FormGroup`的`invalid` 值为`true`,所以我们利用该特点可以轻构的定义保存按钮的`disabled`属性:
```html
<div class="col-sm-10 offset-2">
- <button class="btn btn-primary">保存
+ <button class="btn btn-primary" [disabled]="formGroup.invalid">保存
</button>
</div>
```
此时当班级名称不符合规则时,则保存按钮处于不可点击状态:
![image-20210402165954569](https://img.kancloud.cn/83/a1/83a13d0791bc16e9f0250f81aa33c2ea_1136x402.png)
## 完成功能
最后,完成班级更新功能。在更新时需要将更新的班级ID加入到请求的URL中,将班级名称、对应的班主任ID传入请求body中。
### 缓存ID
在C层中可以很简单的获取到要更新的班级ID,比如`loadById()`的请求参数则代表了当前要更新的ID。所以可以在C层中建立一个属性来存储这个ID:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -15,6 +15,10 @@ export class EditComponent implements OnInit {
*/
nameFormControl = new FormControl('', Validators.required);
teacherId: number | undefined;
+ /**
+ * 要更新的班级ID
+ */
+ clazzId: number | undefined;
/**
* 表单组,用于存放多个formControl
*/
@@ -37,6 +41,7 @@ export class EditComponent implements OnInit {
*/
loadById(id: number): void {
console.log('loadById');
+ this.clazzId = id;
this.httpClient.get<Clazz>('/clazz/' + id.toString())
.subscribe(clazz => {
console.log('接收到了clazz', clazz);
```
### 更新
缓存了班级ID后,在`onSubmit()`方法中便可以获取到更新班级需要的几个核心因素了:
```typescript
onSubmit(): void {
console.log('点击了提交按钮');
+ const clazzId = this.clazzId;
+ const name = this.nameFormControl.value; 👈
+ const teacherId = this.teacherId;
}
```
`FormControl`上有个`value`属性,该属性的值为`FromControl`的当前值 👈
最后,使上述变量进行接拼并发起后台请求:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -3,6 +3,7 @@ import {ActivatedRoute} from '@angular/router';
import {HttpClient} from '@angular/common/http';
import {Clazz} from '../../entity/clazz';
import {FormControl, FormGroup, Validators} from '@angular/forms';
+import {Teacher} from '../../entity/teacher';
@Component({
selector: 'app-edit',
@@ -60,5 +61,13 @@ export class EditComponent implements OnInit {
const clazzId = this.clazzId;
const name = this.nameFormControl.value;
const teacherId = this.teacherId;
+ const clazz = new Clazz({
+ name,
+ teacher: {id: teacherId} as Teacher
+ });
+ this.httpClient.put<Clazz>(`/clazz/${clazzId}`, clazz)
+ .subscribe(
+ () => console.log('更新成功'), 👈
+ error => console.log(error));
}
}
```
后台在更新成功后将返回更新后的班级信息,如果我们在后续的代码中需要这个更新后的班级信息,则可以将此处改写为` (data) => console.log('更新成功', data); 后续使用data信息`;如果在后续的代码中不需要这个更新后的信息,则可以在参数中省咯`data`而直接书写为`()` 👈。
这种在回调函数的方法中可写可不写的参数的用法,也是JavaScript一个特性。我们在书写MockApi时也使用到了这个特性,你注意到了吗?
![image-20210406085228202](https://img.kancloud.cn/60/2d/602d4114ec636f2964d3f79b118bf58c_834x140.png)
## 代码重构
刚刚的代码虽然成功的完成了功能,但各个函数之间的链接还有不小的提升空间。比如我们在`loadById()`中缓存了班级ID,然后在`onSubmit()`方法中使用到了这个ID。如果我们不在`loadById()`中写上一些注释,后续的成员便不清楚在当前方法中为什么要有这么一行代码。
```typescript
loadById(id: number): void {
console.log('loadById');
this.clazzId = id; 👈
this.httpClient.get<Clazz>('/clazz/' + id.toString())
.subscribe(clazz => {
console.log('接收到了clazz', clazz);
this.nameFormControl.patchValue(clazz.name);
this.teacherId = clazz.teacher.id;
}, error => console.log(error));
}
```
在团队开发中,我们最不愿意看到的代码难懂且没有注释的代码;第二个不愿意看到虽有注释但逻辑难懂的代码;最希望看到的是有注释且逻辑易懂的代码;如果在有注释和易懂间只能选择一个,那么更愿意看到易懂的代码。
在更新功能中,充分的利用`FormGroup`能使代码看起来更易懂。`FormGroup`对应着V层的表单,表示更新的班级信息,在`loadById()`对`FormGroup`进行操作便起到告诉队友该项信息将来用于更新表单;`teacherId`当然也是如此。带着此思想我们更新代码如下:
```typescript
formGroup = new FormGroup({
- name: this.nameFormControl
+ id: new FormControl(),
+ name: this.nameFormControl,
+ teacherId: new FormControl()
});
```
如此以下`formGroup`中便存储了更新班级时所需要的所有数据。此外我们还可以为其设置校验属性:
```typescript
formGroup = new FormGroup({
- id: new FormControl(),
+ id: new FormControl(null, Validators.required),
name: this.nameFormControl,
- teacherId: new FormControl()
+ teacherId: new FormControl(null, Validators.required)
});
```
此时一旦我们在开发时不小心忘掉了设置要更新班级的ID,则会使得**保存**铵钮为不可用状态。
有了`FormGroup`后,便可以将更新的班级`ID`以及选择的`teacherId`全部放到`FormGroup`中了:
```typescript
- /**
- * 要更新的班级ID
- */
- clazzId: number | undefined;
/**
* 表单组,用于存放多个formControl
*/
@@ -44,25 +40,27 @@ export class EditComponent implements OnInit {
*/
loadById(id: number): void {
console.log('loadById');
- this.clazzId = id;
+ this.formGroup.get('id')?.setValue(id); 👈
this.httpClient.get<Clazz>('/clazz/' + id.toString())
.subscribe(clazz => {
console.log('接收到了clazz', clazz);
this.nameFormControl.patchValue(clazz.name);
this.teacherId = clazz.teacher.id;
+ this.formGroup.get('teacherId')?.setValue(clazz.teacher.id); 👈
}, error => console.log(error));
}
onTeacherChange($event: number): void {
console.log('接收到了选择的teacherId', $event);
this.teacherId = $event;
+ this.formGroup.get('teacherId')?.setValue($event); 👈
}
onSubmit(): void {
console.log('点击了提交按钮');
- const clazzId = this.clazzId;
+ const clazzId = this.formGroup.get('id')?.value; 👈
const name = this.nameFormControl.value;
- const teacherId = this.teacherId;
+ const teacherId = this.formGroup.get('teacherId')?.value; 👈
const clazz = new Clazz({
name,
teacher: {id: teacherId} as Teacher
```
`FormGroup`中的`get()`方法可以根据定义时的属性来获取其中的`FomControl`。同于该方法的返回值可能为`null`,所以加入`?`来规避相应的语法错误。👈
测试功能正常:
![image-20210406085228202](https://img.kancloud.cn/60/2d/602d4114ec636f2964d3f79b118bf58c_834x140.png)
虽然说是看起来成功了吧,但在控制台中看不到传递给后台的数据总是让人感觉不踏实。为此在MOAKAPI中打印两个数据:
```typescript
+++ b/first-app/src/app/mock-api/clazz.mock.api.ts
@@ -99,6 +99,8 @@ export class ClazzMockApi implements MockApiInterface {
result: (urlMatches: string[], options: RequestOptions) => {
const id = +urlMatches[1];
const clazz = options.body as Clazz;
+ console.log('接收到了id', id);
+ console.log('接收到的clazz', clazz);
return {
id,
name: clazz.name,
```
最后在控制台中看到了更新时提交给后台的数据,这下心里总算是踏实了。
![image-20210406094813317](https://img.kancloud.cn/81/eb/81eb9fb5ab6a314dd7f1bf87a4fa4736_1090x280.png)
## 进阶
我们刚刚使用`FormGroup`移除了组件中`classId`属性,而在处理`teacherId`时,我们仍然不能够删除组件的中`teacherId`。如果能把`teacherId`也移除掉的话`FormGroup`便能够完全地发挥出其功能了。
此时便不得不停下脚步思索:同样是编辑,为什么我们并没有创建一个`name`属性,却偏偏创建了`teacherId`属性了。
这是因为在我们V层中使用了子组件`app-klass-select`,该组件的`@Input()`必须接收了一个`id`,此`id`便是当前组件下的`teacherId`属性。所以如果子组件`app-klass-select`可以向`input`元素一样支持`[formControl]`属性就可以删除`teacherId` ,转而使用`FromControl`代替了。
我们在下节中将讲述如何自定义一个适用`FormControl`的组件。
## 本节作业
1. 在MockApi添加`console.log()`,充分了解在进行http请求时,`result`属性对应的回调函数的参数值都是什么。
2. 请说明为什么在MockApi中有的`result`对应的回调函数上有参数,有的却没有。两种写法有何不同,什么时候应该带参数,什么时候又可以不带参数?
3. 在组件更新成功后,在控制台打印更新成功后的返回值。请思索:如果不需要打印成功后的返回值,更新方法还可以怎么写?
| 名称 | 链接 |
| -------------- | ------------------------------------------------------------ |
| 把表单控件分组 | [https://angular.cn/guide/reactive-forms#grouping-form-controls](https://angular.cn/guide/reactive-forms#grouping-form-controls) |
| 验证表单输入 | [https://angular.cn/guide/reactive-forms#validating-form-input](https://angular.cn/guide/reactive-forms#validating-form-input) |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.4.4.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.4.4.zip) |
##
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结