屬性型指令的運用需要有一些HTML DOM知識,本節我們以loading指令為例拋磚引玉,初步認識一下屬性型指令。
在開始之前我們先為保存按鈕引入個保存圖標:
```html
+++ b/first-app/src/app/student/add/add.component.html
@@ -47,7 +47,8 @@
</div>
<div class="mb-3 row">
<div class="col-sm-10 offset-2">
- <button class="btn btn-primary" [disabled]="formGroup.invalid || formGroup.pending">保存
+ <button class="btn btn-primary" [disabled]="formGroup.invalid || formGroup.pending">
+ <i class="fa fa-save"></i>保存
</button>
</div>
</div>
```
此時將所有的字段都符合要求時,將如下顯示保存按鈕:

現在我們想實現一個加載中效果,即:當后臺發起請求時,保存的這個小圖標變成一個loading動畫:
```html
<button class="btn btn-primary" [disabled]="formGroup.invalid || formGroup.pending">
- <i class="fa fa-save"></i>保存
+ <i class="fas fa-cog fa-spin"></i>保存
</button>
```

同時,為保存按鈕為添加`disable`屬性,使其不能再次被點擊:

好的,預實現的效果有了,讓我們恢復一下V層,最后保存按鈕相關的`html`如下:
```html
<div class="mb-3 row">
<div class="col-sm-10 offset-2">
<button class="btn btn-primary" [disabled]="formGroup.invalid || formGroup.pending">
<i class="fa fa-save"></i>保存
</button>
</div>
</div>
```
## 指令初始化
前面我們多次提過模塊的三個元素:組件、指令和管道,所以按模塊化的思想,建立loading指令前需要先建立一個loading模塊。同時為了更好找,我們在根目錄下建立一個`directive`目錄專門來存放公用指令:
```bash
panjie@panjies-iMac app % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app
panjie@panjies-iMac app % mkdir directive
panjie@panjies-iMac app % cd directive
panjie@panjies-iMac directive %
```
然后在該目錄下建立`loading`模塊:
```bash
panjie@panjies-iMac directive % ng g m loading
CREATE src/app/directive/loading/loading.module.ts (193 bytes)
```
接著進入`loading`文件夾,使用`ng g directive loading`創建loading指令,該指令將自動加入到directive模塊中:
```bash
panjie@panjies-iMac directive % cd loading
panjie@panjies-iMac loading % ng g directive loading
CREATE src/app/directive/loading/loading.directive.spec.ts (228 bytes)
CREATE src/app/directive/loading/loading.directive.ts (143 bytes)
UPDATE src/app/directive/loading/loading.module.ts (259 bytes)
```
最終建立的文件如下:
```bash
panjie@panjies-iMac app % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app
panjie@panjies-iMac app % tree directive
directive
└── loading
├── loading.directive.spec.ts
├── loading.directive.ts
└── loading.module.ts
1 directory, 3 files
```
如果將上述指令添加到當前的學生增加組件中,則每次測試的時候都需要依次填充學生增加組件中的必填字段,填寫合法的手機號等,這明顯是一份重復的勞動。做為懶人的我們,怎么能允許這種事情發生呢?
為此,我們在指令的單元測試中新建一個測試專用組件,然后在這個測試專用組件中引入loading指令,以協助我們完成指令的開發工作:
```typescript
+++ b/first-app/src/app/directive/loading/loading.directive.spec.ts
import {LoadingDirective} from './loading.directive';
import {Component} from '@angular/core';
import {FormGroup, ReactiveFormsModule} from '@angular/forms';
import {ComponentFixture, TestBed} from '@angular/core/testing';
@Component({
template: `
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<button class="btn btn-primary" appLoading><i class="fa fa-save"></i>保存</button>
</form>
`
})
class TestComponent {
formGroup = new FormGroup({});
onSubmit(): void {
console.log('submit');
}
}
describe('LoadingDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TestComponent, LoadingDirective],
imports: [ReactiveFormsModule]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fit('should create an instance', () => {
expect(component).toBeTruthy();
});
});
```
沒錯,正如你看的一樣,我們完全在使用測試組件代碼來輔助我們進行指令開發。啟用單元測試,效果如下:

