Angular Material 18+ 高级教程 – CDK Scrolling
Angular CDK 的意义
经过之前两篇文章 CDK Portal 和 CDK Layout の Breakpoints,我相信大家已经悟到了 CDK 的意义。
CDK 有 3 个方向:
- 包装 BOM / DOM 上层接口 (e.g. CDK Layout)
这个方向主要是让我们不直接操作/依赖 BOM 和 DOM。
还有把接口包装成 RxJS / Promise 之类的,方便我们使用。
- 包装 Angular 上层接口 (e.g. CDK Portal)
这个方向主要是优化 Angular 上层接口调用,或者是扩展弥补一些 Angular 缺失的功能。
- 辅助制作 UI Component
Angular Material Team 是在用 Angular Way 开发 Material Design UI Component 过程中提炼出 CDK 的。
UI Component 通常需要 HTML,CSS,JS 三个地方实现,而 CDK 主要是抽象封装了 JS 的部分,CSS 几乎没有,HTML 有一点点 (大概是这个比例)。
这个方向主要是让我们可以做以下几件事:
a. 制作 Angular Material Team 多年都没有完成的 Material Design UI Component (e.g. Time pickers)
b. 制作变种的 Material Design UI Component,有些 UI Component 没有在 Material Design 规范中,这些 UI Component Angular Material Team 是坚决不肯做的,
但其实 Google Products (Gmail, Google Ads 等) 都有这些 UI Component 的具体实现,这时我们就可以依照它们自己制作出来。
c. 制作不同风格的 UI Component。如果 UI Component 只是视觉上不同,交互逻辑差不多,那 CDK 确实非常合适。
但要记得,CDK 服务的对象始终只有 Angular Material,就好比 Angular 服务的对象始终只有 Google Products 一样,它们不会为了你或者社区去做任何对 Google 没有利益的事。
好,理解了 CDK 的方向,我们在学习和使用它时就能得心应手了。
CDK Scrolling 简单介绍
在做 UI Component 时,我们经常需要监听 scroll event,获取 element scrollTop,操作 element scrollTo 等等。
CDK Scrolling 主要就是对这些 DOM Manipulation 做了封装。
另外,CDK Scrolling 还有一个强大的功能 -- Virtual Scrolling,不熟悉 Virtual Scrolling 的可以看这篇:CSS & JS Effect – Virtual Scrolling
虽然目前 Virtual Scrolling 还不完善

但也勉强可以用于一些小场景。
CdkScrollable 指令
需求
App Template
<div class="scrollable">
<div class="spacer"></div>
</div>
App Styles
.scrollable {
margin-inline: auto;
margin-top: 128px;
overflow-y: auto;
height: 200px;
width: 100px;
border: 1px solid red;
.spacer {
width: 100%;
height: 2000px;
}
}
效果

一个 div,里面有另一个 div 把它撑大,于是它可以 scroll。
需求是监听它的 scroll event。
Without CDK
不使用 CDK 的情况下,我们会这样实现
在 scrollable element 上添加 Template Binding Syntax
<div class="scrollable" (scroll)="handleScroll($event)">
<div class="spacer"></div>
</div>
接着在 App 组件写上 handleScroll 方法
export class AppComponent {
handleScroll(event: Event) {
console.log('scrolled');
}
}
效果

代码虽然简单,但这个方式往往不足够应付 UI Component。
UI Component 一般上交互会比较复杂,一个事件往往会牵连到很多事情,所以更好的方式是以 RxJS 的形式去监听 (RxJS Stream 比较容易拿来组合,或者变化等等,简单说就是比较灵活使用)。
with CDK
在 App 组件 import ScrollingModule,因为我们需要用到相关的指令。

把 CdkScrollable 指令 apply 到 scrollable element
<div class="scrollable" cdkScrollable>
<div class="spacer"></div>
</div>
通过 viewChild 获取 CdkScrollable 指令,然后使用 elementScrolled 方法监听 scroll event。
export class AppComponent implements OnInit {
// 1. 获取 CdkScrollable 指令
scrollable = viewChild.required(CdkScrollable);
ngOnInit() {
// 2. 使用 elementScrolled 监听 scroll event
// 它会返回 RxJS Observable
this.scrollable()
.elementScrolled()
.subscribe(event => {
console.log('scrolled', event);
});
}
}
它返回的是 RxJS Stream。
源码在 scrollable.ts

