按ER图首先生成基础的教师实体以备用:使用编辑器打开前台,并在shell中来到src/app/norm/entity文件夹,使用`ng g class course`生成课程实体。
```javascript
panjiedeMac-Pro:entity panjie$ ng g class course
CREATE src/app/norm/entity/course.spec.ts (154 bytes)
CREATE src/app/norm/entity/course.ts (24 bytes)
```
参考ER图完成字段及构造函数初始化工作:
```javascript
import {Teacher} from './Teacher';
/**
* 课程
*/
export class Course {
id: number;
name: string;
teacher: Teacher;
constructor(data?: { id?: number, name?: string, teacher?: Teacher }) {
if (data) {
if (data.id !== undefined) {
this.id = data.id;
}
if (data.name !== undefined) {
this.name = data.name;
}
if (this.teacher) {
this.teacher = data.teacher;
}
}
}
}
```
# 组件初始化
本组件为course模块的第一个组件,在新建组件前进行模块的初始化,然后在course模块下完成本组件的初始化。
使用shell进行src/app文件夹,使用命令生成一个带有路由模块的Course模块。
```
panjiedeMac-Pro:app panjie$ ng g m course --routing
CREATE src/app/course/course-routing.module.ts (250 bytes)
CREATE src/app/course/course.module.ts (280 bytes)
```
进入自动生成的course文件夹,进一步生成add组件:
```
panjiedeMac-Pro:app panjie$ cd course/
panjiedeMac-Pro:course panjie$ ng g c add
CREATE src/app/course/add/add.component.sass (0 bytes)
CREATE src/app/course/add/add.component.html (18 bytes)
CREATE src/app/course/add/add.component.spec.ts (607 bytes)
CREATE src/app/course/add/add.component.ts (258 bytes)
UPDATE src/app/course/course.module.ts (344 bytes)
```
## V层
src/app/course/add/add.component.html
```html
<div class="row justify-content-center">
<div class="col-4">
<h2>添加课程</h2>
<form (ngSubmit)="onSubmit()" [formGroup]="formGroup">
<div class="form-group">
<label for="name">名称</label>
<input type="text" class="form-control" id="name"
placeholder="名称" formControlName="name">
</div>
<div class="form-group">
<label>任课教师</label>
<app-teacher-select (selected)="course.teacher"></app-teacher-select>
</div>
<div class="form-group">
<label>绑定班级</label>
<div>
<label><input type="checkbox"> 班级1</label>
<label><input type="checkbox"> 班级2</label>
</div>
</div>
<div class="form-group">
<button>保存</button>
</div>
</form>
</div>
</div>
```
## C层
src/app/course/add/add.component.ts
```typescript
import { Component, OnInit } from '@angular/core';
import {FormGroup} from '@angular/forms';
import {Course} from '../../norm/entity/course';
@Component({
selector: 'app-add',
templateUrl: './add.component.html',
styleUrls: ['./add.component.sass']
})
export class AddComponent implements OnInit {
formGroup: FormGroup;
course: Course;
constructor() { }
ngOnInit() {
}
onSubmit() {
}
}
```
## 单元测试
引用ReactiveFormsModule。
src/app/course/add/add.component.spec.ts
```typescript
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AddComponent],
imports: [
ReactiveFormsModule
]
})
.compileComponents();
}));
```
![](https://img.kancloud.cn/c3/38/c338185eaa131bad1102b5a6f2739e12_688x96.png)
解析模板中的app-teacher-select已经学习过了三种方法:
1. 在测试模块中直接引入`AppTeacherSelect`组件。
2. 新建一个`selector`同为`app-teacher-select`的新组件做为AppTeacherSelect组件的替身,并在测试模块中引入该替身组件。
3. 在`TestModule`新建`selector`同为`app-teacher-select`的`AppTeacherSelect`组件,做为原`StudentModule`中的`AppTeacherSelect`组件的替身。
从笔者的实际使用经验来看,第3种方案虽然会面临一定的组件`selector`冲突风险,但仍然不失为当前的"最佳实践"。
![](https://img.kancloud.cn/40/45/404585830ced4878f4ad100539f8d680_844x666.png)
打开shell并来到项目src/app/test/component文件夹,使用如下命令建立测试专用的同名`AppTeacherSelect`组件。
```
panjiedeMac-Pro:component panjie$ ng g c TeacherSelect
CREATE src/app/test/component/teacher-select/teacher-select.component.sass (0 bytes)
CREATE src/app/test/component/teacher-select/teacher-select.component.html (29 bytes)
CREATE src/app/test/component/teacher-select/teacher-select.component.spec.ts (678 bytes)
CREATE src/app/test/component/teacher-select/teacher-select.component.ts (301 bytes)
UPDATE src/app/test/test.module.ts (633 bytes)
```
并将此组件声明到`TestModule`中的`exports`以将其作为输出项向其它模块输出。
src/app/test/test.module.ts
```typescript
exports: [
LoginComponent,
TeacherSelectComponent ➊
],
```
* ➊ 输出(暴露)组件
测试结果:
![](https://img.kancloud.cn/5b/c2/5bc25744aa4b82b89f0f5b9f60127d39_535x240.png)
提示说需要一个FormGroup实例(这说明在C层没有对formGroup进行实例化),同时于报错信息中给出了解决方案,没有比这种报错内容更贴心的了。
## FormGroupBuild
前面已经学经过使用`new FormGroup`的方法实例化`FormGroup`。
src/app/course/add/add.component.ts
```typescript
ngOnInit() {
this.formGroup = new FormGroup({
name: new FormControl('')
});
}
```
本节展示另一个更为简单的`FormBuilder`来实例化`FormGroup`,当表单项较多时使用`FormBuilder`能够降低一些创建表单的复杂度,在生产项目中是一种较常用的形式。
src/app/course/add/add.component.ts
```typescript
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.formGroup = new FormGroup({ ✘
name: new FormControl('') ✘
}); ✘
this.formGroup = this.formBuilder.group({
name: ['']
});
}
```
![](https://img.kancloud.cn/8c/98/8c98e0523635a7a3992f91a64bd10682_427x333.png)
# 名称验证
按需求,课程的名称不能为空,最少为2个字符。
## C层
src/app/course/add/add.component.ts
```typescript
ngOnInit() {
this.formGroup = this.formBuilder.group({
name: ['', [Validators.required, Validators.minLength(2)]]
});
}
```
## V层初始化
src/app/course/add/add.component.html
```html
<input type="text" class="form-control" id="name"
placeholder="名称" formControlName="name" required✚>
<small id="nameRequired" *ngIf="formGroup.get('name').dirty && formGroup.get('name').errors && formGroup.get('name').errors.required" class="form-text text-danger">请输入课程名</small> ✚
<small id="nameMinLength" *ngIf="formGroup.get('name').errors && formGroup.get('name').errors.minlength" class="form-text text-danger">课程名称不得少于2个字符</small> ✚
</div>
<button type="submit" [disabled]="formGroup.invalid" ✚>保存</button>
```
## 单元测试
参考单元测试代码如下:
src/app/course/add/add.component.spec.ts
```typescript
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {AddComponent} from './add.component';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {TestModule} from '../../test/test.module';
import {FormTest} from '../../testing/FormTest';
import {By} from '@angular/platform-browser';
describe('course -> AddComponent', () => {
let component: AddComponent;
let fixture: ComponentFixture<AddComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AddComponent],
imports: [
ReactiveFormsModule,
TestModule
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fit('should create', () => {
expect(component).toBeTruthy();
});
fit('requried校验', () => {
// 初次渲染页面时,不显示校验信息
expect(fixture.debugElement.query(By.css('#nameRequired'))).toBeNull();
// 输入了长度为1的名称,显示校验信息
const formTest: FormTest<AddComponent> = new FormTest(fixture);
formTest.setInputValue('#name', '1');
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('#nameRequired'))).toBeNull();
// 删除输入,显示required
formTest.setInputValue('#name', '');
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('#nameRequired'))).toBeDefined();
});
fit('minLength校验', () => {
// 输入长度小于2的,显示
const formTest: FormTest<AddComponent> = new FormTest(fixture);
formTest.setInputValue('#name', '1');
expect(fixture.debugElement.query(By.css('#nameMixLength'))).toBeDefined();
});
fit('button校验', () => {
// 初始化时,不能点击
let button: HTMLButtonElement = fixture.debugElement.query(By.css('button[type="submit"]')).nativeElement;
expect(button.disabled).toBeTruthy();
// 输入合格的内容后可点击
component.formGroup.get('name').setValue('1234');
fixture.detectChanges();
button = fixture.debugElement.query(By.css('button[type="submit"]')).nativeElement;
expect(button.disabled).toBeFalsy();
});
});
```
## 保存按钮点击测试
由于本组件的表单启用了字段合规性校验功能,所以在进行保存按钮测试时需要表单的值是有效的。
src/app/course/add/add.component.spec.ts
```typescript
fit('点击保存按钮', () => {
component.formGroup.get('name').setValue('1234');
fixture.detectChanges();
spyOn(component, 'onSubmit');
FormTest.clickButton(fixture, 'button[type="submit"]');
expect(component.onSubmit).toHaveBeenCalled();
});
```
![](https://img.kancloud.cn/84/80/8480c73ef07df5dbede66c73d72df9bf_193x97.png)
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.1](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.1) | - |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用