前言

今天来揭秘一下 Angular 的 Event Listening,看看它底层有什么好玩的地方。

(keydown.enter) 语法

在 Component 组件 の Template Binding Syntax 文章中我们就学过了最基本的 Event Listening。

<button
(click)="window.alert('click')"
(mouseenter)="window.alert('mouseenter')"
(mouseleave)="window.alert('mouseleave')"
>click me</button>

上面这些都是常见的 DOM 事件。

如果 Angular 就只有这点能耐就弱爆了,我们来看看 Angular 的扩展功能。

<input
(keydown.enter)="window.alert('enter')"
(keydown.a)="window.alert('a')"
(keydown.arrowDown)="window.alert('arrowDown')"
>

监听 keydown 事件的同时,还可以指定监听某一个 Key,比如 Enter,Escape,ArrayDown 等等等。

它甚至还支持 modifier keys 哦。

<input
(keydown.control.enter)="window.alert('enter')"
(keydown.alt.escape)="window.alert('escape')"
(keydown.shift.arrowDown)="window.alert('arrowDown')"
(keydown.control.alt.shift.a)="window.alert('a')"
>

是不是很方便?

注:Key 不区分大小写,keydown.arrayDown,keydown.ArrayDown,keydown.arraydown 都可以。

Event Listening 源码逛一逛

Angular 是怎么做到监听 keydown.enter 的呢?难道是 compilation 黑魔法?

after compilation

App Template

<input
(click)="window.alert('click')"
(keydown.enter)="window.alert('enter')"
>

run compilation

yarn run ngc -p tsconfig.json

app.component.js

监听 keydown.enter 和监听普通的 click 事件写法是一样的。也就是说,它并不是用 compilation 黑魔法实现的。

那我们追踪下去看看

ɵɵlistener & renderer.listen

ɵɵlistener 函数的源码在 listener.ts

listenerInternal 函数我们在 <<Signal-based Output 源码逛一逛>> 曾经看见过,不过那时是针对监听 @Output 事件,而不是 DOM 事件。

关键只有一句 renderer.listen。

这个 renderer 是 Root Level Provider,我们日常也可以使用它。

<input #input
(click)="window.alert('click')"
(keydown.enter)="window.alert('enter')"
>

相等于

export class AppComponent {
readonly inputElementRef = viewChild.required('input', { read: ElementRef }); constructor() {
const renderer = inject(Renderer2);
afterNextRender(() =>
renderer.listen(this.inputElementRef().nativeElement, 'keydown.enter', () => window.alert('enter')),
);
} window = window;
}

DefaultDomRenderer2

Angular 其实有好几款 Renderer,在 Animation 动画 文章中我们见过 AnimationRenderer,它是其中一款。

Angular 在启动时会提供一些 built-in 的 Provider (BROWSER_MODULE_PROVIDERS) 给 Root Injector,其中一个是 DomRendererFactory2。

注:上图是 browser 环境下 Angular 默认会提供的 Provider。源码在 browser.ts。以前我们逛 NgModule 和 NodeInjector 源码时也见过它的。

DomRendererFactory2 的源码在 dom_renderer.ts

初始化默认 Renderer 是 DefaultDomRenderer2。

创建 Renderer 时它会依据 RenderType2 选择创建哪一款 Renderer。这里 RenderType2 具体传入的是 ComponentDef。

getOrCreateRenderer 方法

有 3 款 Renderer:EmulatedEncapsulationDomRenderer2,ShadowDomRenderer 和 NoneEncapsulationDomRenderer。

EmulatedEncapsulationDomRenderer2 继承自 NoneEncapsulationDomRenderer

ShadowDomRenderer 和 NoneEncapsulationDomRenderer 都继承自 DefaultDomRenderer2

这些派生类都没有 override listen 方法,所以到头来,renderer.listen 就是 DefaultDomRenderer2.listen 方法,我们追她就是了。

DefaultDomRenderer2.listen 方法

实现代码不在这里,它内部也只是调用了 eventManager.addEventListener 方法而已...追了个寂寞。

EventManager

EventManager 也包含在 BROWSER_MODULE_PROVIDERS 里头