没什么特别的,只是用 RxJS 监听了 element scroll event 而已。
注:ngZone 是 Change Detection 的知识,我们可以忽视它,因为 Angular v17.1.0 后 ngZone 基本已经废弃了。
对比使用 CDK 和不使用 CDK 两个版本,我们可以悟出一个道理 -- 开发 UI Component 和开发业务项目是不同的。
开发业务我们会远离 DOM,开发 UI Component 我们会贴近 DOM。
get scrollTop
监听到了 scroll event 下一步通常是获取 scrollTop 值。
CdkScrollable 指令也有针对这个的接口。
const scrollTop = this.scrollable().measureScrollOffset('top');
没什么特别的,底层就是拿 element.scrollTop 属性而已

scrollTo
scrollTo 就是对 DOM element.scrollTo 的封装而已。
this.scrollable().scrollTo({ top: 100, behavior: 'smooth' });
this.scrollable().scrollTo({ bottom: 100, behavior: 'smooth' });
接口比原生 DOM 好多了,甚至支持 scroll to bottom
相关源码


ScrollDispatcher
CdkScrollable 指令可以用来监听 element scroll event,但它监听不到 document scroll event,因为指令无法 apply 超出 App 组件范围。
这时,我们需要使用 ScrollDispatcher。
监听 document scroll event
ScrollDispatcher 是一个 Root Level Provider。
export class AppComponent {
constructor() {
// 1. inject ScrollDispatcher
const scrollDispatcher = inject(ScrollDispatcher);
// 2. 监听 scroll event
scrollDispatcher.scrolled().subscribe(() => {
console.log('document scrolled');
});
}
}
效果


not only document scroll event but all CdkScrollable
ScrollDispatcher 不仅仅监听 document scroll event,它还监听了所有 CdkScrollable 指令的 scroll event。
App Template 有一个 CdkScrollable 指令
<div class="scrollable" cdkScrollable>
<div class="spacer"></div>
</div>
然后 App 组件监听 ScrollDispatcher
export class AppComponent {
constructor() {
const scrollDispatcher = inject(ScrollDispatcher);
scrollDispatcher.scrolled().subscribe(scrollable => {
console.log('scroll dispatcher', scrollable);
});
}
}
如果是 document scrolled 那 callback 函数的参数 scrollable 会是 undefined。
如果是 CdkScrollable 指令 scrolled 那 callback 函数的参数 scrollable 会是 CdkScrollable 指令实例。
效果

提醒:ScrollDispatcher 会监听全世界所有的 CdkScrollable 指令哦
每一个 CdkScrollable 在 OnInit 时都会把自己注册到 ScrollDispatcher

所谓的注册就是监听 CdkScrollable 的 scroll event 然后 ScrollDispatcher 再转发。

only listen ancestor CdkScrollable
通过 ScrollDispatcher 监听 CdkScrollable scroll event 有时候是会比较混乱的,毕竟它是 Root Level Provider,意味着着全世界的 CdkScrollable 指令都会往它身上注册。
也因为这样,ScrollDispatcher 提供了一些过滤监听的方式,比如说 ancestorScrolled 方法。

getAncestorScrollContainers 方法

_scrollableContainsElement 方法

ancestorScrolled 的监听时机
ancestorScrolled 的监听时机是很讲究的,当我们调用 ancestorScrolled 时,请一定要确保 CdkScrollable 已经被注册到 ScrollDispatcher 里。
上面我们有提到,CdkScrollable 指令是在 OnInit 阶段被注册到 ScrollDispatcher 里的,不是 constructor 阶段哦。
总结
想监听指定某个 element 的 scroll event
用 CdkScrollable 指令。
想监听 document scroll event
用 ScrollDispatcher 监听,callback 函数的 scrollable 参数是 undefined 就表示是 document scroll。
想监听 parent / ancestor scroll event
用 ScrollDispatcher.ancestorScrolled 方法。
题外话:Dependency injection based on ancestor element
我们知道 Angular 的 DI 查找的是 NodeInjector Tree 而不是 DOM Tree,不熟悉的朋友可以看这篇 Dependency Injection & NodeInjector。
这是一个局限,有时候使用起来很不方便。
上面这个 ancestorScrolled 方法是一个很好的案例。
1 个 Root Service
1 个指令
1 个 element
指令可以 inject 其它 Service,然后把 element 和相关的 Service 一起记入到 Root Service。
要使用的人就可以通过 Root Service 依据 element 做查找,最后获得相关 Service。
ViewportRuler
ViewportRuler 其实和 Scrolling 没有什么关系,不知为什么 Angular Material 会把它纳入到 Scrolling Module。
ViewportRuler 也是一个 Root Level Provider。它可以监听 viewport resize event 和获取 viewport dimension。
export class AppComponent {
constructor() {
// 1. inject ViewportRuler
const viewportRuler = inject(ViewportRuler);
// 2. 监听 resize
viewportRuler.change().subscribe(event => {
// 3. 获取 viewport dimension
const { width, height } = viewportRuler.getViewportSize();
console.log('dimension', [width, height]);
});
}
}
viewport 指的是 window。
resize 指的是 window resize event 和 orientationchange event

