在編輯班級時,我們希望有如下的效果:

即引用教師組件后,教師組件可以根據傳入的教師信息自動選中某位教師,這就涉及到了組件的**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>
```

## @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()}請求發生錯誤`);
});
}
```
我們先回到班級列表,然后點擊編輯按鈕觸發該組件,測試結果如下:

該錯誤提示我們:在` (teacher.id === this.teacher.id) {`代碼發生錯誤:不能夠在`undefined`上讀取`id`屬性。這是我們新手在使用angular進行開發時常常會遇到的問題。
## 異步請求
如果想弄清楚產生這個錯誤的原因還需要從angular進行組件間的調用的流程說起:

如上圖所示,在進行組件的構建過程中。總共發起了兩次資源請求(進行http請求)。而js在進行資源請求(進行http請求)時發起的為異步操作。也就是說:雖然班級編輯組件早于選擇教師組件發起了http請求,但收到請求結果的順序卻不一定早于后者。所以該組件的測試就會有兩種情況發生:
* [ ] 如果班級編輯組件的http請求返回**早**于選擇教師組件的,則在選擇教師組件進行teacher是否為undefined判斷時:teacher的值并不為undefined,所以不會發生錯誤.
* [ ] 如果班級編輯組件的http請求返回**晚**于選擇教師組件的,則在選擇教師組件進行teacher是否為undefined判斷時:teacher的值仍然為初始化的值undefined,此時便會發生錯誤。
當我們由班級列表中點擊編輯按鈕進入該組件時,網絡請求大概會是這個樣子:

由于后發起訪問的teacher**早**于先發起訪問的klass,所以在執行相關語句時,teacher的值為undefined,故而引發了`Cannot read property 'id' of undefined`錯誤。
## 證真是學習、證偽是提升
更有意思的測試結果是:
* [ ] 如果我們在不打開控制臺的前提下,直接刷新編輯班級頁面,那么10之有9會發生該錯誤。
* [ ] 如果我們在打開控制臺的前提下,直接刷新編輯班級頁面,那么又基本上不會發生該錯誤。

如果在前面我的理論支持下,是否會自動綁定教師應該與是否打開控制臺無關,那么為什么在打開控制臺的情況下,就正常了呢?

這主要是由于最后這個請求的存在,我們發現此請求是在klass請求完成后發起的。我們猜測:當**刷新**頁面時angular會進行項目的初始化,過程大概應該是這樣的:
* [ ] 掃描整個項目
* [ ] 掃描項目中的當前所用到的組件(班級編輯、選擇教師),進行預請求
* [ ] 構建項目
* [ ] 使用預請求的返回結果構建組件(此時teacher的值并不是undefined,所以構建成功)
在此理論的支持下,如果我們在開啟控制臺的前提下,先打開班級列表組件,然后再點編輯按鈕,那么順序應該是這樣的:
* [ ] 掃描整個項目
* [ ] 掃描項目中的當前所用到的組件(班級列表),進行預請求
* [ ] 構建項目
* [ ] 使用預請求的返回結果構建組件(此時teacher的值并不是undefined,所以構建成功)
* [ ] 點擊編輯按鈕進行跳轉,構建班級編輯、選擇教師組件
* [ ] 優先返回了teacher數據,而且發生錯誤。
測試:

測試足夠支持我們的猜想。所以最終的結論是:
* 當有異步請求時,程序的執行順序會受異步請求返回先后的影響。
* 當打開控制臺時開發進行頁面刷新時,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)`,渲染該組件
#### 測試

有了`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
- 總結
- 開發規范
- 備用