基本的功能完成后,本节我们使用`ng s`进行集成测试,在启动测试时将报一个错误:
```bash
Error: src/app/student/edit/edit.component.ts:46:32 - error TS2345: Argument of type 'number | undefined' is not assignable to parameter of type 'number'.
Type 'undefined' is not assignable to type 'number'.
46 this.studentService.update(id, {
~~
```
`ng s`的启动的时候检查出来一个错误,这个错误是在说:我们在组件初始化的时候将`id`的类型定义成了`number | undefined`,而最终这个`id`却做为参数传给`this.studentService.update`。由于`studentService.update`方法中接收的第一参数类型被规定为`number`。所以最后实质上相当于把`'number | undefined'`类型赋值给了`number`类型,而这是不允许的。
为此我们应当如下对`onSubmit`方法进行改进:
```typescript
+++ b/first-app/src/app/student/edit/edit.component.ts
@@ -39,11 +39,12 @@ export class EditComponent implements OnInit {
* @param id id
* @param formGroup 表单组
*/
- onSubmit(id: number, formGroup: FormGroup): void {
+ onSubmit(id: number | undefined①, formGroup: FormGroup): void {
const formValue = formGroup.value as { name: string, phone: string, email: string, clazzId: number };
Assert.isString(formValue.name, formValue.phone, formValue.email, '类型必须为字符串');
Assert.isNumber(formValue.clazzId, '类型必须为number');
- this.studentService.update(id, {
+ Assert.isNumber(id, 'id类型必须为number'); ②
+ this.studentService.update(id as number③, {
name: formValue.name,
email: formValue.email,
phone: formValue.phone,
```
①处对类型进行统一,既然初始化的时候是`number | undefined`,那么我们这保持不变;③`studentService.update`既然只接收`number`,那么在传值前我们可以使用`as`关键字来做类型指定;在类型指定前为了保证这样的指定是没有风险的,在进行指定前对`id`进行断言。
## 增加路由
在`student`路由文件中增加编辑组件路由:
```typescript
+++ b/first-app/src/app/student/student-routing.module.ts
@@ -2,6 +2,7 @@ import {NgModule} from '@angular/core';
import {Route, RouterModule} from '@angular/router';
import {StudentComponent} from './student.component';
import {AddComponent} from './add/add.component';
+import {EditComponent} from './edit/edit.component';
const routes = [
{
@@ -11,6 +12,9 @@ const routes = [
{
path: 'add',
component: AddComponent
+ }, {
+ path: 'edit/:id',
+ component: EditComponent
}
] as Route[];
```
此时点击编辑按钮后将进行正常的跳转,正常显示组件。
![image-20210611142847709](https://img.kancloud.cn/44/ad/44ada7b2f84cc099b6db45f1dec98bd4_2018x760.png)
编辑的学生内容虽然可以正常显示了,但保存按钮却是disabled状态。这里由于我们在编辑组件的学号字段上加入了`numberNotExist`验证器的原因:
```typescript
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist() 👈),
phone: new FormControl('', YzValidators.phone),
email: new FormControl(),
clazzId: new FormControl(null, Validators.required)
});
```
在进行学生编辑组件开发时,我们直接复制了新增学生的代码,在复制代码的同时其验证逻辑也跟着复制了过来。由于学生编辑组件并不涉及到学号的更新,所以学号上原本就不应该设置验证器。
在设置`numberNotExist`情况下,后台会以当前学号做为条件查询后台是否存在有当前学号的记录。这当然是存在的,该记录就是当前正在编辑的学生。
删除学号上的验证器后保存按钮生效:
```typescript
+++ b/first-app/src/app/student/edit/edit.component.ts
@@ -21,7 +21,7 @@ export class EditComponent implements OnInit {
console.log(this.activatedRoute);
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
- number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()),
+ number: new FormControl(''),
phone: new FormControl('', YzValidators.phone),
email: new FormControl(),
clazzId: new FormControl(null, Validators.required)
```
![image-20210611144024063](https://img.kancloud.cn/be/02/be0220d259253fd2d08e6123ba6627e6_2152x764.png)
## 增加跳转
最后增加保存成功后的跳转:
```typescript
+++ b/first-app/src/app/student/edit/edit.component.ts
@@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {YzValidators} from '../../yz-validators';
import {YzAsyncValidators} from '../../yz-async-validators';
-import {ActivatedRoute} from '@angular/router';
+import {ActivatedRoute, Router} from '@angular/router';
import {StudentService} from '../../service/student.service';
import {Assert} from '@yunzhi/ng-mock-api';
@@ -17,7 +17,8 @@ export class EditComponent implements OnInit {
constructor(private yzAsyncValidators: YzAsyncValidators,
private activatedRoute: ActivatedRoute,
- private studentService: StudentService) {
+ private studentService: StudentService,
+ private router: Router) {
console.log(this.activatedRoute);
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
@@ -50,7 +51,7 @@ export class EditComponent implements OnInit {
phone: formValue.phone,
clazz: {id: formValue.clazzId}
}).subscribe(() => {
- console.log('更新成功');
+ this.router.navigate(['../../'], {relativeTo: this.activatedRoute});
});
}
```
集成测试完成。
## 参数快照
当前我们在获取路由中的参数ID时,使用的由`snapshot`上读取属性的方法。`snapshot`的中文译为快照,其中的照可以理解为照片。
为了把它称为`snapshot`呢?是由于该数据一旦生成就不会改变,这就像照片一样,一旦被拍了出来照片上的情景便会被留存下来。
为了更好的理解它,我们对当前模块中的路由做以下改动:
```typescript
+++ b/first-app/src/app/student/student-routing.module.ts
@@ -7,14 +7,16 @@ import {EditComponent} from './edit/edit.component';
const routes = [
{
path: '',
- component: StudentComponent
- },
- {
- path: 'add',
- component: AddComponent
- }, {
- path: 'edit/:id',
- component: EditComponent
+ component: StudentComponent,
+ children: [
+ {
+ path: 'add',
+ component: AddComponent
+ }, {
+ path: 'edit/:id',
+ component: EditComponent
+ }
+ ]
}
] as Route[];
```
即将添加组件、编辑组件放到列表组件的children中。然后对列表组件的V层中增加一个路由出口:
```html
+++ b/first-app/src/app/student/student.component.html
@@ -1,3 +1,4 @@
+<router-outlet></router-outlet>
<div class="row">
<div class="col-12 text-right">
<a class="btn btn-primary mr-2" routerLink="./add"><i class="fas fa-plus"></i>
新增</a>
```
此时当我们在学生列表中点击编辑按钮时,将得到如下界面:
![image-20210611151447615](https://img.kancloud.cn/d2/e5/d2e551f02fc9be0038bd2fc6f36b603f_2162x1342.png)
貌似一切都再正常不过,但如果此时我们再次添加其它学生对应的编辑按钮,则会发现除了浏览器`url`产生了变化以外,其它一切都没有变化。
![image-20210611151622126](https://img.kancloud.cn/8f/75/8f756ff34d6f8d6d7fd1250803ee724a_2154x312.png)
比如点击第一个编辑按钮时,浏览器的地址为:`http://localhost:4200/student/edit/1`,当击第二个编辑按钮时,浏览器的地址为更为:`http://localhost:4200/student/edit/2`。除此以外,编辑组件显示的学生并没有任何变化。
这是由于Angular为了性能提升,在地址切换时,将按以下规律来选择是否构造新组件:
当浏览器地址由`http://localhost:4200/student/edit/1`变更为`http://localhost:4200/student/edit/2`时,Angular发现变更前后的两个地址对应同一个路由,该路由指定了学生编辑组件,则Angular将选择复用的以前的组件。
也就是说浏览器的地址虽然变更了,但组件还是那个组件,并没有发生任何变化。
而获取编辑的学生信息的代码位于学生编辑组件的`ngOnInit()`方法中,该方法仅会在组件实例化的时候被调用1次。所以上述事件的整个过程大体如下:
➊ 浏览器地址为`http://localhost:4200/student`时,学生列表组件不存在,则初始化学生列表组件。该组件中的`ngOnInit`方法被自动调用1次。
➋ 第1次点击编辑按钮时,浏览器地址由`http://localhost:4200/student`跳转至`http://localhost:4200/student/edit/1`,学生编辑组件不存在,则初始化学生编辑组件。该组件中的`ngOnInit`方法被自动调用1次。
➌ 第2次点击编辑按钮时,浏览器地址由`http://localhost:4200/student/edit/1`变更为`http://localhost:4200/student/edit/2`,学生编辑组件已存在,什么也不做。
当学习一个伟大的框架时,尽量不要怀疑框架的思想。比如我们前面遇到了问题,首先想的是应该如何改变自己的思想,让自己的思想与Angular相一致。在这种情况下,再去思索解决问题的方法。
Angular的思想是:如果我们想在路由改变时进行一些操作,则应该去订阅路由信息。因为一旦我们订阅了路由信息,路由发生变化时则会主通地通知我们。
下节中我们将使用**可订阅的路由参数**来取替**路由参数快照**来解决当前问题。
## 资源链接
| 链接 | 名称 |
| ------------------------------------------------------------ | -------- |
| [https://github.com/mengyunzhi/angular11-guild/archive/step7.7.4.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.7.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 发布部署
- 第九章 总结