dimension 指的是 window.innerWidth 和 innerHeight

另外,change 方法返回的 Observable 默认会有一个 auditTime 20ms


Virtual Scrolling
不熟悉 Virtual Scrolling 的可以先看这篇:CSS & JS Effect – Virtual Scrolling
Example
我用回文章里最后一个完整的例子
App 组件
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ScrollingModule],
})
export class AppComponent {
itemHeight = signal(40);
itemTexts = signal(new Array(500).fill(null).map((_, index) => `item${index + 1}`));
}
App Template
<div class="viewport" cdkVirtualScrollingElement>
<div class="header">Header</div> <cdk-virtual-scroll-viewport [itemSize]="itemHeight()" class="item-list">
<ng-template let-item cdkVirtualFor [cdkVirtualForOf]="itemTexts()">
<div class="item">{{ item }}</div>
</ng-template>
</cdk-virtual-scroll-viewport> <div class="footer">Footer</div>
</div>
几个知识点:
cdkVirtualScrollingElement 指令
如果 scrollable 和 cdk-virtual-scroll-viewport 不是同一个 element,那就需要用到 CdkVirtualScrollingElement 指令。
cdkVirtualForOf 指令
它和 ngForOf 指令的接口完全一模一样,把它当成 ngForOf 指令使用就可以了。
- [itemSize] 就是 itemHeight,一定要提供。
App Styles
.viewport {
margin-top: 128px;
margin-inline: auto;
width: 256px;
height: 256px;
border: 2px solid red;
.header,
.footer {
height: 80px;
background-color: #988beb;
display: flex;
justify-content: center;
align-items: center;
}
.item-list .item {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
&:nth-child(odd) {
background-color: pink;
}
&:nth-child(even) {
background-color: lightblue;
}
}
}
效果


Cache Concept
默认情况下 CDK Virtual Scrolling 会缓存 20 个创建过的 Item ViewRef。
相关源码在 recycle-view-repeater-strategy.ts

要缓存多少,我们可以自己配置

Elements with parent tag requirements
<cdk-virtual-scroll-viewport class="viewport" [itemSize]="itemHeight()">
<dl> <ng-template
let-keyValue
cdkVirtualFor
[cdkVirtualForOf]="keyValues()"
>
<dt>{{ keyValue.key }}</dt>
<dd>{{ keyValue.value }}</dd>
</ng-template> </dl>
</cdk-virtual-scroll-viewport>
注意看它的结构,cdk-virtual-scroll-viewport 里面先是一个 dl,dl 里面才是 <ng-template>。
其实 cdk-virtual-scroll-viewport 并不负责创建 item

它只负责 item-wrapper 和 spacer 而已,item 是由 cdkVirtualForOf 指令创建好后 transclude 进来的。
DataSource and lazy loading
虽然 Virtual Scrolling 解决了渲染时的性能问题,但是 cdkVirtualForOf 要求我们预先传入一个完整的 item list,
假如我有 10 万条 items 而且资料是从后端取来的,那我就需要一次性下载 10 万条资料,这样也会导致 http 请求性能问题。
为此,Angular Material 设计了一个叫 DataSource 的概念,它允许我们分量提供 items。
注:DataSoruce 概念不仅仅用于 Virtual Scrolling,其它地方比如 Table 和 Tree 也可以使用 DataSoruce (以后会教)
DataSource 是一个抽象 class,它长这样