## 使用指令
屬性性指令可以輕構的加入到組件上:
```html
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
- <button class="btn btn-primary"><i class="fa fa-save"></i>保存</button>
+ <button class="btn btn-primary" ??appLoading><i class="fa fa-save"></i>保存</button>
</form>
```
?? 關鍵字被聲明在指令對應的`selector`上
```typescript
import {Directive} from '@angular/core';
@Directive({
selector: '[??appLoading]'
})
export class LoadingDirective {
constructor() {
}
}
```
## 宿主元素
由于`appLoading`指令必須依賴于某個元素才能正常工作,所以我們把它依賴的元素稱為其宿主元素。比如上述代碼中`button`元素即為指令`appLoading`的宿主元素。
指令若要在`button`點擊時改變`button`中的圖標,則需要首先在指令中獲取這個`button`元素。在指令中,可以非常輕松的獲取到指令的宿主元素:
```typescript
+++ b/first-app/src/app/directive/loading/loading.directive.ts
@@ -1,11 +1,12 @@
-import {Directive} from '@angular/core';
+import {Directive, ElementRef} from '@angular/core';
@Directive({
selector: '[appLoading]'
})
export class LoadingDirective {
- constructor() {
+ constructor(private elementRef: ElementRef) {
+ console.log(elementRef);
}
}
```
控制臺信息如下:

控制臺中信息顯示在指令中注入的`ElementRef`有個`nativeElement`屬性,該屬性即是宿主元素`button`對應的DOM對象。有了這個宿主元素后,我們大概需要做如下幾件事:
- 獲取宿主元素的點擊事件,也宿主元素被點擊時,能夠執行我們設定的方法。
- 點宿主被點擊時,隱藏其內置的圖標。
- 點宿主被點擊時,向其內添加一個loading圖標。
- 當宿主被點擊時,設置宿主的disabled屬性。
下面我們們依次完成上述幾個功能。
## 監聽點擊
Html DOM全稱為Html Document Object Modle,簡單來說就是把Html中的每個元素都可以看做對象來處理。
比如我們獲取當前Button對象:
```typescript
constructor(private elementRef: ElementRef) {
console.log(elementRef);
const htmlButtonElement = elementRef.nativeElement as HTMLButtonElement①;
console.log(htmlButtonElement);
```
- ① 使用用`as`將其聲明為`HTMLButtonElement`的好處是可以在后面的代碼中快速使用`HTMLButtonElement`中的屬性與方法;壞處是該指令日后可能僅能夠應用到`Button`類型的宿主元素上。

Html DOM中的`addEventListener()`可以方便的設置事件的監聽,比如我們在此想監聽按鈕被點擊的事件:
```typescript
constructor(private elementRef: ElementRef) {
console.log(elementRef);
const htmlButtonElement = elementRef.nativeElement as HTMLButtonElement;
console.log(htmlButtonElement);
htmlButtonElement.addEventListener('click', () => {
console.log('宿主被點擊');
});
}
```
此時,點按鈕被點擊時,則會觸發相應的回調方法:

除此以外`addEventListener`還支持監聽其它的類型,有興趣的同學可以通過點擊本小節最后的資源列表查看學習。
## 隱藏內置圖標
隱藏內置圖標前,需要找到這個要隱藏的圖標。初始比較簡單的方法是借助瀏覽器的檢查功能。 我們當前項目引入了fontawesome圖標庫,該圖標庫把`<i class="fa fa-save"></i>`類似的代碼最終轉換成了`svg`失量圖顯示,所以最終使用瀏覽器在檢查圖標時,得到的是`svg`代碼。這也是在前面的章節中我們定義圖標與文字間的距離時為什么要使用`.btn > svg.svg-inline--fa `的原因。

此時我們需要做的便是獲取到這個`svg`元素,然后將它隱藏掉。
```typescript
+++ b/first-app/src/app/directive/loading/loading.directive.ts
@@ -11,6 +11,8 @@ export class LoadingDirective {
console.log(htmlButtonElement);
htmlButtonElement.addEventListener('click', () => {
console.log('宿主被點擊');
+ const svgElement = htmlButtonElement.querySelector('svg') as SVGElement;
+ console.log(svgElement);
});
}
```

再然后隱藏該元素就很簡單了,為該元素添加一個`display: none`的樣式即可:
```typescript
+++ b/first-app/src/app/directive/loading/loading.directive.ts
@@ -13,6 +13,7 @@ export class LoadingDirective {
console.log('宿主被點擊');
const svgElement = htmlButtonElement.querySelector('svg') as SVGElement;
console.log(svgElement);
+ svgElement.style.display = 'none';
});
}
```
此時button按鈕并點擊時,小圖標就不見了。

