在同时启动后台、前台的基础上,我们进行对接测试,打开地址:[http://localhost:4200/edit/1](http://localhost:4200/edit/1),得到如下结果并且未在控制台中发生错误,说明对接成功。
![](https://img.kancloud.cn/67/6a/676a90e13a3eded2f44ef739c4e3b9a5_599x300.png)
# 表单绑定
在新增教师的小节中,我们学习了数据双向绑定,成功的实现了在C层中获取V层表单中的最新数据。本节我们在新增教师代码的基础上,稍做调整。
TeacherEditComponent
```
export class TeacherEditComponent implements OnInit {
...
/**
* 提交表单
*/
onSubmit(): void {
console.log('submit');
}
}
```
teacher-edit.component.html
```
<pre>{{teacher | json}}</pre>
<form id="teacherEditForm①" (ngSubmit)="onSubmit()"> ②
<div>
<label for="name">姓名:</label>
<input type="text" id="name" name="name" [(ngModel)]="teacher.name"/> ➊
</div>
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username" [(ngModel)]="teacher.username"> ➊
</div>
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" [(ngModel)]="teacher.email"> ➊
</div>
<div>
<label>性别:</label>
<label> <input type="radio" name="sex" value="true" [(ngModel)]="teacher.sex"> 男</label> ➊
<label> <input type="radio" name="sex" value="false" [(ngModel)]="teacher.sex"> 女</label> ➊
</div>
<div>
<button>提交</button>
</div>
</form>
```
* ➊ 由以前直接绑定`name`等变量,改为绑定`teacher`对象的`name`属性。
## 测试
浏览[http://localhost:4200/edit/1](http://localhost:4200/edit/1)并测试:
![](https://img.kancloud.cn/98/33/9833a44aecf426656e69c0f4ded553a5_449x361.png)
虽然大体得到了我们想要的结果,查控制台中发生了一个错误:
```
TeacherEditComponent.html:5 ERROR TypeError: Cannot read property 'name' of undefined
at Object.eval [as updateDirectives] (TeacherEditComponent.html:5)
```
它在说在TeacherEditComponent.html的第5行上发生了一个类型错误,不能够在`undefined`上读取`'name'`属性。该错误产生的原因是这样:
TeacherEditComponent
```
export class TeacherEditComponent implements OnInit {
public teacher: any; ➊
...
ngOnInit(): void { ➋
const id = this.route.snapshot.paramMap.get('id');
const url = 'http://localhost:8080/Teacher/' + id;
this.httpClient.get(url)
.subscribe((data) => {
this.teacher = data; ➍
}, () => {
console.log(`请求 ${url} 时发生错误`);
});
} ➌
...
}
```
组件的执行顺序如下:
* ➊ 定义了变量teacher,声明类型为任意类型`any`,并使用`undefined`对其进行初始化。
* ➋ V层渲染前,执行`ngOnInit()`方法。
* ➌ 该方法执行完毕后,渲染V层。
* ☀ V层第一次渲染,由于`teacher`的值为`undefined`所以控制台报错。
* ☀ V层第二次渲染,由于`teacher`的值为`undefined`所以控制台报错。
* ➍ 用获取到的值对`teacher`赋值。
* ☀ V层第三次渲染,由于`teacher`当前的值为对象,且存在`name`属性,所以成功的进行绑定。
> 至于为什么会在执行➍以前渲染两次V层并不属于本教程的讨论范围,如果你真的对它有极浓厚的兴趣。可以在教程学习完成后,尝试阅读:[生生命周期勾子](https://www.angular.cn/guide/lifecycle-hooks#lifecycle-hooks),或许你能找到自己想要的答案。
发生错语的原因找到了,那么解决该错误的方法就很简单了。
TeacherEditComponent
```
public teacher: any; ✘
public teacher: any = {}; ✚ ➊
```
> 在教程中我们将使用`✘`来表示由原文件中删除该行代码,用`✚`来表示在文件中增加代码。所以上述代码的实现含义为:将`public teacher: any;`修改为:`public teacher: any = {};`
* ➊ 定义变量的同时使用`{}`来赋初值,规避了变量定义后默认为`undefined`的问题。
> `{}`和`undefined`是有本质区别的。`{}` :存在该对象,但该对象是张白纸;`undefined`不存在该对象。比如现在我计划养一条白毛狗,并把狗的名字起名为`大白`,此时`大白`就等于是`undefined`;过后的几天我真的买回来了一条刚出生的小狗,而它就是我的`大白`,此时`大白`就等于是`{}`。
再次测试控制台打印的异常便如期消失了。
## true与"true"
但最终的问题仍然没有修正,通过查看V层的打印数据我们发现后台是成功了回传了教师的性别sex字段的。但在V层中却没有成功的选重性别,这是为什么呢?
![](https://img.kancloud.cn/12/c0/12c00c223eaf014c4b9d17de8c905c43_301x277.png)
我们随便的点击一个性别观察发生了什么:
![](https://img.kancloud.cn/91/13/911332a6e1e8b0de63755d74ee84e543_220x236.png)
后台返回的是`true`,而点击V层的性别后,设置的值是`"true"`。问题的关键就在于此:`true`的类型为`boolean`,在数据存储时占用1个bit;而`"true"`是个`string`,在数据存储时,最少占用4*8 = 32个bit。`true`与`"true"`仅仅是在显示时长的像了一些而已。那么解决这个问题的方法应该有两个:1 让`spring`返回性别的时候,返回字符串代替boolean。2 angular V层中进行性别绑定时,使用boolean代替字符串。
我们在开发系统的时候,特别是前后台进行对接的时候经常会遇到此类的问题,这时候就应该讨论下事情的本质应该是个什么样子了。比如性别字段,它的本质就是`boolean`非真既假,那么此时后台的数据返回就没有错,处理该问题需要来处理前台。在`angualr`中的模板驱动表单中,可以使用`[value]`指令来绑定表单中的`boolean`值。
teacher-edit.component.html
```
<label> <input type="radio" name="sex" value="true" [(ngModel)]="teacher.sex"> 男</label> ✘
<label> <input type="radio" name="sex" value="false" [(ngModel)]="teacher.sex"> 女</label> ✘
<label> <input type="radio" name="sex" [value]="true" [(ngModel)]="teacher.sex"> 男</label> ✚
<label> <input type="radio" name="sex" [value]="false" [(ngModel)]="teacher.sex"> 女</label> ✚
```
保存文件后浏览器自动刷新后成功的绑定了教师的姓别。
*****
甲方技术认为:我需要快速的来解决我所遇到的当前问题,只要能解决问题那么就是好的,什么数据库的第一范式、第二范式都是狗屁,对什么面向接口、面向对象全部嗤之以鼻。
我们认为:事情应该是个什么样子就得是个什么样子,规范化才能规避一些意想不到的问题,才能够最大限度的减少文档,减少大家的沟通成本。这是一种本质的开发理念的冲突,坚持规范可能并不是最快的解决方案,但一定是最优的解决方案。
*****
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 使用ngModel进行双向数据绑定 | [https://www.angular.cn/guide/forms#two-way-data-binding-with-ngmodel](https://www.angular.cn/guide/forms#two-way-data-binding-with-ngmodel) | 5 |
| 生命周期勾子 | [https://www.angular.cn/guide/lifecycle-hooks](https://www.angular.cn/guide/lifecycle-hooks) | 15 |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step2.4.3](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step2.4.3) | - |
- 序言
- 第一章: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
- 总结
- 开发规范
- 备用