新增课程时需要选择绑定的班级,班级的个数可以是多个,此时适用于使用checkbox实现。为增加其复用性,本节将其直接打造为一个共享组件。并在课程新增中使用该组件完成班级的选择。 # 功能开发 公用的组件加入到CoreModule中: ``` panjiedeMac-Pro:web-app panjie$ cd src/app/core/ panjiedeMac-Pro:core panjie$ ng g c multipleSelect CREATE src/app/core/multiple-select/multiple-select.component.sass (0 bytes) CREATE src/app/core/multiple-select/multiple-select.component.html (30 bytes) CREATE src/app/core/multiple-select/multiple-select.component.spec.ts (685 bytes) CREATE src/app/core/multiple-select/multiple-select.component.ts (305 bytes) UPDATE src/app/core/core.module.ts (486 bytes) ``` ## V层 src/app/core/multiple-select/multiple-select.component.html ```html <div> <label class="label" *ngFor="let data of list$➊ | async➋"> <input type="checkbox" (change)="onChange(data)➌"> {{data.name}} </label> </div> ``` * ➊ 定义变量时以$结尾,表明该变量为一个可观察(订阅)的数据源 * ➋ angular提供的async管道实现了自动订阅观察的数据源 * ➌ checkbox被点击时触发change事件 >[success] async 管道会订阅一个 Observable 或 Promise,并返回它发出的最近一个值。 当新值到来时,async 管道就会把该组件标记为需要进行变更检测。当组件被销毁时,async 管道就会自动取消订阅,以消除潜在的内存泄露问题。 src/app/core/multiple-select/multiple-select.component.sass ```sass label margin-right: 1em ``` ## C层 src/app/core/multiple-select/multiple-select.component.ts ```typescript import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import {Observable} from 'rxjs'; @Component({ selector: 'app-multiple-select', templateUrl: './multiple-select.component.html', styleUrls: ['./multiple-select.component.sass'] }) export class MultipleSelectComponent implements OnInit { /** 数据列表 */ @Input() list$: Observable<Array<{ name: string }>>; /** 事件弹射器,用户点选后将最终的结点弹射出去 */ @Output() changed = new EventEmitter<Array<any>>(); constructor() { } /** 用户选择的对象 */ selectedObjects = new Array<any>(); ngOnInit() { } /** * 点击某个checkbox后触发的函数 * 如果列表中已存在该项,则移除该项 * 如果列表中不存在该项,则添加该项 */ onChange(data: any) { let found = false; this.selectedObjects.forEach((value, index) => { if (data === value) { found = true; this.selectedObjects.splice(index, 1); } }); if (!found) { this.selectedObjects.push(data); } this.changed.emit(this.selectedObjects); } } ``` ## 单元测试 src/app/core/multiple-select/multiple-select.component.spec.ts ```typescript import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; import {MultipleSelectComponent} from './multiple-select.component'; import {Subject} from 'rxjs'; import {Course} from '../../norm/entity/course'; import {By} from '@angular/platform-browser'; describe('MultipleSelectComponent', () => { let component: MultipleSelectComponent; let fixture: ComponentFixture<MultipleSelectComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [MultipleSelectComponent], imports: [] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(MultipleSelectComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('数据绑定测试', () => { ➊ const subject = new Subject<Course[]>(); component.list$ = subject.asObservable(); // 由于V层直接使用了异步async管道绑定的观察者 // 所以在给C层对list$赋值后 // 使用detectChanges将此值传送给V层 fixture.detectChanges(); // 当V层成功的绑定到数据源后,使用以下代码发送数据,才能够被V层接收到 subject.next([new Course({id: 1, name: 'test1'}), new Course({id: 2, name: 'test2'})]); fixture.detectChanges(); // 断言生成了两个label const div = fixture.debugElement.query(By.css('div')); expect(div.children.length).toBe(2); }); it('点选测试', () => { // 准备观察者 const subject = new Subject<Course[]>(); component.list$ = subject.asObservable(); fixture.detectChanges(); // 发送数据 const course = new Course({id: 1, name: 'test1'}); subject.next([course]); fixture.detectChanges(); // 获取checkbox并点击断言 const checkBox: HTMLInputElement = fixture.debugElement.query(By.css('input[type="checkbox"]')).nativeElement as HTMLInputElement; spyOn(component, 'onChange'); checkBox.click(); expect(component.onChange).toHaveBeenCalledWith(course); }); it('onChange -> 选中', () => { expect(component.selectedObjects.length).toBe(0); const course = new Course(); component.onChange(course); expect(component.selectedObjects.length).toBe(1); expect(component.selectedObjects[0]).toBe(course); }); it('onChange -> 取消选中', () => { const course = new Course(); component.selectedObjects.push(course); expect(component.selectedObjects.length).toBe(1); component.onChange(course); expect(component.selectedObjects.length).toBe(0); }); it('onChange -> 弹射数据', () => { let result: Array<any>; component.changed.subscribe((data) => { result = data; }); const course = new Course(); component.onChange(course); expect(result[0]).toBe(course); }); }); ``` 在V中使用了async管道实现了自动订阅数据源的功能。➊ 处对应的数据流大概如下: ![](https://img.kancloud.cn/dd/f5/ddf58284544635592c0a01de09991603_640x480.gif) # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.3](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.3) | - | | AsyncPipe | [https://angular.cn/api/common/AsyncPipe](https://angular.cn/api/common/AsyncPipe) | 5 |