前言
今天來揭祕一下 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 個,顧名思義:
-
DomEventsPlugin
負責 (click), (mouseenter) 這些
-
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) 揭祕完成。
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 17+ 高級教程 – Routing 路由 (功能篇)
下一篇 Angular 17+ 高級教程 – Prettier, ESLint, Stylelint
想查看目錄,請移步 Angular 17+ 高級教程 – 目錄