相對(duì)新增而言,編輯功能有一些小小的復(fù)雜,在開(kāi)始編碼前讓我們共同復(fù)習(xí)一下其流程:
- 獲取路由中的參數(shù)id
- 根據(jù)獲取的參數(shù)id請(qǐng)求后臺(tái)的班級(jí)數(shù)據(jù)
- 使用班級(jí)數(shù)據(jù)填充V層表單
- 編輯V層
- 將編輯后的表單提交到后臺(tái)
有了個(gè)大概的流程后,但可以嘗試進(jìn)行開(kāi)發(fā)了。
## 獲取路由參數(shù)
路由參數(shù)位于`ActivatedRoute`中:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
- constructor() { }
+ constructor(private activatedRoute: ActivatedRoute) { }
```
在`ng t`中,使用`RouterTestingModule`提供`RouterState`:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.spec.ts
+import {RouterTestingModule} from '@angular/router/testing';
- ReactiveFormsModule
+ ReactiveFormsModule,
+ RouterTestingModule
]
```
參考在編輯教師組件關(guān)于獲取路由參數(shù)的代碼,可以調(diào)用`activatedRoute`上的`snapshot`來(lái)獲取到請(qǐng)求的`id`信息:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -12,6 +12,7 @@ export class EditComponent implements OnInit {
}
ngOnInit(): void {
+ const id = this.activatedRoute.snapshot.params.id;
}
```
## 測(cè)試
`ActivatedRoute`在獲取`id`值時(shí),依賴于`url`中路由信息,在當(dāng)前`ng t`的環(huán)境下,URL地址固定為`http://localhost:9876/?id=xxxxx`,所以在單元測(cè)試中并沒(méi)有辦法直觀的感覺(jué)到代碼`const id = this.activatedRoute.snapshot.params.id;`的執(zhí)行情況。
為此,我們暫時(shí)放棄對(duì)路由的測(cè)試,待clazz模塊中全部的組件完成后啟用`ng s` 時(shí),再來(lái)觀察路由情況。但是獲取路由中的參數(shù)`id`卻是我們開(kāi)發(fā)組件的第一項(xiàng),如果不完成這項(xiàng),后續(xù)的操作好像無(wú)法進(jìn)行。在`ng s`進(jìn)行集成開(kāi)發(fā)、測(cè)試時(shí)是這樣的,但當(dāng)前是`ng t`單元測(cè)試,它可以在測(cè)試過(guò)程中按我們的需求變更組件的屬性值或是調(diào)用組件中的某些方法。利用`ng t`的此特性,我們?cè)诋?dāng)前組件中添加如下代碼:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -13,6 +13,15 @@ export class EditComponent implements OnInit {
ngOnInit(): void {
const id = this.activatedRoute.snapshot.params.id;
+ // 調(diào)用loadById方法,獲取預(yù)編輯的班級(jí)
+ }
+
+ /**
+ * 由后臺(tái)加載預(yù)編輯的班級(jí).
+ * @param id 班級(jí)id.
+ */
+ loadById(id: number): void {
+ console.log('loadById');
}
}
```
此時(shí),我們?cè)趩卧獪y(cè)試中便可以直接調(diào)用`loadById()`方法,從而模似獲取在要編輯的`id`值后的后續(xù)操作:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.spec.ts
@@ -33,5 +33,8 @@ describe('EditComponent', () => {
expect(component).toBeTruthy();
getTestScheduler().flush();
fixture.autoDetectChanges();
+
+ // 手動(dòng)觸發(fā)loadById方法,模擬組件獲取路由參數(shù)后的操作
+ component.loadById(123);
});
```
效果如下:

