在编辑班级时,我们希望有如下的效果: ![](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 |