我们在`src/app/clazz`文件夹中,新建klassSelect组件: > 笔者在这犯了一个命名错误,组件的名称应该是`TeacherSelect`,而不是`KlassSelect`。 ```bash panjie@panjies-Mac-Pro clazz % pwd /Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/clazz panjie@panjies-Mac-Pro clazz % ng g c klassSelect CREATE src/app/clazz/klass-select/klass-select.component.css (0 bytes) CREATE src/app/clazz/klass-select/klass-select.component.html (27 bytes) CREATE src/app/clazz/klass-select/klass-select.component.spec.ts (662 bytes) CREATE src/app/clazz/klass-select/klass-select.component.ts (298 bytes) UPDATE src/app/clazz/clazz.module.ts (409 bytes) ``` 然后把班级添加组件中的相关的V层代码复制过来: ```html +++ b/first-app/src/app/clazz/klass-select/klass-select.component.html <select id="teacher" class="form-control" name="teacher" [(ngModel)]="clazz.teacherId"> <option *ngFor="let teacher of teachers" [ngValue]="teacher.id"> {{teacher.name}} </option> </select> ``` 再把班级添加组件中的相关的C层代码复制过来: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts export class KlassSelectComponent implements OnInit { teachers = new Array<Teacher>(); teacherId: number | undefined; constructor(private httpClient: HttpClient) { } ngOnInit(): void { // 获取所有教师 this.httpClient.get<Array<Teacher>>('teacher') .subscribe(teachers => this.teachers = teachers); } } ``` 最后变更当V层中的`ngModel`,并使用`ng t`开启测试。 ```html +++ b/first-app/src/app/clazz/klass-select/klass-select.component.html @@ -1,6 +1,5 @@ <select id="teacher" class="form-control" - [(ngModel)]="clazz.teacherId"> + [(ngModel)]="teacherId"> name="teacher"> <option *ngFor="let teacher of teachers" [ngValue]="teacher.id"> {{teacher.name}} </option> ``` ## MockApi 在单元测试的动态测试模块中,引用`MockApiInterceptor`以及`HttpModule`,并加`TeacherMockApi`以提供teacher相关的后台模块数据。 ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts @@ -1,6 +1,9 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {KlassSelectComponent} from './klass-select.component'; +import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; +import {MockApiInterceptor} from '@yunzhi/ng-mock-api'; +import {TeacherMockApi} from '../../mock-api/teacher.mock.api'; describe('KlassSelectComponent', () => { let component: KlassSelectComponent; @@ -8,7 +11,18 @@ describe('KlassSelectComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [KlassSelectComponent] + declarations: [KlassSelectComponent], + imports: [ + HttpClientModule + ], + providers: [ + { + provide: HTTP_INTERCEPTORS, multi: true, + useClass: MockApiInterceptor.forRoot([ + TeacherMockApi + ]) + } + ] }) .compileComponents(); }); ``` 最后也是最关键的,保证整个项目中仅有当前一个 `fit`,并且在其中启用自动检测变更机制: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts @@ -35,5 +35,6 @@ describe('KlassSelectComponent', () => { fit('should create', () => { expect(component).toBeTruthy(); + fixture.autoDetectChanges(); }); }); ``` ![image-20210324084043748](https://img.kancloud.cn/ff/ab/ffab5f83c4049bc5e37438562aa276db_964x188.png) 效果有了,还需要关注控制台: ![image-20210324085431814](https://img.kancloud.cn/12/28/12282d2af2a7cb945a36030064ba49a7_1130x102.png) 提示说`option`上不能绑定`ngValue`,原因是`ngValue`并不是个已知的属性。请尝试解决后继续学习。 ## @Output() 教师被用户选择后,我们需要将被选择的数据通过`output()`方法将数据弹出。在前面的章节中我们已经学习过`@Output()`的使用方法,在此我们大概可以总结出:如果一个组件需要向外传送数据,则需要加入`@Output()`注解,该注解的字段类型为`EventEmitter`,用于向父组件弹射数据。 ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts @@ -1,7 +1,8 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, OnInit, EventEmitter, Output} from '@angular/core'; import {Teacher} from '../../entity/teacher'; import {HttpClient} from '@angular/common/http'; + @Component({ selector: 'app-klass-select', templateUrl: './klass-select.component.html', @@ -11,6 +12,9 @@ import {HttpClient} from '@angular/common/http'; export class KlassSelectComponent implements OnInit { teachers = new Array<Teacher>(); teacherId: number | undefined; + @Output() + beChange = new EventEmitter<any>(); + constructor(private httpClient: HttpClient) { } ``` 而至于谁会成为自己的父组件,当前组件并不关心也不需要关心,因为成为自己父组件的前提仅仅是在组件的V层中引入`app-klass-select`,所以任意组件都可以成为自己的父组件。而无论谁成为自己的父组件,当前组件均是通过`@Output()`向其弹值。 ## 数据监听 教师能选择了,`@Output`也有了。当下的问题是如何感知用户选择了某个教师并在选择后通过`@Output()`向外发送通知。Angular最少提供了两种方案来解决此类问题。 > 这种对数据的感知又被称为**数据监听Data Listen**,完成这项功能的又被称为**数据监听器 Data Listener**。 ### (change)事件 Angular提供的`(change)`事件可以感知表单项的变化,在变化时被调用一次: ```html +++ b/first-app/src/app/clazz/klass-select/klass-select.component.html @@ -1,6 +1,7 @@ <select id="teacher" class="form-control" name="teacher" - [(ngModel)]="teacherId"> + [(ngModel)]="teacherId" + (change)="onChange()"> <option *ngFor="let teacher of teachers" [ngValue]="teacher.id"> {{teacher.name}} </option> ``` C层创建对应的方法: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts @@ -24,4 +24,7 @@ export class KlassSelectComponent implements OnInit { .subscribe(teachers => this.teachers = teachers); } + onChange(): void { + console.log('change called'); + } } ``` 此时,我们选择教师列表中的教师时,C层中的`onChange`方法将被调用一次: ![image-20210324090833599](https://img.kancloud.cn/6a/9b/6a9b9e1cdd8e8251a1e9cc91d01cc148_1428x436.png) 如此以来,选择教师后调用`onChange()`方法,接着便可以在`onChange()`方法中向外弹出选择的教师ID了。 ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts @@ -24,4 +24,9 @@ export class KlassSelectComponent implements OnInit { .subscribe(teachers => this.teachers = teachers); } + onChange(): void { + console.log('change called'); + console.log(this.teacherId); + this.beChange.emit(this.teacherId); + } } ``` #### 测试 凡事都不能想当然,除非你有百分百的把握(即使是这样,实际中也会犯错 ),数据弹射出是否被成功接收,还需要进行测试: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts @@ -38,5 +38,7 @@ describe('KlassSelectComponent', () => { fit('should create', () => { expect(component).toBeTruthy(); fixture.autoDetectChanges(); + component.beChange + .subscribe((data: any) => console.log('接收到了弹出的数据', data)); }); }); ``` ![image-20210324092704858](https://img.kancloud.cn/37/86/3786cbc6b3944be981b64a928c4ada80_918x168.png) 最后,我们规范一下`beChange`事件弹出器的数据类型: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts @@ -13,7 +13,7 @@ export class KlassSelectComponent implements OnInit { teacherId: number | undefined; @Output() - beChange = new EventEmitter<any>(); + beChange = new EventEmitter<number>(); constructor(private httpClient: HttpClient) { } ``` 同步修正单元测试: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.spec.ts @@ -39,6 +39,6 @@ describe('KlassSelectComponent', () => { expect(component).toBeTruthy(); fixture.autoDetectChanges(); component.beChange - .subscribe((data: any) => console.log('接收到了弹出的数据', data)); + .subscribe((data: number) => console.log('接收到了弹出的数据', data)); }); }); ``` ### 响应式表单 `(beChange)`事件虽然可用,但实际上却是沿用了AngularJS(Angular的前身)的思路。而Angular则提供了效率更高,更加面向对应的响应式表单来解决此类问题。 由于**响应式表单**是Angular的核心知识点之一,我们单独在下下节内容中进行讲解。 | 名称 | 链接 | | --------- | ------------------------------------------------------------ | | OnChanges | [https://angular.cn/api/core/OnChanges](https://angular.cn/api/core/OnChanges) | | 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.2.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.2.1.zip) |