我们需要 extends 它,并且 override connect 和 disconnect 方法。
顾名思义,connect 会在 CdkVirtualForOf 指令 OnInit 时被调用,disconnect 则是在 OnDestroy 时被调用。
class ItemTextsDataSource
class ItemTextsDataSource extends DataSource<string | null> {
private subscription = new Subscription();
private itemsBS: BehaviorSubject<readonly (string | null)[]>;
constructor(itemCount: number) {
super();
// 1. 初始化一个空 item list
// 虽然我们不需要立刻 http request 10 万条 item 的具体信息,但我们依然需要知道 item 总数是多少。
this.itemsBS = new BehaviorSubject<readonly (string | null)[]>(new Array(itemCount).fill(null));
}
override connect(collectionViewer: CollectionViewer): Observable<readonly (string | null)[]> {
this.subscription.add(
// 3. connect 时,我们会等到参数 collectionViewer
// 我们需要监听它,每当 user scroll 它会触发,并且告诉我们当前 user 需要看到哪些 item
collectionViewer.viewChange.subscribe(range => {
// 4. 比如 user scroll 到中间,
// 需要显示第 50001 到 50021 的 items
// start 和 end 指的是 index
const { start, end } = range;
// 5. 这里可以发 http request 去拿 item 的具体信息
// 然后添加进 itemsBS
window.setTimeout(() => {
const newItems = new Array(end - start).fill(null).map((_, index) => `Item ${start + index + 1}`);
const items = this.itemsBS.value;
this.itemsBS.next([...items.slice(0, start), ...newItems, ...items.slice(end, items.length)]);
}, 1000);
}),
);
// 2. 返回 items$ Observable
return this.itemsBS.asObservable();
}
override disconnect() {
// 7. connect 的时候,我们监听了 CollectionViewer.viewChange,这里需要退订
// 题外话,即使不退订也不会造成内存泄漏的,因为 CdkVirtualForOf 指令 OnDestroy 的时候会 complete 掉 CollectionViewer.viewChange。
// 但是呢,我们最好还是养成退订的好习惯,毕竟 complete 和 unsubscribe 在一些特定情况下效果是不一样。
this.subscription.unsubscribe();
}
}
接着换上 ItemTextsDataSource
export class AppComponent {
readonly itemHeight = signal(40);
// 1. 把 itemTexts 换成 itemDataSource
// readonly itemTexts = signal(new Array(100000).fill(null).map((_, index) => `item${index + 1}`));
readonly itemTextsDataSource = new ItemTextsDataSource(100000);
}
App Template

item 是 null 就显示 loading...
效果

官网给了一个很不错的 Demo,大家也可以看一看。
Connect DataSource multiple times
DataSoruce 的接口设计是挺奇葩的

它让我们产生几个疑问:
每一次调用 connect 返回的 Observable 必须是同一个吗?
unsubscribe connect 返回的 Observable 和 disconnect 是一样的效果吗?
我们看看 Material Table 如何实现 DataSource

