在angular中可以简单的这样的认为:每个界面就一个组件。比如当前我们需要一个教师添加界面,那么就对应需要一个教师添加组件`TeacherAddComponent`。
# 创建组件
我们在`app`文件夹下,创建`TeacherAddComponent`的C层`teacher-add.component.ts`以及V层`teacher-add.component.html`并输入以下内容:
app/teacher-add.component.ts
```js
import {Component, OnInit} from '@angular/core';
/**
* 教师添加组件
*/
@Component({
templateUrl: './teacher-add.component.html' ➊
})
export class TeacherAddComponent implements OnInit { ➋
ngOnInit(): void {
}
}
```
* ➊ 定义项目模板`teacher-add.component.html`
* ➋ 初始化组件
teacher-add.component.html
```html
<form id="teacherAddForm">
<div>
<label for="name">姓名:</label>
<input type="text" id="name" name="name"/>
</div>
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
</div>
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email">
</div>
<div>
<label>性别:</label>
<label> <input type="radio" name="sex" value="true"> 男</label>
<label> <input type="radio" name="sex" value="false"> 女</label>
</div>
<div>
<button>提交</button>
</div>
</form>
```
# 绑定路由
app/app-routing.module.ts
```js
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {TeacherAddComponent} from './teacher-add.component';
const routes: Routes = [
{ ➊
path: 'add', ➋
component: TeacherAddComponent ➌
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
```
* ➊ routes数组中的每一项的类型均是对象`{}`。
* ➋ 该对象中的`path`属性规定了访问路径。
* ➌ 该对象中的`component`属性规定了对应的组件。
上面的过程我们还可以称为:编排路由的索引,即访问某些URL应该对应找到些组件。在英文中`map`译成名词是地图,译成动词则可取编排索引之意,所以上述过程在英文中又叫做`mapping`。以`ing`结尾的代表正在进行的动作,所以我们刚刚在绑定路由的过程中,恰恰是在`mapping`。而`mapping`在计算机的专业术语中,又称为`映射`。
# 测试
![](https://img.kancloud.cn/7c/52/7c52145d5bb11a50cca22ca124a0f9cf_1111x132.png)
这个错误大概再说:TeacherAddComponent组件并不属于任何模块`或者`它虽然属于某个模块(潜台词)但该模块并没有并导入到当前模块中。
之所以出现了该错误由于我同大家一样,在编写教程的时候,早就把教程中在前面讲过的一张图忘的一干二净了。
![](https://img.kancloud.cn/4d/be/4dbe104efb560c95003f6f1cf6f3fe83_390x403.png)
> 我们如此聪明的大脑会自动忘掉一些我们认为并不重要的事情然后为更重要的事情来腾挪出记忆空间。我们成长的过程其实是一次次地抽象、汇总、总结的过程,把原本繁琐的东西一点点的总结抽像压缩到大脑恰恰可以掌握的程度。而最终的压缩空间取决于我们在历史上对此类知识的掌握以及我们主动去思考的时间。
由上图可以看出:**组件是依赖于模块而存在的**
## 组件添加到模块
我们此时,再读1.5.1小节,还会收获:在`AppMoudle`的`@NgMoudle`注解的`declarations`选项是用来声明该模块下的组件的。
app.module.ts 变更前
```js
@NgModule({
declarations: [
AppComponent
],
```
app.module.ts 变更后
```js
import {TeacherAddComponent} from './teacher-add.component';
@NgModule({
declarations: [
AppComponent,
TeacherAddComponent
],
```
此时,我们访问:[http://localhost:4200/add](http://localhost:4200/add)地址时,控制台的错误消失。随之而来的新问题是:为什么还在显示`AppComponent`,而不是显示`TeacherAddComponent`。这是由于:
* ① 系统定义了启动组件为`AppComponent`,则在系统启动时,永远会启动该组件。
* ② 路由定义仅代表定义了转发规则,但并不代表立即启动此规则。
* ③ 由启动的的组件`AppComponent`来决定是否启用路由规则。
## 在`AppComponent`中启用路由
在`AppComponent`的V层的适当的位置添加代码`<router-outlet></router-outlet>`,则表示:➀加载路由组件(启用路由)➁将路由组件显示的内容显示在当前位置。
app.component.html
```
<router-outlet></router-outlet> ➊
<table>
<tr>
<th>序号</th>
<th>姓名</th>
<th>用户名</th>
<th>邮箱</th>
<th>性别</th>
<th>注册时间</th>
<th>操作</th>
</tr>
<tr *ngFor="let teacher of teachers" >
<td>{{teacher.id}}</td>
<td>{{teacher.name}}</td>
<td>{{teacher.username}}</td>
<td>{{teacher.email}}</td>
<td>
<span *ngIf="teacher.sex">女</span>
<span *ngIf="!teacher.sex">男</span>
</td>
<td>{{teacher.createTime | date: 'yyyy-MM-dd'}}</td>
<td>删除</td>
</tr>
</table>
```
* ➊ 放到教师列表前面
效果:
![](https://img.kancloud.cn/01/6c/016c7c32a76f3b0e1309a8081a18d66e_485x208.png)
```html
<table>
<tr>
<th>序号</th>
<th>姓名</th>
<th>用户名</th>
<th>邮箱</th>
<th>性别</th>
<th>注册时间</th>
<th>操作</th>
</tr>
<tr *ngFor="let teacher of teachers" >
<td>{{teacher.id}}</td>
<td>{{teacher.name}}</td>
<td>{{teacher.username}}</td>
<td>{{teacher.email}}</td>
<td>
<span *ngIf="teacher.sex">女</span>
<span *ngIf="!teacher.sex">男</span>
</td>
<td>{{teacher.createTime | date: 'yyyy-MM-dd'}}</td>
<td>删除</td>
</tr>
</table>
<router-outlet></router-outlet> ➊
```
* ➊放至教师列表后面
![](https://img.kancloud.cn/11/bd/11bd40daabf785e650559b51a279617c_514x215.png)
# 本节小测
尝试再建立`TeacherEditComponent`组件,将其映射到`edit`地址上并测试。
## 上节答案
由于`bootstrap`属性的接收值是一个数组,那么我认为是可以输入多个启动组件的。为了验证自己想的是否正确,我直接将`TeacherAddComponent`也加入到启动的列表中。
```js
import {TeacherAddComponent} from './teacher-add.component';
bootstrap: [AppComponent, TeacherAddComponent]
```
访问[http://localhost:4200/](http://localhost:4200/)结果:
`TeacherAddComponent_Host.ngfactory.js? [sm]:1 ERROR Error: The selector "ng-component" did not match any elements`
> 结论一:不行,启动组件只支持一个。
其实到这我们直接记往该结论就可以了,在很多项目中我们的确只使用一个`AppComponent`组件就够了,这个结论虽然不见得是`绝对正确`的,但已经解决了我们当前碰到的问题,本着`够用的就是最好的`原则我们已经圆满的完成了任务。
*****
前面我们讲过知识就是在不断的证真与证违中积累及升华的。下面我进一步的展示一下该理论。
看报错提示好像与启动组件的个数无关,下面我继承测试:在启动属性上只保留`TeacherAddComponent`,删除原`AppComponent`组件,看是否正常启动`TeacherAddComponent`。
```js
bootstrap: [TeacherAddComponent]
```
结果仍然是:
`TeacherAddComponent_Host.ngfactory.js? [sm]:1 ERROR Error: The selector "ng-component" did not match any elements`
此时,便发生了证违。即证明结论是`违`的,这将是对此知识点升华的开始。
> 结论二:结论一不一定正确。
*****
观察报错信息,说这个"ng-component" `selector` 没有匹配上任何的元素。然后我们通过观察`AppComponent`与`TeacherAddComponent`发现:`TeacherAddComponent`较`AppComponent`少了个`selector`属性
app.component.ts
```js
@Component({
selector: 'app-root', ➊
templateUrl: './app.component.html',
styleUrls: ['./app.component.sass']
})
/**
* 实现OnInit接口,该接口规定了ngOnInit方法。
* angular在组件准备完毕后,将自动调用ngOnInit方法
*/
export class AppComponent implements OnInit {
```
* ➊此处有selector属性
teacher-add.component.ts
```js
@Component({ ➊
templateUrl: './teacher-add.component.html'
})
export class TeacherAddComponent implements OnInit {
ngOnInit(): void {
}
}
```
➊ 未添加selector
尝试添加selector
teacher-add.component.ts
```js
@Component({
selector: 'app-teacher-add',
templateUrl: './teacher-add.component.html'
})
```
此时报错信息变成了:
`TeacherAddComponent_Host.ngfactory.js? [sm]:1 ERROR Error: The selector "app-teacher-add" did not match any elements`
看来的确有影响,但却没有解决根本问题。
> 结论:是否配置selector对启动成功与否无影响 。如果未给组件配置selector,则系统为其添加默认selector => `ng-component`。
*****
我们继续瞎猜加验证。刚刚报错的`app-teacher-add`是我刚刚手动添加的,然后报错了。那么以前使用`AppComponent`,其中的`selector`是`app-root`为什么不报错呢?试试将两个组件的`selector`换一下:
teacher-add.component.ts
```js
@Component({
selector: 'app-root',
templateUrl: './teacher-add.component.html'
})
export class TeacherAddComponent implements OnInit {
ngOnInit(): void {
}
}
```
app.component.ts
```js
@Component({
selector: 'app-teacher-add',
templateUrl: './app.component.html',
styleUrls: ['./app.component.sass']
})
/**
* 实现OnInit接口,该接口规定了ngOnInit方法。
* angular在组件准备完毕后,将自动调用ngOnInit方法
*/
export class AppComponent implements OnInit {
```
结果:
![](https://img.kancloud.cn/a6/7c/a67c21674248bc1e9d60eba16e1a335f_278x192.png)
正常显示了!
> 结论:可以变更启动的组件,但该组件的`selector`必须是`app-root`
*****
我们继续猜:该组件的`selector`必须是`app-root`,为什么不可以是其它呢?可以在哪指定了`app-root`这个值。全项目搜索下看看能不能找到这个指定的它的位置。
![](https://img.kancloud.cn/b0/67/b0678bc5653a11b7ccaab381d1702cd0_960x207.png)
从搜索的结果上不难看出,原来在`index.html`中规定了这个`app-root`,打开该文件然后将其修改为:`app-teacher-add`呢?
index.html
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebApp</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-teacher-add></app-teacher-add>
</body>
</html>
```
结果:
`TeacherAddComponent_Host.ngfactory.js? [sm]:1 ERROR Error: The selector "app-root" did not match any elements`
我好像懂了:启动时找`TeacherAddComponent`没错,`TeacherAddComponent`的selector为`app-root`(我们前面刚改了),系统启动时在`index.html`中查找该selector`app-root`,如果没找到则报 `The selector "app-root" did not match any elements`,即这个选择器没有匹配任何元素的错误。
> 结论三:启用组件的selector必须在index.html中被使用。
*****
最后,我们再来彻底推翻原结论一,即启动组件只能有一个。
我们先进行组件的恢复,将`TeacherAddComponent`的`selector`设置为`app-teacher-add`,将`AppComponent`的`selector` 设置为`app-root`。保持`index.html`中的`<app-teacher-add></app-teacher-add>`不变。
系统正常显示教师添加界面。
然后将两个组件都添加到启动中:
```js
bootstrap: [TeacherAddComponent, AppComponent]
```
再将`app-root`也添加到`index.html`中。
index.html
```html
<body>
<app-teacher-add></app-teacher-add>
<app-root></app-root>
</body>
```
测试:
![](https://img.kancloud.cn/b3/f4/b3f4d18cb1d020038e8b77b7d9473a5d_493x276.png)
> 结论四:启用组件可以为多个,但每个组的selector必须在index.html中被使用。
**总结:** 结论四并不见得正确,但它此时已经解决了我前面所有的问题。在当前来讲,已经是最适用的合理的答案。
> 结论是否绝对正确并不重要,重要的是它是否适用于当前自己遇到的所有同类问题。
[源码地址](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step2.3.1.0)
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 入口组件 | [https://www.angular.cn/guide/entry-components](https://www.angular.cn/guide/entry-components) | 10 |
| 启动过程 | [https://www.angular.cn/guide/bootstrapping](https://www.angular.cn/guide/bootstrapping) | 15 |
| 路由配置 | [https://www.angular.cn/guide/router#configuration](https://www.angular.cn/guide/router#configuration) | 5 |
| 路由出口 | [https://www.angular.cn/guide/router#router-outlet](https://www.angular.cn/guide/router#router-outlet) | 5 |
| 组件简介 | [https://www.angular.cn/guide/architecture-components](https://www.angular.cn/guide/architecture-components) | 25 |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step2.3.1](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step2.3.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
- 总结
- 开发规范
- 备用