Angular Material 分页数据源
加入我的邮件列表,即可获取有关 Angular 和 JavaScript 等 Web 技术的深度文章和独家内容。
本文将介绍如何为Angular Material库开发一个响应式数据源,该数据源可复用于多个不同的分页端点,并允许您针对每个实例配置搜索和排序输入。最终成果已发布在StackBlitz上。
虽然JavaScript 的功能非常丰富,但很多时候我们用它来获取和显示数据。在 Angular 中,数据获取主要通过 HTTP 协议完成,而数据显示则可以通过各种不同的用户界面组件来实现。这些组件可以是表格、列表、树状结构,或者任何你需要的界面形式。
Angular Material 提供了一些可以用于此的组件,例如表格组件。开发者甚至预料到了需要将数据检索与数据显示分离,因此提供了数据源 (DataSource)的概念。
对于大多数实际应用而言,为表格提供一个 DataSource 实例是管理数据的最佳方式。DataSource 旨在封装应用程序特有的排序、筛选、分页和数据检索逻辑。—— Angular Material 文档
很多时候,我们需要展示的数据量太大,无法一次性全部获取。可以通过对数据进行切片,并通过分页来解决这个问题。这样,用户就可以流畅地在页面之间切换。对于许多不同的数据展示视图,我们可能都需要用到这种方法——因此,将这种行为封装起来,避免重复编写,是非常明智的做法。
分页和排序数据源
我们来看一个数据源实现,它可以让你对数据进行排序并获取连续的页面。首先,我们稍微简化一下 Material 数据源:
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs';
export interface SimpleDataSource<T> extends DataSource<T> {
connect(): Observable<T[]>;
disconnect(): void;
}
通常情况下,这些方法connect()会disconnect()接受一个CollectionViewer 对象,但是,让显示数据的组件来决定显示哪一部分数据似乎并不明智。Material表的官方数据源也忽略了这个参数。
接下来,我们将在一个名为 . 的单独文件中定义一些用于分页数据的可重用类型page.ts。
import { Observable } from 'rxjs';
export interface Sort<T> {
property: keyof T;
order: 'asc' | 'desc';
}
export interface PageRequest<T> {
page: number;
size: number;
sort?: Sort<T>;
}
export interface Page<T> {
content: T[];
totalElements: number;
size: number;
number: number;
}
export type PaginatedEndpoint<T> = (req: PageRequest<T>) => Observable<Page<T>>
通用参数T始终指的是我们正在处理的数据类型——稍后在我们的示例中就是User。
该Sort<T>类型定义了要应用于数据(即发送到服务器)的排序方式。此排序可以通过物料表的表头或通过选择来创建。
APageRequest<T>是我们最终要传递给某个服务的数据,该服务会发起相应的 HTTP 请求。然后,该服务会返回一个Page<T>包含所请求数据的响应。
A是一个接受 a并返回包含相应 a 的 RxJS 流(又称 observable)的PaginatedEndpoint<T>函数。PageRequest<T>Page<T>
现在我们可以通过实现分页数据源来使用这些类型,如下所示:
import { Observable, Subject } from 'rxjs';
import { switchMap, startWith, pluck, share } from 'rxjs/operators';
import { Page, Sort, PaginatedEndpoint } from './page';
export class PaginatedDataSource<T> implements SimpleDataSource<T> {
private pageNumber = new Subject<number>();
private sort = new Subject<Sort<T>>();
public page$: Observable<Page<T>>;
constructor(
endpoint: PaginatedEndpoint<T>,
initialSort: Sort<T>,
size = 20) {
this.page$ = this.sort.pipe(
startWith(initialSort),
switchMap(sort => this.pageNumber.pipe(
startWith(0),
switchMap(page => endpoint({page, sort, size}))
)),
share()
)
}
sortBy(sort: Sort<T>): void {
this.sort.next(sort);
}
fetch(page: number): void {
this.pageNumber.next(page);
}
connect(): Observable<T[]> {
return this.page$.pipe(pluck('content'));
}
disconnect(): void {}
}
让我们从构造函数开始,一步一步地来。它接受三个参数:
- 我们将使用分页端点来获取页面。
- 首先进行初步排序
- 页面加载内容数量的可选参数,默认值为每页 20 项。
我们使用 RxJS 主题初始化实例属性sort。通过使用主题,我们可以根据类方法的调用来改变排序方式,该方法sortBy(sort: Sort<T>)会将下一个值传递给主题。pageNumber此外,在构造函数中还会初始化另一个主题,这使我们能够通过该方法指示数据源获取不同的页面fetch(page: number)。
我们的数据源将通过该属性公开页面流page$。我们根据排序的变化构建此可观察流。RxJS 操作符startWith()允许我们轻松地为排序提供初始值。
然后,每当排序发生变化时,我们都会利用运算符切换到页码流switchMap()。现在,只要排序不变,我们就会查看从任何排序的第一页开始的页码——同样使用startWith()。
当数据源需要获取不同的页面时(由调用触发fetch(page: number)),我们将使用所需的参数查询分页端点。最终,此可观察对象会向多个可能使用该数据的组件提供数据页面。因此,您可以使用share()同步这些订阅。
最后,connect()我们在内部通过操作符将任何页面映射到其内容,从而提供一个项目列表流pluck()。此方法最终将由 Material 表或任何其他与 DataSource 接口兼容的组件调用。您可能想知道为什么我们不直接将页面映射到其内容——那是因为我们需要其他页面属性,例如大小或数量,这些属性随后可供MatPaginator使用。
disconnect()这里不需要执行任何操作——当所有使用该数据源的组件取消订阅时,我们的数据源将自动关闭。
在组件中使用数据源
在处理特定数据的组件内部,我们现在可以使用 Material 表格作为数据源。具体做法是创建一个新实例,并传递一个函数,该函数会将页面请求转发到相应的服务。我们还会传递一个默认排序规则。
它将UserService负责在方法PageRequest<User>内部将请求转换为符合服务器 API 的正确 HTTP 请求page()。
@Component(...)
export class UsersComponent {
displayedColumns = ['id', 'name', 'email', 'registration']
data = new PaginatedDataSource<User>(
request => this.users.page(request),
{property: 'username', order: 'desc'}
)
constructor(private users: UserService) {}
}
同样,现在要更改排序方式,可以data.sortBy(sort)在用户选择新的排序方式后调用该函数。
在模板中,您需要将数据源传递给 Material 表或任何其他支持此概念的组件。您还需要定义一个MatPaginator ,允许用户切换页面。该分页器还可以通过AsyncPipe轻松地从数据源获取页面流,并调用相应的方法data.fetch(page: number)来获取不同的页面。
<table mat-table [dataSource]="data">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Username</th>
<td mat-cell *matCellDef="let user">{{user.username}}</td>
</ng-container>
...
</table>
<mat-paginator *ngIf="data.page$ | async as page"
[length]="page.totalElements" [pageSize]="page.size"
[pageIndex]="page.number" [hidePageSize]="true"
(page)="data.fetch($event.pageIndex)">
</mat-paginator>
添加查询参数
当数据量庞大时,您可能需要帮助用户快速找到所需内容。您可以提供基于文本的搜索功能,或者提供结构化输入框,以便用户按特定属性筛选数据。这些查询参数会根据您查询的数据而有所不同。为了解决这个问题,我们将调整数据源,使其能够使用一组通用的查询参数。
首先,我们将向Q数据源的类型添加一个通用参数,表示某些数据的查询模型,最终得到类型PaginatedDataSource<T, Q>。
接下来,我们将添加一个构造函数参数用于初始查询,并创建一个 subject 属性this.query = new BehaviourSubject<Q>(initalQuery)。这种类型的 subject 允许我们访问其最后一个值。我们利用这一特性,通过实例方法实现对查询的部分更新:
queryBy(query: Partial<Q>): void {
const lastQuery = this.query.getValue();
const nextQuery = {...lastQuery, ...query};
this.query.next(nextQuery);
}
此方法接受查询模型的部分表示BehaviorSubject<Q>。我们通过访问并合并两个查询,使用扩展运算符将新查询与上一个查询结合起来。这样,当仅更新一个参数时,旧的查询属性就不会被覆盖。
然后,我们不再仅仅基于排序主题来构建可观察的页面流,而是使用 RxJS 操作符将排序和查询的更改结合起来combineLatest()。两个参数流都从它们的初始值开始——sort通过`<sort> startWith()`,query通过 `<query>` 的构造函数参数BehaviorSubject。
const param$ = combineLatest([
this.query,
this.sort.pipe(startWith(initialSort))
]);
this.page$ = param$.pipe(
switchMap(([query, sort]) => this.pageNumber.pipe(
startWith(0),
switchMap(page => endpoint({page, sort, size}, query))
)),
share()
)
随后,我们还会将查询传递给分页端点。为此,我们需要按如下方式调整其类型:
export type PaginatedEndpoint<T, Q> = (req: PageRequest<T>, query: Q) => Observable<Page<T>>
PaginatedDataSource<T, Q>现在我们可以更新组件,使其提供一些查询输入。首先,根据特定查询类型调整初始化方式,例如UserQuery`<query>`。然后,提供一个分页端点,将页面请求和查询转发到 `<query> UserService`。最后,传递一个初始查询。
在我们的示例中,我们将允许用户通过文本输入进行搜索,并允许用户选择注册日期:
interface UserQuery {
search: string
registration: Date
}
data = new PaginatedDataSource<User, UserQuery>(
(request, query) => this.users.page(request, query),
{property: 'username', order: 'desc'},
{search: '', registration: undefined}
)
在模板内部,我们可以通过调用data.queryBy()包含查询参数的部分查询模型,简单地将输入值转发到数据源:
<mat-form-field>
<mat-icon matPrefix>search</mat-icon>
<input #in (input)="data.queryBy({search: in.value})" type="text" matInput placeholder="Search">
</mat-form-field>
<mat-form-field>
<input (dateChange)="data.queryBy({registration: $event.value})" matInput [matDatepicker]="picker" placeholder="Registration"/>
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<table mat-table [dataSource]="data">
...
</table>
...
现在,无论何时更改输入内容,显示的页面都会相应更新——前提是您已将查询参数正确转发到服务器并在服务器上正确处理它们。
装载指示
如果你想向用户表明你正在获取页面,你可以PaginatedDataSource<T, Q>基于私有主题扩展相应的可观察属性:
private loading = new Subject<boolean>();
public loading$ = this.loading.asObservable();
然后,你可以在调用前后手动更新主题的值,或者使用我在关于Angular 加载指示的文章中介绍的PaginatedEndpoint<T, Q>操作符。只需将其附加到分页端点返回的 observable 对象即可:indicate(indicator: Subject<boolean>)
this.page$ = param$.pipe(
switchMap(([query, sort]) => this.pageNumber.pipe(
startWith(0),
switchMap(page => this.endpoint({page, sort, size}, query)
.pipe(indicate(this.loading))
)
)),
share()
)
然后您可以像这样显示加载指示器:
<my-loading-indicator *ngIf="data.loading$ | async"></my-loading-indicator>
总结
通过巧妙的行为参数化,我们可以重用大量逻辑,从而编写功能强大且可配置的组件来显示任何类型的数据。我们对 Material 数据源的扩展,只需几行代码即可实现远程数据的分页、排序和筛选。
这里是 StackBlitz 上的完整示例。我还提供了一个函数式数据源版本,其中省略了对类的使用。
文章来源:https://dev.to/angular/angular-material-pagination-datasource-19cg