为了更加清晰的认识组件的使用方法,我们暂时停止单元测试的步代,来到教师增加组件,并在该组件中引入教师列表组件。 klass/add/add.component.html ``` <h3>新增班级</h3> <form (ngSubmit)="onSubmit()"> <label for="name">名称:<input id="name" type="text" [formControl]="name"/></label> <label for="teacherId">教师:<app-teacher-select id="teacherId"></app-teacher-select➊></label> <button>保存</button> </form> ``` * ➊ 对应教师列表组件klass/teacher-select/teacher-select.component.ts定义的`selector: 'app-teacher-select'` 然后我们启动前后台,在教师管理中添加两个测试教师后来到班级新增界面: ![](https://img.kancloud.cn/7c/6d/7c6d38addcd8aaa936188cc700004cfb_399x112.gif) 当前的界面中有两个基础组件:班级新增组件及选择教师组件。 ![](https://img.kancloud.cn/f7/6a/f76ac203e99c67cf715371a357ec3e43_460x202.png) 我们现在面临的问题是:当用户选择了某个教师后,如何将选择的教师由`教师选择组件`传递给`班级新增组件`,近而在`班级新增组件`中获取这个选择上的教师最终完成班级的保存功能。 # 事件弹射器 在前面我们提起到,用户与浏览器的一切交互都可以认为是在触发一个事件,比如鼠标点击事件,键盘输入事件。同样的,用户选择select中的某个option也是一个事件。在angular中组件通过定义`EventEmitter 事件弹射器`的方式来向处发送数据。从本质上来讲,`EventEmitter 事件弹射器`也是个可被观察者,它提供的功能是:如果本组件发生了某个事件,就会通过`EventEmitter 事件弹射器`来发送通知,如果你想获取到这些通知,那么只需要订阅我即可。 ## 初始化 klass/teacher-select/teacher-select.component.html ``` <select id="teacherSelect" [formControl]="teacherSelect" (change)="onChange()➊"> <option *ngFor="let teacher of teachers" [ngValue]="teacher"> {{teacher.name}} </option> </select> ``` * ➊ 当用户选择select中的某个option时,会触发change事件。 klass/teacher-select/teacher-select.component.ts ``` @Output()➊ selected① = new EventEmitter<Teacher>();➋ ... onChange() { console.log(this.teacherSelect.value); ➌ this.selected.emit(this.teacherSelect.value); ➍ } ``` * ➊ 此属性为组件的输出属性 * ① 此处的命名不能与angular自带的相冲突,比如说我们**不**能使用**change**来命名。 * ➋ 实例化事件发射器,此发射器发送的内容为`Teacher` * ➌ 控制台打印当前select的值(用户进行选择时,会实时的绑定到fromControl上),用于测试 * ➍ 向组件外发(弹)射数据 #### 测试 ![](https://img.kancloud.cn/47/25/47250e780091ba26ac6102c04762a64a_423x174.gif) ## 获取弹射的值 在班级新增组件中,我们尝试获取该组件弹射出的值 klass/add/add.component.html ``` <h3>新增班级</h3> <form (ngSubmit)="onSubmit()"> <label for="name">名称:<input id="name" type="text" [formControl]="name"/></label> <label for="teacherId">教师:<app-teacher-select id="teacherId" (selected)➊="onTeacherSelected($event)①"></app-teacher-select></label> <button>保存</button> </form> ``` * ➊ 使用我们刚刚在教师组件中定义的`@Output() selected`,这也是为什么我们不能够命名为`@Output() change`的原因。如果我们命名为:`@Output() change`,则此时在新增班的V层中便应该如下使用:`<app-teacher-select id="teacherId" (change)="onTeacherSelected($event)">`。此时angular会把`change()`解析为内置指令,进而使得该弹射功能失效(**请自行测试**) * ① 方法名需要在C层中定义,而参数名可以随性起,但我们一般为起名为$event以示此处为该组件的一个弹射器。 klass/add/add.component.ts ``` /** * 当选择某个教师时触发 * @param {Teacher} teacher 教师 */ onTeacherSelected(teacher: Teacher) { console.log(teacher); } ``` #### 测试 ![](https://img.kancloud.cn/38/92/389208e447cca9c8042f13dd973ed061_572x249.gif) 如上所示,用户选择教师后,我们分别在教师列表组件及班级新增组件中获取到了该项的值。 # 单元测试 完成了功能以后我们来**补充**单元测试,学习如何在单元测试中来测试组件事件弹射器。由于事件弹射器本质上来讲是一个可观察者,所以我们在测试的代码中直接来订阅这个观察者: klass/teacher-select/teacher-select.component.spec.ts ``` /** * 1. 模拟返回数据给教师列表 * 2. 观察弹射器 * 3. 模拟点击第0个option * 4. 断言观察到的数据是教师列表的第一个教师 */ fit('测试组件弹射器', () => { const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController); const req = httpTestingController.expectOne('http://localhost:8080/Teacher'); req.flush(teachers); fixture.detectChanges(); component.selected.subscribe((teacher: Teacher) => { ➊ console.log('data emit:', teacher); ① expect(teacher.name).toEqual(teachers[0].name); ➋ }); const htmlSelectElement: HTMLSelectElement = fixture.debugElement.query(By.css('#teacherSelect')).nativeElement; htmlSelectElement.value = htmlSelectElement.options[0].value; ➌ htmlSelectElement.dispatchEvent(new Event('change')); ➍ fixture.detectChanges(); ② }); ``` * ➊ 观察弹射器,并定义了弹射器弹出的数据类型为Teacher * ① 测试语句,有数据弹出时该执行此语句。若该语句未被执行,说明数据未弹出 * ➌ 设置select值的值为第一个option * ➍ 执行change事件来选中该option * ② 多写几个fixture.detectChanges(); 不吃亏 测试结果: ``` LOG: Teacher{id: 1, name: '潘杰', username: 'panjie', email: undefined, sex: undefined} Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 0 of 13 SUCCESS (0 secs / 0 secs) LOG: 'data emit', Teacher{id: 1, name: '潘杰', username: 'panjie', email: undefined, sex: undefined} Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 0 of 13 SUCCESS (0 secs / 0 secs) Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 13 (skipped 12) SUCCESS (0.123 secs / 0.099 secs) TOTAL: 1 SUCCESS TOTAL: 1 SUCCESS ``` ### 重构测试 本代码使用了与前面代码段相同的代码,本着**不造重复的轮子**的原则,对整体的文件进行重构后如下: klass/teacher-select/teacher-select.component.spec.ts ``` import {async, ComponentFixture, flush, TestBed} from '@angular/core/testing'; import {TeacherSelectComponent} from './teacher-select.component'; import {BrowserModule, By} from '@angular/platform-browser'; import {ReactiveFormsModule} from '@angular/forms'; import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; import {Teacher} from '../../norm/entity/Teacher'; describe('TeacherSelectComponent', () => { let component: TeacherSelectComponent; let fixture: ComponentFixture<TeacherSelectComponent>; const teachers = new Array(new Teacher(1, 'panjie', '潘杰'), new Teacher(2, 'zhangxishuo', '张喜硕')); beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [TeacherSelectComponent], imports: [ BrowserModule, ReactiveFormsModule, HttpClientTestingModule ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(TeacherSelectComponent); component = fixture.componentInstance; fixture.detectChanges(); }); /*断言发请了后台请求,模拟返回数据后,断言V层的select个数为2*/ it('获取教师列表后选择教师', () => { expectInit(); const htmlSelectElement: HTMLSelectElement = fixture.debugElement.query(By.css('#teacherSelect')).nativeElement; expect(htmlSelectElement.length).toBe(2); testOptionValue(htmlSelectElement); }); /** * 断言option的值与teacher中name的相同 * 循环teachers数组。断言与option的值一一相等 * @param htmlSelectElement html元素 */ const testOptionValue = (htmlSelectElement: HTMLSelectElement) => { const htmlOptionElements: HTMLCollectionOf<HTMLOptionElement> = htmlSelectElement.options; for (let i = 0; i < teachers.length; i++) { const htmlOptionElement: HTMLOptionElement = htmlOptionElements.item(i); console.log(htmlOptionElement.text); expect(htmlOptionElement.text).toEqual(teachers[i].name); } }; /** * 1. 模拟返回数据给教师列表 * 2. 观察弹射器 * 3. 模拟点击第0个option * 4. 断言观察到的数据是教师列表的第一个教师 */ fit('测试组件弹射器', () => { expectInit(); component.selected.subscribe((teacher: Teacher) => { console.log('data emit', teacher); expect(teacher.name).toEqual(teachers[0].name); }); const htmlSelectElement: HTMLSelectElement = fixture.debugElement.query(By.css('#teacherSelect')).nativeElement; htmlSelectElement.value = htmlSelectElement.options[0].value; htmlSelectElement.dispatchEvent(new Event('change')); fixture.detectChanges(); }); /** * 断言组件进行了初始化 * 访问了正确的后台地址 */ const expectInit = () => { const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController); const req = httpTestingController.expectOne('http://localhost:8080/Teacher'); expect(req.request.method).toEqual('GET'); req.flush(teachers); fixture.detectChanges(); }; }); ``` # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.5.4](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.5.4) | - | | 事件弹射器 | [EventEmitter](https://www.angular.cn/api/core/EventEmitter) | 15 |