开发分页方法有两个方法,第一个是直接在当前的clazz列表组件开发,第二个是将分页专门的剥离出一个组件。虽然第二个方法更正综,但我们初期完成某个功能时,往往是先采用第一种,待使用第一种完成功能后,再使用第二种。
在此仍然采用step by step的敏捷开发模式,所谓的敏捷就是指把大的复杂的功能拆分成小的可衡量的功能模块。然后一步步完成各个功能模块,完成一点测试一点,直至最终功能完成。
## 静态分页
我们先实现一个静态分页,即html中的页数是固定的,但点击页码却真实的触发C层的分页方法,重新按页数请求数据。
```html
+++ b/first-app/src/app/clazz/clazz.component.html
@@ -32,10 +32,10 @@
<nav class="row justify-content-md-center">
<ul class="pagination col-md-auto">
- <li class="page-item disabled"><a class="page-link" href="#">上一页</a></li>
- <li class="page-item active"><a class="page-link" href="#">1</a></li>
- <li class="page-item"><a class="page-link" href="#">2</a></li>
- <li class="page-item"><a class="page-link" href="#">3</a></li>
- <li class="page-item"><a class="page-link" href="#">下一页</a></li>
+ <li class="page-item disabled"><span class="page-link">上一页</span></li>
+ <li class="page-item active"><span class="page-link" (click)="onPage(1)">1</span></li>
+ <li class="page-item"><span class="page-link" (click)="onPage(2)">2</span></li>
+ <li class="page-item"><span class="page-link" (click)="onPage(3)">3</span></li>
+ <li class="page-item"><span class="page-link">下一页</span></li>
</ul>
```
上述代码使用了`span`标签来替换`a`标签,加入了`onPage()`方法并对应传入了页码,在C层建立`onPage()`方法如下:
```typescript
+ onPage(page: number): void {
+ console.log(page);
+ }
```
然后在V层点击相应的分页,查看控制台变化:
![image-20210330163243332](https://img.kancloud.cn/fb/16/fb16e6bd2c533699fc7a6a3ac2e49070_1666x318.png)
触发了C层的方法后, 我们便可以利用该分页的值去请求对应的后台数据了:
```typescript
this.httpClient.get<Page<Clazz>>('/clazz/page?page=' + page.toString())
.subscribe(pageData => {
this.pageData = pageData;
console.log(pageData);
});
```
在上述代码中,我们接收了带有`page`信息的请求地址,这样以来后台便可以接收到`page`的信息从而返回当前页码的数据了。但由于我们引用的`MockApi`天生存在一些缺陷的原因,导致MockApi无法匹配URL中的参数,所以此时点击分页时将得到一个如下错误:
![image-20210331082004280](https://img.kancloud.cn/29/0d/290dfa576d9accce87270ac5015b36b9_2108x130.png)
> MockApi是团队的一个开源项目,地址为:[https://github.com/yunzhiclub/ng](https://github.com/yunzhiclub/ng),希望能够得到有能力的小伙伴的帮助,使MockApi越来越好。
其实`MockApi`之所以没有匹配这种直接将参数放到URL的情况是因为:在使用HttpClient发起带有参数的请求时,正确的打卡姿势是使用`HttpParams`,比如我们想在请求中加入`page`参数,则需要如下使用:
```typescript
+++ b/first-app/src/app/clazz/clazz.component.ts
@@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {Page} from '../entity/page';
import {Clazz} from '../entity/clazz';
import {Teacher} from '../entity/teacher';
-import {HttpClient} from '@angular/common/http';
+import {HttpClient, HttpParams} from '@angular/common/http';
@Component({
selector: 'app-clazz',
@@ -33,7 +33,8 @@ export class ClazzComponent implements OnInit {
}
onPage(page: number): void {
- this.httpClient.get<Page<Clazz>>('/clazz/page?page=' + page.toString())
+ const httpParams = new HttpParams().append('page', page.toString());
+ this.httpClient.get<Page<Clazz>>('/clazz/page', {params: httpParams})
```
此时当我们再次点击分页按扭时,错误消失并且可以在控制台中发现打印的数据返回情况:
![image-20210331083022413](https://img.kancloud.cn/ac/17/ac17f628d29b07afc3beff9c32b1d20d_1454x134.png)
但组件中显示的班级列表,却没有任何变化。这是由于我们在当前的单元测试中使用了`fixture.detectChanges();`来手动控制了组件的渲染。这导致了C层中的数据即使发生了变化,由于没有启用自动检测变更机制,所以组件的V层也不会自动渲染。
所以我们得出的结论是:如果想借助`ng t`来查看一些功能,则需要在测试用例的最后一行启用测试夹具的自动检测变更机制:
```typescript
+++ b/first-app/src/app/clazz/clazz.component.spec.ts
@@ -36,5 +36,6 @@ describe('ClazzComponent', () => {
expect(component).toBeTruthy();
getTestScheduler().flush();
fixture.detectChanges();
+ fixture.autoDetectChanges();
});
});
```
此时,当我们点击分页按钮时,组件的内容也会随着发生变化,这与应用真实后台的效果一模一样。
**小BUG**:我们刚刚不小心写了一个小BUG。上个小节中的接口规范中,每几页是`0基`而非`1基`的,也就是说如果我们获取第1页的信息,则应该将`page`设置为0。为此我们变更一下V层的代码:
```html
- <li class="page-item active"><span class="page-link" (click)="onPage(1)">1</span></li>
- <li class="page-item"><span class="page-link" (click)="onPage(2)">2</span></li>
- <li class="page-item"><span class="page-link" (click)="onPage(3)">3</span></li>
+ <li class="page-item active"><span class="page-link" (click)="onPage(0)">1</span></li>
+ <li class="page-item"><span class="page-link" (click)="onPage(1)">2</span></li>
+ <li class="page-item"><span class="page-link" (click)="onPage(2)">3</span></li>
```
此时当我们当击第1页时,向C层传递的是0;点击第2页时,向C层传递的是1。
## 加入每页大小
前面我们在初始化组件时,初始化了每页大小为3,而当前的模拟数据返回的每页大小为默认的20。参考在请求时加入`page`的代码,我们在请求中加入`size`:
```typescript
onPage(page: number): void {
- const httpParams = new HttpParams().append('page', page.toString());
+ const httpParams = new HttpParams().append('page', page.toString())
+ .append('size', this.size.toString());
this.httpClient.get<Page<Clazz>>('/clazz/page', {params: httpParams})
```
有了第几页、每页大小后,在当下进行测试,仍然发现返回了20条数据。这是由于我们在ClazzMockApi中的相关方法,并没有对`page`和`size`进行处理的原因。在ClazzMockApi中,我们是可以轻松的获取到请求参数的:
```typescript
+++ b/first-app/src/app/mock-api/clazz.mock.api.ts
@@ -2,6 +2,7 @@ import {ApiInjector, MockApiInterface, randomNumber, RequestOptions} from '@yunz
import {Clazz} from '../entity/clazz';
import {Teacher} from '../entity/teacher';
import {Page} from '../entity/page';
+import {HttpParams} from '@angular/common/http';
/**
* 班级模拟API
@@ -37,7 +38,9 @@ export class ClazzMockApi implements MockApiInterface {
{
method: 'GET',
url: '/clazz/page',
- result: () => {
+ result: (urlMatches: string[], options: RequestOptions) => { 👈
+ const httpParams = options.params as① HttpParams;
+ console.log(httpParams.get('page'), httpParams.get('size'));
const size = 20;
const clazzes = new Array<Clazz>();
for (let i = 0; i < size; i++) {
```
result属性设置为回调函数的方法我们在前面已然接触过,该方法支持0个,1个或2个参数。MockApi在模拟返回数据时,将使用相应的值做为参数来调用result属性对应的方法。其中第一个参数`urlMatches`为请求URL应用正则表式的匹配结果,类型为字符串数组;第二个参数`options`为请求的具体信息,包括请求主体,请求`header`,以及我们本次使用的请求参数`params`。
① `RequestOptions`中的`params`属性有多个类型,其实包括了我们使用的`HttpParams` ,在此使用`as`指定其具体类型。`as`的使用情景为:我们确认数据的确切类型时。由于该`params`实际上为C层中我们使用`get`方法传入的`HttpParams`类型,所以我们在此使用了`as HttpParams`来指定以规避`typescript`的一些语法提示。
此时再次点击相应的分页,则会在控制台中打印具体的分页信息:
![image-20210331091626114](https://img.kancloud.cn/e7/e0/e7e02a6de6c85358f7c39553853c2f1d_1446x128.png)
第一行打印了两个`null`,就是由于在组件的`ngOnInit()`方法中同样调用了后台的分页,在该方法中未指定`page`及`size`,所以在执行`httpParams.get('page')`方法时返回了`null`。
## 完善模拟数据
完善的模拟数据能够友好的支持组件开发,而且有些代码可以只造一个轮子,在开发的时候是非常具有性价比的。在模拟Api返回数据前,是完全可以根据传入的`page`、`size` 值来定制返回数据的:
```typescript
+++ b/first-app/src/app/mock-api/clazz.mock.api.ts
result: (urlMatches: string[], options: RequestOptions) => {
+ // 初始化两个默认值
+ let page = 0;
+ let size = 20;
+
const httpParams = options.params as HttpParams;
- console.log(httpParams.get('page'), httpParams.get('size'));
- const size = 20;
+ if (httpParams.has('page')) {
+ // 在这里我们使用了`has()`方法来判断是否存在该字段。
+ // 所以在此执行httpParams.get('page')必然返回一个非null的值
+ // 结合httpParams.get('page')返回值类型规定为null | string
+ // null | string去了一个null,则返回值类型必然为string,所以在使用as指定
+ // + 的目的是将string类型转换为number
+ page = +(httpParams.get('page') as string);
+ }
+
+ if (httpParams.get('size')) {
+ size = +(httpParams.get('size') as string);
+ }
+
@@ -55,9 +70,9 @@ export class ClazzMockApi implements MockApiInterface {
}
return new Page<Clazz>({
content: clazzes,
- number: 2,
+ number: page,
size,
- numberOfElements: 20
+ numberOfElements: size * 10
});
}
}
```
此时当未接收到`page`或`size`时将使用默认值设置`page`,`size`值,接收到`page`或`size`时将使用接收到的值。在返回值中,加入了当前页、每页大小信息,且当数据的总数量控制在10页。此时,在组件初始化时将返回第1页的数据,每页大小20条;点击分页时,分页大小为3条,对应返回当前页码的数据。比如点击第2页:
![image-20210331093356204](https://img.kancloud.cn/bc/17/bc172b1c418b3243a22789b55f42b80d_1868x572.png)
控制台打印的返回数据信息,显示当前页为第2页。
![image-20210331093346647](https://img.kancloud.cn/05/42/0542a1db37622f1869742a7b76677789_804x238.png)
最后为了保持组件风格统一,我们在组件初始化时当每页大小设置为3:
```typescript
+++ b/first-app/src/app/clazz/clazz.component.ts
@@ -28,7 +28,8 @@ export class ClazzComponent implements OnInit {
}
ngOnInit(): void {
- this.httpClient.get<Page<Clazz>>('/clazz/page')
+ this.httpClient.get<Page<Clazz>>('/clazz/page',
+ {params: new HttpParams().append('size', this.size.toString())})
.subscribe(pageData => this.pageData = pageData);
}
```
对应的后台请求完成后,接下来实现分页的点亮效果:比如当前为第1页,则第1页是选中状态:
![image-20210331093808677](https://img.kancloud.cn/f1/0b/f10b0ccfcd7b0a452848bbac1b6fba67_317x52.png)
如果是第2页,则2是选中状态,以此累推。
## 点亮效果
观察html我们得知点亮效果是由样式控制的:
```typescript
<li class="page-item active👈"><span class="page-link" (click)="onPage(0)">1</span></li>
```
则我们可以使用以下思路来完成该功能:
- 在C层记录当前是第几页
- 在V层的分页按钮上做判断,如果其分页值等于当前页则加入`active`样式
### 记录当前页
记录当前页比较简单,仅仅需要在`onPage()`方法中对`page`设置值即可:
```typescript
+++ b/first-app/src/app/clazz/clazz.component.ts
@@ -34,10 +34,14 @@ export class ClazzComponent implements OnInit {
}
onPage(page: number): void {
+ // 在请求数据之前设置当前页
+ this.page = page; 👈
const httpParams = new HttpParams().append('page', page.toString())
.append('size', this.size.toString());
this.httpClient.get<Page<Clazz>>('/clazz/page', {params: httpParams})
.subscribe(pageData => {
+ // 在请求数据之后设置当前页
+ this.page = page; 👈
this.pageData = pageData;
console.log(pageData);
});
```
在设置当前页时有两种选择:在获取到后台返回数据之前或之后。我们在此使用第二种方案:在获取到后台返回数据之后,为此删除在请求数据之前设置当前页的代码:
```typescript
onPage(page: number): void {
- // 在请求数据之前设置当前页
- this.page = page;
const httpParams = new HttpParams().append('page', page.toString())
.append('size', this.size.toString());
```
### ngClass
使用我们前面学习过的`ngIf`指令,可以非常轻松的完成:根据当前页情况选择是否添加`active`样式功能:
```html
<ul class="pagination col-md-auto">
<li class="page-item disabled"><span class="page-link">上一页</span></li>
<li *ngIf="page !== 0" class="page-item"><span class="page-link" (click)="onPage(0)">1</span></li>
<li *ngIf="page === 0" class="page-item active"><span class="page-link" (click)="onPage(0)">1</span></li>
<li *ngIf="page !== 1" class="page-item"><span class="page-link" (click)="onPage(1)">2</span></li>
<li *ngIf="page === 1" class="page-item active"><span class="page-link" (click)="onPage(1)">2</span></li>
<li *ngIf="page !== 2" class="page-item"><span class="page-link" (click)="onPage(2)">3</span></li>
<li *ngIf="page === 2" class="page-item active"><span class="page-link" (click)="onPage(2)">3</span></li>
<li class="page-item"><span class="page-link">下一页</span></li>
</ul>
```
![image-20210331095454709](https://img.kancloud.cn/f4/ce/f4ce05432ce40144776a8f464c60d6fe_832x305.png)
上述方法虽然可行,但冗余的代码有些过多。Angular当然可以更优雅的处理此种情况---- `[ngClass]`。`[ngClazz]`能够实现根据某种条件来选择是否添加某样式的功能,比如当前情况可以使用`[ngClass]`改写如下:
```html
<ul class="pagination col-md-auto">
<li class="page-item disabled"><span class="page-link">上一页</span></li>
<li [ngClass]="{active: page === 0}"👈 class="page-item"><span class="page-link" (click)="onPage(0)">1</span></li>
<li [ngClass]="{active: page === 1}"👈 class="page-item"><span class="page-link" (click)="onPage(1)">2</span></li>
<li [ngClass]="{active: page === 2}"👈 class="page-item"><span class="page-link" (click)="onPage(2)">3</span></li>
<li class="page-item"><span class="page-link">下一页</span></li>
</ul>
```
`[ngClass]`接收一个对象,将对象上的某个属性值为`true`时,将使用该属性名做为class值,添加其所在的元素(宿主元素)的`class`属性上。所以查看元素观察生成的`html`代码时,会看到如下代码:
![image-20210331100102895](https://img.kancloud.cn/1c/3d/1c3dcf39bf8299dddbf87f74c96adc65_1466x258.png)
同样,我们还可以使用`[ngClass]`来动态的为**上一页**、**下一页**添加相应的`disabled` 样式。
在下个小节中,我们将共同学习如何生成一个动态的分页,从而替换当前手写的1,2,3页。
## 本节作业
1. `onPage()`方法中使用`this.page = page`更新了当前页,请尝试将代码移至请求数据之前,然后再点击分页,观察两种设置方式的不同。
2. 使用`[ngClass]`为**上一页**、**下一页**添加相应的`disabled` 样式。
| 名称 | 链接 |
| ------------------ | ------------------------------------------------------------ |
| 配置 HTTP URL 参数 | [https://angular.cn/guide/http#configuring-http-url-parameters](https://angular.cn/guide/http#configuring-http-url-parameters) |
| NgClass | [https://angular.cn/guide/built-in-directives#ngclass](https://angular.cn/guide/built-in-directives#ngclass) |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.3.4.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.3.4.zip) |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结