属性型指令的运用需要有一些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> ``` 此时将所有的字段都符合要求时,将如下显示保存按钮: ![image-20210414101547572](https://img.kancloud.cn/c9/0e/c90ee9c91ef995c0da8f7b4ffb646fae_1152x224.png) 现在我们想实现一个加载中效果,即:当后台发起请求时,保存的这个小图标变成一个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> ``` ![image-20210414101907128](https://img.kancloud.cn/95/8a/958ac82d3fb9d4f1c4d98521076ca12f_1448x198.png) 同时,为保存按钮为添加`disable`属性,使其不能再次被点击: ![image-20210414102036368](https://img.kancloud.cn/4b/ad/4bad3fd8cb4751b1e817b6fc0465c1d9_1364x218.png) 好的,预实现的效果有了,让我们恢复一下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(); }); }); ``` 没错,正如你看的一样,我们完全在使用测试组件代码来辅助我们进行指令开发。启用单元测试,效果如下: ![image-20210414105319307](https://img.kancloud.cn/e9/d2/e9d23152c9352a278b622683a1c8de59_718x174.png) ## 使用指令 属性性指令可以轻构的加入到组件上: ```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); } } ``` 控制台信息如下: ![image-20210414111710023](https://img.kancloud.cn/4a/88/4a880f0df7c35b3cb9f48c75163e926e_1424x108.png) 控制台中信息显示在指令中注入的`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`类型的宿主元素上。 ![image-20210414121125049](https://img.kancloud.cn/1a/5e/1a5ef6d2dc862b08d5492629cb60757d_994x92.png) 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('宿主被点击'); }); } ``` 此时,点按钮被点击时,则会触发相应的回调方法: ![image-20210414121303204](https://img.kancloud.cn/41/10/411002a12a1aa93ca0cc6eb7dd85bb45_1366x178.png) 除此以外`addEventListener`还支持监听其它的类型,有兴趣的同学可以通过点击本小节最后的资源列表查看学习。 ## 隐藏内置图标 隐藏内置图标前,需要找到这个要隐藏的图标。初始比较简单的方法是借助浏览器的检查功能。 我们当前项目引入了fontawesome图标库,该图标库把`<i class="fa fa-save"></i>`类似的代码最终转换成了`svg`失量图显示,所以最终使用浏览器在检查图标时,得到的是`svg`代码。这也是在前面的章节中我们定义图标与文字间的距离时为什么要使用`.btn > svg.svg-inline--fa `的原因。 ![image-20210414133818309](https://img.kancloud.cn/85/90/85900109bdc04cd22d4d13622589096b_2342x496.png) 此时我们需要做的便是获取到这个`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); }); } ``` ![image-20210414134208253](https://img.kancloud.cn/81/e9/81e95feb1af8e0cf82aa70651fbdd6f7_2770x214.png) 再然后隐藏该元素就很简单了,为该元素添加一个`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按钮并点击时,小图标就不见了。 ![image-20210414134444312](https://img.kancloud.cn/31/be/31be82777e904c19d40ed8458ac3bc0c_1222x562.png) ## 追加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); }); ``` 此时当我们当击保存按钮时,动态的小齿轮便会替换原来的保存图标了。 ![image-20210414140055143](https://img.kancloud.cn/1e/60/1e60757dfe416a23b40c11d66a251feb_490x158.png) ## 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; }); } ``` ![image-20210414140213686](https://img.kancloud.cn/d8/ee/d8eeee02332d5e8ccf1298c0527879eb_740x154.png) 此时当我们当击按钮时,按钮前的保存图标将变更为一个动态的小齿轮,同时按钮变更为不可用状态。 ## 重构 功能完成了只是开始,最终还需要一个看起来过得去,更容易修正的代码。 ```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> ``` 测试效果: ![image-20210414102036368](https://img.kancloud.cn/4b/ad/4bad3fd8cb4751b1e817b6fc0465c1d9_1364x218.png) ## 本节作业 完成新增组件中的`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) |