在编辑班级时,我们希望有如下的效果:
![](https://img.kancloud.cn/27/62/2762a482e7c83eabb862baa5767731ef_579x343.gif)
即引用教师组件后,教师组件可以根据传入的教师信息自动选中某位教师,这就涉及到了组件的**Input 输入**。
# 静态的输入值
所谓静态的输入值是指:一旦将数据输入至组件,该值就不再变更或是无需考虑其变更,此种情况的实现最为简单。我们仍然启动前后台,并在班级编辑组件中启用选择教师组件。
klass/edit/edit.component.html
```
<h3>编辑班级</h3>
<form (ngSubmit)="onSubmit()" [formGroup]="formGroup">
<label for="name">名称:<input id="name" type="text" formControlName="name"/></label>
<label for="teacherId">教师:<app-teacher-select id="teacherId"></app-teacher-select></label>
<button>更新</button>
</form>
```
![](https://img.kancloud.cn/f3/5c/f35c5bc15fdc20d78e66b514f386c359_394x313.png)
## @Input
我们使用了`@Input()`来标识了某个属性,进而表示该属性为**输入型属性**,它的作用是接收组件的传入值。
klass/teacher-select/teacher-select.component.ts
```
@Output() selected = new EventEmitter<Teacher>();
@Input()➊ teacher: { id: number };➋
constructor(private httpClient: HttpClient) {
}
```
* ➊ 标识teacher为**输入型属性**
* ➋ 标识接收的teacher类型为对象,该对象中必须存在`id`属性,且该属性的类型为`number`
由于我们只需要根据关键字`id`来判断该组件具体应该选中哪个教师,所以在数据类型上只规定`{ id: number }`,当然你也可以规定传入的对象类型必须是一个`教师`,比如:`teacher: Teacher`。
## 选中这个教师
有了传入的教师ID后,我们便可以根据这个ID来确定应该选中哪个选项了。在上个小节中我们通过发现:当`select`中的某个`option`被选中时,`select`对就在的`fomControl`的值就会对应被设置为哪一个;在前面的小节中,我们还学习了`FormControl`具有双向数据绑定的特质。也就是说:
* [ ] 当`option`被选中时,数据将绑定到`FormControl`值。
* [ ] 反过来:当数据被绑定到`FormControl`的值时,某个对应的`option`则会自动被绑定。
所以要想实现选中某个教师的功能,我们告组件当前的`select`对应绑定了哪个教师即可:
klass/teacher-select/teacher-select.component.ts
```
/**
* 获取所有的教师,并传给V层
*/
ngOnInit() {
this.teacherSelect = new FormControl();
const url = 'http://localhost:8080/Teacher';
this.httpClient.get(url)
.subscribe((teachers: Array<Teacher>) => {
this.teachers = teachers;
this.teachers.forEach((teacher: Teacher) => { ➊
if (teacher.id === this.teacher.id) {
this.teacherSelect.setValue(teacher);
}
});
});
```
* ➊ 对教师数组进行遍历,当传入的教师ID与当前的遍历项教师ID相同时,则设置`select`的选中值。
## 测试
要想使用刚刚我们创建的组件,则必须在向该组件中传入`教师`。按此思想我们对原班级编辑组件进行改造。
klass/edit/edit.component.html
```
<h3>编辑班级</h3>
<form (ngSubmit)="onSubmit()" [formGroup]="formGroup">
<label for="name">名称:<input id="name" type="text" formControlName="name"/></label>
<label for="teacherId">教师:<app-teacher-select id="teacherId" [teacher]="teacher"➊ ></app-teacher-select></label>
<button>更新</button>
</form>
```
* ➊ 第一个`teacher`对应选择教师组件的`@Input() teacher`;第二个`teacher`应该对应班级编辑组件中的C层属性。
```
...
formGroup: FormGroup;
teacher: Teacher; ✚
private url: string;
...
/**
* 加载要编辑的班级数据
*/
loadData(): void {
this.httpClient.get(this.getUrl())
.subscribe((klass: Klass) => {
this.formGroup.setValue({name: klass.name, teacherId: klass.teacher.id});
this.teacher = klass.teacher; ✚
}, () => {
console.error(`${this.getUrl()}请求发生错误`);
});
}
```
我们先回到班级列表,然后点击编辑按钮触发该组件,测试结果如下:
![](https://img.kancloud.cn/c4/94/c49482be8b1813652b455489e93fa170_1238x456.png)
该错误提示我们:在` (teacher.id === this.teacher.id) {`代码发生错误:不能够在`undefined`上读取`id`属性。这是我们新手在使用angular进行开发时常常会遇到的问题。
## 异步请求
如果想弄清楚产生这个错误的原因还需要从angular进行组件间的调用的流程说起:
![](https://img.kancloud.cn/fd/a5/fda5e636ea0c6b4f157a2bfabb57a431_740x532.png)
如上图所示,在进行组件的构建过程中。总共发起了两次资源请求(进行http请求)。而js在进行资源请求(进行http请求)时发起的为异步操作。也就是说:虽然班级编辑组件早于选择教师组件发起了http请求,但收到请求结果的顺序却不一定早于后者。所以该组件的测试就会有两种情况发生:
* [ ] 如果班级编辑组件的http请求返回**早**于选择教师组件的,则在选择教师组件进行teacher是否为undefined判断时:teacher的值并不为undefined,所以不会发生错误.
* [ ] 如果班级编辑组件的http请求返回**晚**于选择教师组件的,则在选择教师组件进行teacher是否为undefined判断时:teacher的值仍然为初始化的值undefined,此时便会发生错误。
当我们由班级列表中点击编辑按钮进入该组件时,网络请求大概会是这个样子:
![](https://img.kancloud.cn/21/ad/21ad00a6c45ff54605f5fa641a596520_943x367.png)
由于后发起访问的teacher**早**于先发起访问的klass,所以在执行相关语句时,teacher的值为undefined,故而引发了`Cannot read property 'id' of undefined`错误。
## 证真是学习、证伪是提升
更有意思的测试结果是:
* [ ] 如果我们在不打开控制台的前提下,直接刷新编辑班级页面,那么10之有9会发生该错误。
* [ ] 如果我们在打开控制台的前提下,直接刷新编辑班级页面,那么又基本上不会发生该错误。
![](https://img.kancloud.cn/d1/9b/d19bb96626568c88079b2770bc8da8c9_547x487.gif)
如果在前面我的理论支持下,是否会自动绑定教师应该与是否打开控制台无关,那么为什么在打开控制台的情况下,就正常了呢?
![](https://img.kancloud.cn/33/bc/33bc1ce359a57bba5aa29802bc8d5ab1_1653x209.png)
这主要是由于最后这个请求的存在,我们发现此请求是在klass请求完成后发起的。我们猜测:当**刷新**页面时angular会进行项目的初始化,过程大概应该是这样的:
* [ ] 扫描整个项目
* [ ] 扫描项目中的当前所用到的组件(班级编辑、选择教师),进行预请求
* [ ] 构建项目
* [ ] 使用预请求的返回结果构建组件(此时teacher的值并不是undefined,所以构建成功)
在此理论的支持下,如果我们在开启控制台的前提下,先打开班级列表组件,然后再点编辑按钮,那么顺序应该是这样的:
* [ ] 扫描整个项目
* [ ] 扫描项目中的当前所用到的组件(班级列表),进行预请求
* [ ] 构建项目
* [ ] 使用预请求的返回结果构建组件(此时teacher的值并不是undefined,所以构建成功)
* [ ] 点击编辑按钮进行跳转,构建班级编辑、选择教师组件
* [ ] 优先返回了teacher数据,而且发生错误。
测试:
![](https://img.kancloud.cn/a2/60/a260c1646aea058de6ca131c75cf4d16_547x487.gif)
测试足够支持我们的猜想。所以最终的结论是:
* 当有异步请求时,程序的执行顺序会受异步请求返回先后的影响。
* 当打开控制台时开发进行页面刷新时,angular会尝试启用扫描及加载机制。
* 在正式的开发中,应该适时的关闭控制台来进行组件的测试。
## 使用ngIf来规避undefined错误
由于异步请求的存在,我们无法预测哪个请求会先返回。这无疑将会降低我们系统在使用中的可靠性。暴露很多类似于`在我电脑上没问题`、`测试的时候是好好的`这种好像低级、但实际是**"无解"**的问题。
要防止选择教师组件报这样的错误我们只需要保证:在编辑班级组件成功的获取到班级数据前,不要渲染选择教师组件即可。而`*ngIf`恰恰可以实现这个小功能:
```
<h3>编辑班级</h3>
<form (ngSubmit)="onSubmit()" [formGroup]="formGroup">
<label for="name">名称:<input id="name" type="text" formControlName="name"/></label>
<label for="teacherId">教师:<app-teacher-select *ngIf="teacher"➊ id="teacherId" [teacher]="teacher"></app-teacher-select></label>
<button>更新</button>
</form>
```
* ➊当teacher存在时`if (teacher)`,渲染该组件
#### 测试
![](https://img.kancloud.cn/27/62/2762a482e7c83eabb862baa5767731ef_579x343.gif)
有了`ngIf`的存在,当初始化时teacher为undefined时,该组件就不会渲染了。而当teacher有值时才会渲染该组件,此时传入选择教师组件的teacher必然不是undefined,当然也就成功的规避了上述错误。
## 单元测试
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.5.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.5.5) | - |
| 通过输入型绑定把数据从父组件传到子组件 | [https://www.angular.cn/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding](https://www.angular.cn/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding) | 10 |
| SelectControlValueAccessor | [https://www.angular.cn/api/forms/SelectControlValueAccessor](https://www.angular.cn/api/forms/SelectControlValueAccessor) | 15 |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用