EventManager 源码在 event_manager.ts

里头依然没有具体的 addEventListener,它依赖 EventManagerPlugin...又追了个寂寞。

EventManagerPlugin

EventManagerPlugin 也是包含在 BROWSER_MODULE_PROVIDERS  里头

它是 multiple Provider,一共有 2 个,顾名思义:

  1. DomEventsPlugin

    负责 (click), (mouseenter) 这些

  2. KeyEventsPlugin

    负责 (keydown.enter), (keyup.escape) 这些

KeyEventsPlugin

那我们继续追 KeyEventsPlugin,它的源码在 key_events.ts

关键就在这里了,'keydown.enter' 会被拆解,element.addEventListener 监听的是 'keydown',然后 'enter' 被用作于 callback 的 filter。

用代码来表达大概长这样

const key = 'keydown.enter';
const eventName = key.split('.')[0]; // 'keydown'
const specifyKey = key.split('.')[1]; // 'enter'
const callbackFn = (e: KeyboardEvent) => {
console.log('enter', e);
}; input.addEventListener(eventName, e => {
const keyboardEvent = e as KeyboardEvent;
if (keyboardEvent.key === specifyKey) {
callbackFn(keyboardEvent);
}
});

好,(keydown.enter) 揭秘完成。

(document.click) 语法

Angular 无法超脱三界之外,按理说它是没办法监听到 document 和 body 的,但是它却可以这样写

@if (show()) {
<button (body:click)="click()">click me</button>
<button (document:click)="click()">click me</button>
}

虽然声明 event listener 在 button 但实际上监听的是 body 和 document。

当 button 被销毁,它也会 remove body 和 document 的 event listener。

是不是很神奇?

通常它会搭配 host binding 使用。

Not working in Renderer2.listen

(document:click) 语法不适用于 Renderer2.listen,它只适用于 Template Binding Syntax。

export class HelloWorldComponent {
constructor() {
const hostElement: HTMLElement = inject(ElementRef).nativeElement;
const renderer = inject(Renderer2);
afterNextRender(() => {
// 这是错误的,不要这样写 !!!
renderer.listen(hostElement, 'document:click', () => console.log('click'));
});
}
}

原理是这样的,下图是 compile 之后的 <button (document:click)="log()" >

比平常多了一个参数 ɵɵresolveDocument (源码在 misc_utils.ts)

它是一个函数,接收一个 element 返回 element.ownerDocument

ɵɵlistener 内部会调用 listenerInternal 函数 (源码在 listener.ts)

可以看到,document 是在 Renderer2.listen 之前就弄好的。

Hammer.js Gesture

Angular 可以搭配 Hammer.js 来监听手势 Gesture。

Hammer.js 是一个用来监听 Gesture 手势的库。

虽然它早在 2016 年就已经停止维护了,但时至今日它依然是许多人的选择 (npm 7 days download 1.3m)

Angular 也选择了它作为监听手势的底层实现。

由于不是很多项目需要支持手势操作,所以它默认是不开启的,我们需要做一些 setup 才能使用它。

我们先来看看它的使用方式,之后才讲解如何 setup。

App Template

 <div (swipe)="display.textContent = display.textContent + ' swipe'" class="slider">
<p>gesture me</p>
</div> <div class="display" #display></div>

swipe 是 Hammer.js 其中一个手势事件。

效果

Hammer.js 还支持很多其它的手势 (比如:Pan,Pinch,Press,Rotate,Swipe,Tap 等等)

想了解更多的朋友可以看官网 Docs – Hammer.js (或许有一天我会写一篇文章来讲解 Hammer.js)

Setup Hammer.js

首先在 app.config.ts 提供相关的 Provider。

import { importProvidersFrom, type ApplicationConfig } from '@angular/core';
import { HammerModule } from '@angular/platform-browser'; export const appConfig: ApplicationConfig = {
providers: [importProvidersFrom(HammerModule)],
};

没有 provideHammer 函数,我们只能用 importProvidersFrom + HammerModule 的方式来提供 Provider。

HammerModule 源码在 hammer_gestures.ts