假如有 multiple connect,它始终返回同一个 Observable (_renderData 是 BehaviorSubject),内部 subscription 也只维持一个。
disconnect 会 unsubscribe 内部的 subscription,与此同时 _renderData 不会在 next 了,但它没用 complete,所以它是可以 re-connect 的。
- unsubscribe connect 返回的 Observable 和 disconnect 效果是不一样的。
结论:MatTableDataSource 大家可以 share 着用,只有其中一个人需要负责 disconnect 就可以了。
我没提到的功能:
有些功能很冷门,而且很简单,我就不一一讲解了:
-
items 只需要显示足够填满 viewport 就可以了,但是如果我们希望它提早出现则可以 set buffer。
这类似 IntersectionObserver 的 rootMargin 设置。
-
它还支持 horizontal
-
创建 append 后的 item 不会再被清除。
这意味着 Node 会越来越多,可能会引起性能问题,要留意哦。
-
通常 scrollable 是 cdk-virtual-scroll-viewport,
如果遇到有 header 的场景,那 scrollable 就不是 cdk-virtual-scroll-viewport 了
<div class="viewport" cdkVirtualScrollingElement>
这时需要需要在 scrollable element 加上 CdkVirtualScrollingElement 指令。
如果 scrollable 是 window / document,那就需要用 CdkVirtualScrollableWindow 指令
<cdk-virtual-scroll-viewport scrollWindow itemSize="50">
目录
上一篇 Angular Material 18+ 高级教程 – CDK Layout の Breakpoints
下一篇 Angular Material 18+ 高级教程 – Material Icon
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding
Angular Material 18+ 高级教程 – CDK Scrolling的更多相关文章
- Angular Material 教程之布局篇
Angular Material 教程之布局篇 (一) : 布局简介https://segmentfault.com/a/1190000007215707 Angular Material 教程之布局 ...
- Angular Material TreeTable Component 使用教程
一. 安装 npm i ng-material-treetable --save npm i @angular/material @angular/cdk @angular/animations -- ...
- Angular Material design设计
官网: https://material.io/design/ https://meterial.io/components 优秀的Meterial design站点: http://material ...
- angular使用@angular/material 出现"export 'ɵɵinject' was not found in '@angular/core'
WARNING in ./node_modules/@angular/cdk/esm5/a11y.es5.js 2324:206-214 "export 'ɵɵinject' was not ...
- Material使用11 核心模块和共享模块、 如何使用@angular/material
1 创建项目 1.1 版本说明 1.2 创建模块 1.2.1 核心模块 该模块只加载一次,主要存放一些核心的组件及服务 ng g m core 1.2.1.1 创建一些核心组件 页眉组件:header ...
- Siki_Unity_2-9_C#高级教程(未完)
Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...
- Angular Material Starter App
介绍 Material Design反映了Google基于Android 5.0 Lollipop操作系统的原生应用UI开发理念,而AngularJS还发起了一个Angular Material ...
- 关于 Angular引用Material出现node_modules/@angular/material/button-toggle/typings/button-toggle.d.ts(154,104): error TS2315: Type 'ElementRef' is not generic.问题
百度了好久 ,,,最后谷歌出来了.. 该错误可能来自于您将@ angular / material设置为6.0.0, 但所有其他Angular包都是5.x.您应该始终确保Material主要版本与An ...
- Angular Material & Hello World
前言 Angular Material(下称Material)的组件样式至少是可以满足一般的个人开发需求(我真是毫无设计天赋),也是Angular官方推荐的组件.我们通过用这个UI库来快速实现自己的i ...
- 基于 Angular Material 的 Data Grid 设计实现
自 Extensions 组件库发布以来,Data Grid 成为了使用及咨询最多的组件.最开始 Data Grid 的设计非常简陋,经过一番重构,组件质量有了质的提升. Extensions 组件库 ...
随机推荐
- SMOTE与SMOGN算法R语言代码
本文介绍基于R语言中的UBL包,读取.csv格式的Excel表格文件,实现SMOTE算法与SMOGN算法,对机器学习.深度学习回归中,训练数据集不平衡的情况加以解决的具体方法. 在之前的文章S ...
- oeasy教您玩转linux 010211 牛说 cowsay
我们来回顾一下 上一部分我们都讲了什么? 软件包工具是 apt 软件包不但能下载,也能升级,还能删除 专门管理软件包的 aptitude 这次我们下载个牛说 cowsay: sudo apt inst ...
- oeasy教您玩转python - 012 - # 刷新时间
刷新时间 回忆上次内容 通过搜索 我们学会 import 导入 time 了 time 是一个 module import 他可以做和时间相关的事情 time.time() 得到当前时间戳 tim ...
- 使用 useLazyFetch 进行异步数据获取
title: 使用 useLazyFetch 进行异步数据获取 date: 2024/7/20 updated: 2024/7/20 author: cmdragon excerpt: 摘要:&quo ...
- 简单了解java中的io流中的字节流
了解一下前置知识字符集,我们常见的字符集有ASCII,GBK,UTF-8 GBK中一个字需要两个字节存储 UTF-8中一个字母需要一个字节,并以0开头,一个汉字需要三个字节,与GBK不同的是,他支持的 ...
- NameCheap域名怎么样,如何注册购买域名?如何解析域名?
Namecheap介绍 Namecheap是一家国外域名注册商和网站托管公司,成立于2000年,提供域名注册.虚拟主机.电子邮件托管.SSL证书.免费的WHOIS保护.CDN.VPS主机和独立服务器. ...
- 自写Json转换工具
前面写了简单的API测试工具ApiTools,返回的json有时需要做很多转换,于是开发了这个工具. 功能包括 1.json字符串转为表格,可以直观的展示,也可以复制,并支持转换后的表格点击列头进行排 ...
- 【Centos6】手动配置网卡
在安装时忘记手动勾选链接网络 导致初始状态没有网卡的IP地址 这里参考这篇文章的解决办法: https://blog.51cto.com/u_13570193/2091655 首先检查是否有E1000 ...
- 【Java】【常用类】SimpleDateFormat 简单日期格式化类
Date类的API不易于国际化,大部分基本摈弃了 java.text.SimpleDateFormate 不和语言环境有关的方式来格式化和解析日期的具体类 支持 文本转格式,格式转文本 public ...
- 【Zookeeper】Re02 CuratorAPI
Curator,提供给Java操作ZK的API组件: 需要的组件依赖: <!-- https://mvnrepository.com/artifact/org.apache.curator/cu ...