接下來(lái)便可以繼續(xù)開(kāi)發(fā)其它的功能了。
## Api
后臺(tái)提供了獲取某個(gè)班級(jí)的地址,信息如下:
```bash
GET /clazz/{id}
```
| **類型Type** | **名稱Name** | **描述Description** | **類型Schema** |
| :----------- | :----------- | :------------------ | :----------------------------------------------------------- |
| Response | | 班級(jí) | `{id: number, name: string, teacher: {id: number, name: string}}` |
我們?cè)?.4.3小節(jié)中給出過(guò)獲取教師信息的接口地址,稍加觀察我們可以總結(jié)出以下規(guī)律:
- 獲取某個(gè)X時(shí),請(qǐng)求方法為`GET`
- 獲取某個(gè)X時(shí),根據(jù)X的類型不同,地址前綴會(huì)有所不同。比如獲取某個(gè)教師的前綴是`/teacher`,而獲取某個(gè)班級(jí)的前綴是`/clazz`。以此累推在后面的章節(jié)中,獲取學(xué)生的前綴將是`/student`。
- 獲取某個(gè)X時(shí),必須指名X的關(guān)鍵字(一般是id),并其關(guān)鍵字以`/xx`的形式放到最后。比如獲取id為1的班級(jí)的URL為`/clazz/1` 。
而遵循上述規(guī)則的后臺(tái)接口,我們稱其為`RESTful API`;反之如果某個(gè)后臺(tái)接口遵循了`RESTful API`風(fēng)格,則必然符合上述3點(diǎn)規(guī)則。
> ? `RESTful API`還規(guī)定了其它的后臺(tái)接口規(guī)則,教程中的API也符合這種規(guī)則。
## 獲取班級(jí)
調(diào)用`httpClient`來(lái)獲取某個(gè)班級(jí)的操作相信大家已經(jīng)輕車熟路了,代碼如下:
```typescript
- constructor(private activatedRoute: ActivatedRoute) {
+ constructor(private activatedRoute: ActivatedRoute,
+ private httpClient: HttpClient) {
}
loadById(id: number): void {
console.log('loadById');
+ this.httpClient.get<Clazz>('/clazz/' + id.toString())
+ .subscribe(clazz => {
+ console.log('接收到了clazz', clazz);
+ }, error => console.log(error));
}
```
測(cè)試:

> 由于作者粗心的原因,上述提供信息并不完全正確。
## MockApi
有了后臺(tái)的API規(guī)范,便可以對(duì)應(yīng)增加一個(gè)模擬API了:
```typescript
+++ b/first-app/src/app/mock-api/clazz.mock.api.ts
@@ -3,6 +3,7 @@ import {Clazz} from '../entity/clazz';
import {Teacher} from '../entity/teacher';
import {Page} from '../entity/page';
import {HttpParams} from '@angular/common/http';
+import {randomString} from '@yunzhi/ng-mock-api/testing';
/**
* 班級(jí)模擬API
@@ -75,6 +76,21 @@ export class ClazzMockApi implements MockApiInterface {
numberOfElements: size * 10
});
}
+ },
+ {
+ method: 'GET',
+ url: `/clazz/(\\d+)`,
+ result: (urlMatches: Array<string>) => {
+ console.log(urlMatches);
+ // 使用 + 完成字符串向數(shù)字的轉(zhuǎn)換
+ const id = +urlMatches[1];
+ return {
+ id,
+ name: randomString('班級(jí)名稱'),
+ teacher: {
+ id: randomNumber(),
+ name: randomString('教師')
+ }
+ } as Clazz;
+ }
}
```
上述方法中,我們把`result`設(shè)置成了`function` ,該函數(shù)中的第一個(gè)參數(shù)的的類型為`Array<string>`,也可以書(shū)寫(xiě)為`string[]`。`urlMatches`中的第0個(gè)參數(shù)為請(qǐng)求的URL信息;第1至n個(gè)參數(shù)為正則表式達(dá)的匹配值。
比如當(dāng)我們對(duì)`/clazz/123`發(fā)起請(qǐng)求時(shí),按正則表達(dá)式`/clazz/(\\d+)`來(lái)匹配,最終的`urlMatches`值為:

