本节我们完成最核心、最简单的更新功能。 ## 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) | ##