没错,它也是一个 EventManagerPlugin。至于它背地里做了什么,我想大家应该推测的出来,我们就不再逛源码了。

Load Hammer.js

单单提供 Provider 是不够的。

它会响警报。

原因是 Angular 不会替我们加载 Hammer.min.js。

我们需要自己来

yarn add hammerjs
yarn add @types/hammerjs --dev

然后 import 'hammerjs'

只要 Angular 在 listening 时,window 有 Hammer 属性就可以了

另外,它也支持 lazy loading 哦。

import { importProvidersFrom, type ApplicationConfig } from '@angular/core';
import { HAMMER_LOADER, HammerModule } from '@angular/platform-browser'; export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(HammerModule),
{
// 1. 提供 HAMMER_LOADER Token
provide: HAMMER_LOADER,
// 2. 一个返回 Promise<void> 的函数
useValue: () => import('hammerjs'),
},
],
};

等到要 addEventListener 时才去加载 Hammer.min.js

总结

严格来说 Angular 并没有 built-in 支持手势监听,它只是 built-in 了一个基于 Hammer.js 的 EventManagerPlugin 而已。

如果我们不使用 Hammer.js 那基本上 Angular 完全没有帮上忙。

Custom EventManagerPlugin

Angular built-in 有 DomEventsPlugin,KeyEventsPlugin,HammerGesturesPlugin。

我们也可以提供自定义的 EventManagerPlugin 去扩展事件监听。

 <div (control.click)="window.alert('control click')" class="slider" role="presentation">
<p>gesture me</p>
</div>

监听 control 键 + mouse click 事件。

自定义 EventManagerPlugin

@Injectable()
export class ControlClickEventManagerPlugin extends EventManagerPlugin {
constructor() {
super(inject(DOCUMENT));
}
override supports(eventName: string): boolean {
return eventName === 'control.click';
}
override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
const callback = (e: MouseEvent) => {
if (e.ctrlKey) {
handler(e);
}
};
element.addEventListener('click', callback);
return () => {
element.removeEventListener('click', callback);
};
}
}

提供 Provider

export const appConfig: ApplicationConfig = {
providers: [
{
provide: EVENT_MANAGER_PLUGINS,
useClass: ControlClickEventManagerPlugin,
multi: true,
},
],
};

搞定!

提醒:EventManagerPlugin 必须提供给 Root Level Injector 哦 (它目前还不支持 lazy loading),因为依赖 EventManagerPlugin 的 EventManger 和 Renderer 都是 Root Level 的。

相关 Github Issue – Support lazy-loaded event plugins

目录

上一篇 Angular 18+ 高级教程 – Routing 路由 (功能篇)

下一篇 Angular 18+ 高级教程 – Library

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding

Angular 18+ 高级教程 – EventManagerPlugin & Hammer.js Gesture的更多相关文章

  1. JS高级教程

    JS高级教程 JS高级教程

  2. Hammer.js分析(一)——基础结构

    从github上面将源码下载下来,会发现有个src文件夹.当前版本是2.0.6. 总的结构如下: 一.常量 这里将常量全部列在一起是可以在对比源码的时候,更方便的查看相应内容. var VENDOR_ ...

  3. hammer用法 jquery.hammer.js

    jquery.hammer.js使用时要先引入hammer.min.js 下面代码是滑动效果:   $("#nav").hammer().bind('swiperight', fu ...

  4. hammer.js的六大事件

    1.Pan事件:在指定的dom区域内,一个手指放下并移动事件,即触屏中的拖动事件.这个事件在触屏开发中比较常用: Panstart 拖动开始 Panmove 拖动过程 Panend 拖动结束 Panc ...

  5. Hammer.js

    一.前言 移动端框架当前还处在初级阶段,但相对于移动端的应用来说已经有很长时间了.虽然暂时还没有PC端开发的需求量大,但移动端的Web必然是一种趋势,在接触移动端脚本的过程中,最开始想到的是juqer ...

  6. javascript 手势缩放 旋转 拖动支持:hammer.js

    原文: https://cdn.rawgit.com/hammerjs/hammer.js/master/tests/manual/visual.html /*! Hammer.JS - v2.0.4 ...

  7. Hammer.js移动端触屏框架的使用

    hammer.js是一个多点触摸手势库,能够为网页加入Tap.Double Tap.Swipe.Hold.Pinch.Drag等多点触摸事件,免去自己监听底层touchstart.touchmove. ...

  8. 移动开发框架,第【二】弹:Hammer.js 移动设备触摸手势js库

    hammer.js是一个多点触摸手势库,能够为网页加入Tap.Double Tap.Swipe.Hold.Pinch.Drag等多点触摸事件,免去自己监听底层touchstart.touchmove. ...

  9. hammer.js初探

    hammer.js官方文档 hammerjs是什么 hammerjs是一个短小精悍的库,他可以让我们轻松的实现移动端上的手势. hammerjs的两大优势如下: 为移动端网页添加相关手势. 去除移动端 ...

  10. 插件: Hammer.js

    官网: http://hammerjs.github.io/  hammer.js 官网 http://hammerjs.github.io/api/ 官网API(官网写的实在太简了!不好用.注意里面 ...

随机推荐

  1. ELK Stack - Elasticsearch · 搜索引擎 · 部署应用 · 内部结构 · 倒排索引 · 服务接入

    系列目录 ELK Stack - Elasticsearch · 搜索引擎 · 全文检索 · 部署应用 · 内部结构 · 倒排索引 · 服务接入 ELK Stack - Kibana (待续) ELK ...

  2. 使用GSAP制作动画视频

    GSAP 3Blue1Brown给我留下了深刻印象.利用动画制作视频,内容简洁,演示清晰.前两天刚好碰到一件事,我就顺便学习了一下怎么用代码做动画. 以javascrip为例,有两个动画引擎,GSAP ...

  3. [oeasy]python0112_扩展ascii_Extended_ascii_法文字符

    法文字符 回忆上次内容 上次回顾了 字型编码的进化过程 从 7-seg 到 点阵字库 终于让字母.数字.标点 明确了字型 小写字符 占据了位置 法文字符 没有地方放了     ​   添加图片注释,不 ...

  4. EasyDesktop 浏览器书签管理从未如此简单

    作为一名软件开发从业人员, 每天80%的时间都在与浏览器打交道, 一半的时间在用浏览器开发调试, 另一半时间则是在互联网上搜寻各种知识和资源. 为此, 我的浏览器书签栏存储和很多非常棒的链接, 多到2 ...

  5. 使用JavaScript编写vue指令v-model,v-model原理实现

    首先先要知道的是v-model的作用是实现数据的双向绑定,即: 数据在视图层的双向响应. 实现思路主要分为两步: 第一步:数据层到视图层的响应 将数据响应到视图层的方式,在vue2使用的是Object ...

  6. 20+前端常用的vscode插件(总结推荐)

    本篇文章给大家总结分享20多个前端常用的vscode插件.有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助. 1. vscode 简介vscode是微软开发的的一款代码编辑器,就如官网上 ...

  7. Microsoft Dynamics CRM 插件被限制2分钟超时解决方案

    背景: 在隔离模式"沙箱"中运行的插件或自定义工作流活动将有2分钟的硬限制.如果你的插件很复杂,需要超过2分钟,有一些解决方法. CRM on premise (本地版) 选择插件 ...

  8. 【Vue】使用iframe解决多应用整合问题(微前端)

    一.需求背景 有老系统需要重构,新做的系统需要做一个大一统的整合,类似一个分类栏目 在菜单位置罗列出有什么子系统应用,点击对应的应用菜单,展示区跳转到相应的子系统应用中 我用Excel简单描述了下系统 ...

  9. 【Mybatis】11 注解的使用

    文档引用:http://www.mybatis.cn/archives/678.html 视频参考:https://www.bilibili.com/video/BV1NE411Q7Nx?p=15 注 ...

  10. 【Mybatis】09 日志工厂配置 & Log4J

    什么是日志 Log? 系统日志是记录系统中硬件.软件和系统问题的信息,同时还可以监视系统中发生的事件. 用户可以通过它来检查错误发生的原因,或者寻找受到攻击时攻击者留下的痕迹. 系统日志包括系统日志. ...