如此,我們便可以通過(guò)`urlMatches`來(lái)獲取請(qǐng)求的`id`信息了。在這里特別需要注意`urlMatches`數(shù)組元素的類型為`string` ,所以要使用時(shí)要進(jìn)行適當(dāng)?shù)霓D(zhuǎn)換。比如我們這里輸出數(shù)字`123`而非字符串`123` ,則使用了`+`將字符串轉(zhuǎn)換為了數(shù)字。
上述代碼中我們還引用了`@yunzhi/ng-mock-api/testing`中的`randomString()`方法來(lái)快速的生成隨機(jī)字符串, 這樣一來(lái)保證了每刷新一次請(qǐng)求都會(huì)接收到不同的響應(yīng)信息。
## 響應(yīng)式表單
接收到后臺(tái)的返回值后,接下來(lái)將接收到的值綁定到V層的表單中。自本節(jié)開(kāi)始,我們將全面啟用更加面向?qū)ο蟮捻憫?yīng)式表單,所以使用`([ngModel])`在V層中綁定數(shù)據(jù)的方式已然成為歷史。
### 初始化表單
V層中共使用了兩個(gè)字段信息,分別為班級(jí)名稱及教師。教師我們使用了教師選擇組件,選擇的教師可以通過(guò)相關(guān)的方法進(jìn)行綁定。所以在這僅需要初始化一個(gè)響應(yīng)式表單來(lái)處理班級(jí)名稱即可:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -2,6 +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} from '@angular/forms';
@Component({
selector: 'app-edit',
@@ -9,6 +10,10 @@ import {Clazz} from '../../entity/clazz';
styleUrls: ['./edit.component.css']
})
export class EditComponent implements OnInit {
+ /**
+ * 班級(jí)名稱.
+ */
+ nameFormControl = new FormControl('');
constructor(private activatedRoute: ActivatedRoute,
private httpClient: HttpClient) {
@@ -28,6 +33,7 @@ export class EditComponent implements OnInit {
this.httpClient.get<Clazz>('/clazz/' + id.toString())
.subscribe(clazz => {
console.log('接收到了clazz', clazz);
+ this.nameFormControl.setValue(clazz.name);
}, error => console.log(error));
}
```
### 綁定
接著將其綁定到V層的name輸入框上:
```html
+++ b/first-app/src/app/clazz/edit/edit.component.html
@@ -2,7 +2,7 @@
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">名稱</label>
<div class="col-sm-10">
- <input type="text" class="form-control">
+ <input type="text" class="form-control" [formControl]="nameFormControl">
<small class="text-danger">
班級(jí)名稱不能為空
</small>
```
效果如下:

## 驗(yàn)證非空
可以通過(guò)向響應(yīng)式表單中加入驗(yàn)證器的方式來(lái)實(shí)現(xiàn)對(duì)某個(gè)表單項(xiàng)的驗(yàn)證,比如此時(shí)要求名稱不能為空,則可以為其添加一個(gè)非空驗(yàn)證器:
```typescript
-import {FormControl} from '@angular/forms';
+import {FormControl, Validators} from '@angular/forms';
@Component({
selector: 'app-edit',
@@ -13,7 +13,7 @@ export class EditComponent implements OnInit {
/**
* 班級(jí)名稱.
*/
- nameFormControl = new FormControl('');
+ nameFormControl = new FormControl('', Validators.required);
```
此時(shí)將`nameFormControl`中的`value`為空時(shí),其`invalid`屬性則將為`true`,利用該特定在V層中定制錯(cuò)誤提示信息:
```html
+++ b/first-app/src/app/clazz/edit/edit.component.html
@@ -3,7 +3,7 @@
<label class="col-sm-2 col-form-label">名稱</label>
<div class="col-sm-10">
<input type="text" class="form-control" [formControl]="nameFormControl">
- <small class="text-danger">
+ <small class="text-danger" *ngIf="nameFormControl.invalid">
班級(jí)名稱不能為空
</small>
</div>
```
**invalid**的譯文為:無(wú)效的。
## 測(cè)試
但當(dāng)我們查看效果時(shí),好像并沒(méi)有起作用,錯(cuò)誤的提示信息仍然存在。

莫非是剛剛的代碼寫(xiě)錯(cuò)了?為了驗(yàn)證這個(gè)觀點(diǎn),我們?cè)赩層中打印下這個(gè)`nameFormControl.invalid`。
```html
<div class="col-sm-10">
+ {{nameFormControl.invalid}}
<input type="text" class="form-control" [formControl]="nameFormControl">
<small class="text-danger" *ngIf="nameFormControl.invalid">
```
打印發(fā)現(xiàn)其值的確為`true`。

而我們的想法時(shí),當(dāng)班級(jí)名稱為空時(shí)`nameFormControl.invalid`的值為`true`,如果非空的時(shí)候應(yīng)該為`false`才對(duì)。這時(shí)候就要再把前面講過(guò)的`zone.js`與Angular的變更檢測(cè)機(jī)制般出來(lái)了。
Angular的變更檢測(cè)是建立在`zone.js`的基礎(chǔ)上的,`zone.js`使用通知的方式來(lái)發(fā)送數(shù)據(jù)變更的通知。這樣做可以有效的提升數(shù)據(jù)監(jiān)聽(tīng)的效率。只所以叫做數(shù)據(jù)監(jiān)聽(tīng),是由于在非`zone.js`的模式下,若要感知某個(gè)數(shù)據(jù)的變化,則需要時(shí)時(shí)的**釘**著這個(gè)數(shù)據(jù),這就像間諜片中對(duì)X進(jìn)行監(jiān)視一下;而在`zone.js`模式下,被監(jiān)聽(tīng)的數(shù)據(jù)由被動(dòng)監(jiān)聽(tīng)變成了主動(dòng)通知,這就像間諜片中對(duì)X進(jìn)行了策反一樣,一旦有了新情況X會(huì)主動(dòng)的告訴我們。這種通知的模塊有效的提升的應(yīng)用效率,我們?cè)僖膊恍枰M(fèi)勁地**釘**著某個(gè)數(shù)據(jù)是否產(chǎn)生變化了。
`zone.js` 實(shí)現(xiàn)監(jiān)聽(tīng)的原理是在原方法上打補(bǔ)?。∕onkey patch),我們也可以理解為在原方法中放置間諜。在沒(méi)有`zone.js`以前`setTimeout` 就是`setTimeout`,而在引入`zone.js`以后,`setTimeout`就是有間諜的`setTimeout`了。正是這個(gè)間諜的存在,所以`zone.js`能夠通知Angular:異步的方法被調(diào)用了,V層對(duì)應(yīng)的數(shù)據(jù)**可能**發(fā)生了變化。Angular接收到這個(gè)通知后,開(kāi)始進(jìn)行變更檢測(cè),發(fā)現(xiàn)變化則重新渲染V層的界面。
在模擬后臺(tái)Api時(shí),為了能夠正綜的手動(dòng)控制后臺(tái)的返回值`@yunzhi/ng-mock-api/testing`在返回模擬數(shù)據(jù)時(shí),按以下兩種情況使用了兩種模式返回?cái)?shù)據(jù):
1. 如果當(dāng)前是執(zhí)行的測(cè)試代碼觸發(fā)數(shù)據(jù)請(qǐng)求,則使用的是**彈珠測(cè)試**的模式來(lái)返回?cái)?shù)據(jù)。此方案`zone.js` 感知不到,所以在此模式下即使在單元測(cè)試中啟用了自動(dòng)變更檢測(cè),也不會(huì)有效。
2. 如果當(dāng)前的數(shù)據(jù)觸發(fā)由開(kāi)發(fā)人員在V層中交互引起的,則使用非彈珠測(cè)試的模式來(lái)返回?cái)?shù)據(jù),此方案`zone.js`能夠感知到,所以在此模式下單元測(cè)試若啟用了自動(dòng)變更檢測(cè),則V層會(huì)重新渲染。
所以如果我們?nèi)缦赂膶?xiě)測(cè)試代碼的話,變更檢測(cè)將失效:
```typescript
fit('should create', () => {
expect(component).toBeTruthy();
// 先啟動(dòng)變更檢測(cè)
fixture.autoDetectChanges(); ??
// 在該代碼前進(jìn)行了組件初始化,模擬請(qǐng)求了教師列表數(shù)據(jù)。
// 此代碼將返回還未響應(yīng)的所有請(qǐng)求,包含:教師列表數(shù)據(jù)
getTestScheduler().flush();
component.loadById(123);
// loadByIdy方法中觸發(fā)了請(qǐng)求123班級(jí)數(shù)據(jù)
// 此代碼將返回還未響應(yīng)的所有請(qǐng)求,包含:請(qǐng)求ID為123的班級(jí)數(shù)據(jù)
getTestScheduler().flush();
});
```

列表值為空且校驗(yàn)未生效,說(shuō)明V層的確沒(méi)有進(jìn)行渲染。
### autoDetectChanges()
那么既然啟動(dòng)自動(dòng)變更檢,那么為什么將其放在數(shù)據(jù)發(fā)送后就能夠生效呢?
```typescript
fit('should create', () => {
expect(component).toBeTruthy();
// 在該代碼前進(jìn)行了組件初始化,模擬請(qǐng)求了教師列表數(shù)據(jù)。
// 此代碼將返回還未響應(yīng)的所有請(qǐng)求,包含:教師列表數(shù)據(jù)
getTestScheduler().flush();
// 啟動(dòng)變更檢測(cè),此時(shí)教師列表生效
fixture.autoDetectChanges(); ??
component.loadById(123);
// loadByIdy方法中觸發(fā)了請(qǐng)求123班級(jí)數(shù)據(jù)
// 此代碼將返回還未響應(yīng)的所有請(qǐng)求,包含:請(qǐng)求ID為123的班級(jí)數(shù)據(jù)
getTestScheduler().flush();
});
```
如上代碼將`autoDetectChanges()`放到了第一次觸發(fā)返回?cái)?shù)據(jù)下方,則教師列表在V層中被重新渲染:

如果將其放到最下方,則校驗(yàn)也會(huì)隨之生效:
```typescript
fit('should create', () => {
expect(component).toBeTruthy();
// 在該代碼前進(jìn)行了組件初始化,模擬請(qǐng)求了教師列表數(shù)據(jù)。
// 此代碼將返回還未響應(yīng)的所有請(qǐng)求,包含:教師列表數(shù)據(jù)
getTestScheduler().flush();
component.loadById(123);
// loadByIdy方法中觸發(fā)了請(qǐng)求123班級(jí)數(shù)據(jù)
// 此代碼將返回還未響應(yīng)的所有請(qǐng)求,包含:請(qǐng)求ID為123的班級(jí)數(shù)據(jù)
getTestScheduler().flush();
// 最后啟動(dòng)變更檢測(cè),則formControl也會(huì)被重新渲染
fixture.autoDetectChanges(); ??
});
```

這是由于`autoDetectChanges()`實(shí)際上等于`detectChanges()` + `自動(dòng)檢測(cè)`。也就是說(shuō)每執(zhí)行一次`autoDetectChanges()`將首先執(zhí)行`detectChanges()`,然后才是開(kāi)啟自動(dòng)變更檢測(cè)功能。
總而言之,如果我們想在`ng t`中實(shí)時(shí)的查看組件的一些交互效果,則應(yīng)該在單元測(cè)試的最后兩行放置如下代碼:
```typescript
getTestScheduler().flush();
fixture.autoDetectChanges();
```
### FormControl
如果你剛剛跟上了教程在不停的思索的話,相信應(yīng)該在下圖有所疑問(wèn):

此圖片出現(xiàn)在如下測(cè)試代碼中:
```typescript
fit('should create', () => {
expect(component).toBeTruthy();
// 先啟動(dòng)變更檢測(cè)
fixture.autoDetectChanges(); ??
// 在該代碼前進(jìn)行了組件初始化,模擬請(qǐng)求了教師列表數(shù)據(jù)。
// 此代碼將返回還未響應(yīng)的所有請(qǐng)求,包含:教師列表數(shù)據(jù)
getTestScheduler().flush();
component.loadById(123);
// loadByIdy方法中觸發(fā)了請(qǐng)求123班級(jí)數(shù)據(jù)
// 此代碼將返回還未響應(yīng)的所有請(qǐng)求,包含:請(qǐng)求ID為123的班級(jí)數(shù)據(jù)
getTestScheduler().flush();
});
```
我想你的疑問(wèn)應(yīng)該出現(xiàn)在班級(jí)的名稱上。既然我們說(shuō)上述代碼將導(dǎo)致Angular無(wú)法獲取V層發(fā)生了變化,也不會(huì)重新對(duì)V層進(jìn)行渲染的話。那么為什么V層中會(huì)出現(xiàn)**班級(jí)名稱**?
這個(gè)原因主要有兩個(gè),簡(jiǎn)單解釋如下:
1. 請(qǐng)求某個(gè)班級(jí)的數(shù)據(jù)返回后,在C層中調(diào)用了`formControl`的`setValue()`方法,而`Angular`可以在我們調(diào)用該方法時(shí),得到一個(gè)通知。
2. Angular的**局部渲染**功能使得其在得知到這個(gè)通知后,僅僅渲染了其對(duì)應(yīng)的`input`的值。
至于更深入的原因已經(jīng)超出了本教程的范圍。
## 本節(jié)作業(yè)
1. 刪除班級(jí)名稱,看非空驗(yàn)證器是否生效。
2. 除非空驗(yàn)證外,`Validators`中還存在很多內(nèi)置驗(yàn)證器,請(qǐng)找到它們并猜猜其具體作用,最后嘗試驗(yàn)證自己的猜想。
3. 為教師列表增加一個(gè)`@Input()`,使其接收請(qǐng)求班級(jí)返回?cái)?shù)據(jù)的`clazz.teacher.id`。在設(shè)置過(guò)程中,應(yīng)該將`@Input()`注釋到屬性上還是`set`方法上,為什么?
| 名稱 | 鏈接 |
| ------------------------------- | ------------------------------------------------------------ |
| ZoneJS 的原理與應(yīng)用 | [https://juejin.cn/post/6859348400463314951](https://juejin.cn/post/6859348400463314951) |
| 翻閱源碼后,我終于理解了Zone.js | [https://zhuanlan.zhihu.com/p/50835920](https://zhuanlan.zhihu.com/p/50835920) |
| 編寫(xiě)彈珠測(cè)試 | [https://cloud.tencent.com/developer/section/1489402](https://cloud.tencent.com/developer/section/1489402) |
| 本節(jié)源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.4.2.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.4.2.zip) |
- 序言
- 第一章 Hello World
- 1.1 環(huán)境安裝
- 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 請(qǐng)求后臺(tái)數(shù)據(jù)
- 2.2.1 HttpClient
- 2.2.2 請(qǐng)求數(shù)據(jù)
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調(diào)函數(shù)
- 2.2.5 集成測(cè)試
- 2.2.6 本章小節(jié)
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對(duì)接后臺(tái)
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數(shù)
- 2.4.3 插值與模板表達(dá)式
- 2.4.4 初識(shí)泛型
- 2.4.5 更新教師
- 2.4.6 測(cè)試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標(biāo)庫(kù)
- 2.6.3 firefox
- 2.7 總結(jié)
- 第三章 用戶登錄
- 3.1 初識(shí)單元測(cè)試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結(jié)
- 第四章 個(gè)人中心
- 4.1 原型
- 4.2 管道
- 4.3 對(duì)接后臺(tái)
- 4.4 x-auth-token認(rèn)證
- 4.5 攔截器
- 4.6 小結(jié)
- 第五章 系統(tǒng)菜單
- 5.1 延遲及測(cè)試
- 5.2 手動(dòng)創(chuàng)建組件
- 5.3 隱藏測(cè)試信息
- 5.4 規(guī)劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結(jié)
- 第六章 班級(jí)管理
- 6.1 新增班級(jí)
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級(jí)
- 6.1.3 ApiInterceptor
- 6.1.4 數(shù)據(jù)驗(yàn)證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構(gòu)
- 6.1.8 小結(jié)
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應(yīng)式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應(yīng)用組件
- 6.2.5 小結(jié)
- 6.3 班級(jí)列表
- 6.3.1 原型設(shè)計(jì)
- 6.3.2 初始化分頁(yè)
- 6.3.3 MockApi
- 6.3.4 靜態(tài)分頁(yè)
- 6.3.5 動(dòng)態(tài)分頁(yè)
- 6.3.6 @Input()
- 6.4 編輯班級(jí)
- 6.4.1 測(cè)試模塊
- 6.4.2 響應(yīng)式表單驗(yàn)證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構(gòu)
- 6.4.7 小結(jié)
- 6.5 刪除班級(jí)
- 6.6 集成測(cè)試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉(zhuǎn)
- 6.6.4 ngStyle
- 6.7 初識(shí)Service
- 6.7.1 catchError
- 6.7.2 單例服務(wù)
- 6.7.3 單元測(cè)試
- 6.8 小結(jié)
- 第七章 學(xué)生管理
- 7.1 班級(jí)列表組件
- 7.2 新增學(xué)生
- 7.2.1 exports
- 7.2.2 自定義驗(yàn)證器
- 7.2.3 異步驗(yàn)證器
- 7.2.4 再識(shí)DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結(jié)
- 7.3 單元測(cè)試進(jìn)階
- 7.4 學(xué)生列表
- 7.4.1 JSON對(duì)象與對(duì)象
- 7.4.2 單元測(cè)試
- 7.4.3 分頁(yè)模塊
- 7.4.4 子組件測(cè)試
- 7.4.5 重構(gòu)分頁(yè)
- 7.5 刪除學(xué)生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向?qū)ο?/a>
- 7.6 集成測(cè)試
- 7.7 編輯學(xué)生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學(xué)生
- 7.7.4 集成測(cè)試
- 7.7.5 可訂閱的路由參數(shù)
- 7.7.6 小結(jié)
- 7.8 總結(jié)
- 第八章 其它
- 8.1 打包構(gòu)建
- 8.2 發(fā)布部署
- 第九章 總結(jié)