## 追加loading圖標
接下來再追加一個loading的圖標。想追加一個loading圖標進行,首先我們需要有一個loading圖標。
```typescript
+++ b/first-app/src/app/directive/loading/loading.directive.ts
@@ -15,7 +15,7 @@ export class LoadingDirective {
console.log(svgElement);
svgElement.style.display = 'none';
-
+ const loadingElement = document.createElement('i') as HTMLElement;
});
```
然后為這個創建的元素添加3個class值:
```typescript
const loadingElement = document.createElement('i') as HTMLElement;
+ loadingElement.classList.add('fas');
+ loadingElement.classList.add('fa-cog');
+ loadingElement.classList.add('fa-spin');
```
這樣一來,我們便使用代碼構造了一個這樣的html元素: `<i class="fas fa-cog fa-spin"></i>`,而該元素將被fontawesome處理成動起來的小齒輪。
最后,讓我們把這個動起來的小齒輪添加到原來已經隱藏掉的保存按鈕之前:
```typescript
const loadingElement = document.createElement('i') as HTMLElement;
loadingElement.classList.add('fas');
loadingElement.classList.add('fa-cog');
loadingElement.classList.add('fa-spin');
+ htmlButtonElement.insertBefore(loadingElement, svgElement);
});
```
此時當我們當擊保存按鈕時,動態的小齒輪便會替換原來的保存圖標了。

## disabled
有了前面的經驗,相信設置disabled屬性應該難不到你了吧。
```typescript
+++ b/first-app/src/app/directive/loading/loading.directive.ts
@@ -20,6 +20,8 @@ export class LoadingDirective {
loadingElement.classList.add('fa-cog');
loadingElement.classList.add('fa-spin');
htmlButtonElement.insertBefore(loadingElement, svgElement);
+
+ htmlButtonElement.disabled = true;
});
}
```

此時當我們當擊按鈕時,按鈕前的保存圖標將變更為一個動態的小齒輪,同時按鈕變更為不可用狀態。
## 重構
功能完成了只是開始,最終還需要一個看起來過得去,更容易修正的代碼。
```typescript
import {Directive, ElementRef} from '@angular/core';
@Directive({
selector: '[appLoading]'
})
export class LoadingDirective {
constructor(private elementRef: ElementRef) {
const htmlButtonElement = elementRef.nativeElement as HTMLButtonElement;
htmlButtonElement.addEventListener('click', () => {
this.buttonOnClick(htmlButtonElement);
});
}
buttonOnClick(htmlButtonElement: HTMLButtonElement): void {
// 隱藏原圖標
const svgElement = htmlButtonElement.querySelector('svg') as SVGElement;
svgElement.style.display = 'none';
// 生成小齒輪,并添加到button中
const loadingElement = document.createElement('i') as HTMLElement;
loadingElement.classList.add('fas');
loadingElement.classList.add('fa-cog');
loadingElement.classList.add('fa-spin');
htmlButtonElement.insertBefore(loadingElement, svgElement);
// 禁用按鈕
htmlButtonElement.disabled = true;
}
}
```
最終指令代碼如上。
## export
前面我們學習過,默認情況下組件、指令和管道都是模塊的私有元素。它們若想在模塊外被使用,則需要將其在模塊中拋出:
```typescript
+++ b/first-app/src/app/directive/loading/loading.module.ts
@@ -6,6 +6,9 @@ import {LoadingDirective} from './loading.directive';
declarations: [LoadingDirective],
imports: [
CommonModule
+ ],
+ exports: [
+ LoadingDirective
]
})
export class LoadingModule {
```
此時,若其它組件想使用Loading指令,則只需要在自己所在的模塊中引入LoadingModule即可。
## 使用
指令完成后,我們嘗試將其用在學生新增組件中。
第一步,在動態測試模塊中引入Loading模塊:
```typescript
+++ b/first-app/src/app/student/add/add.component.spec.ts
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddComponent],
imports: [
ReactiveFormsModule,
ClazzSelectModule,
MockApiTestingModule,
?? LoadingModule
]
})
.compileComponents();
});
```
第二步,在組件的V層中使用loading指令:
```html
+++ b/first-app/src/app/student/add/add.component.html
@@ -47,7 +47,7 @@
</div>
<div class="mb-3 row">
<div class="col-sm-10 offset-2">
- <button class="btn btn-primary" [disabled]="formGroup.invalid || formGroup.pending">
+ <button appLoading class="btn btn-primary" [disabled]="formGroup.invalid || formGroup.pending">
<i class="fa fa-save"></i>保存
</button>
</div>
```
測試效果:

## 本節作業
完成新增組件中的`onSubmit`方法。
| 名稱 | 鏈接 |
| ----------------- | ------------------------------------------------------------ |
| HTMLElement | [https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) |
| HTMLButtonElement | [https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement) |
| 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step7.2.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.2.5.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 發布部署
- 第九章 總結