blog
blog copied to clipboard
Angular 事件绑定扩展增强 - Angular Events Plugin
Angular提供了许多事件类型来与你的应用进行通信。 Angular中的事件可帮助你在特定条件下触发操作,例如单击,滚动,悬停,聚焦,提交等。
通过事件,可以在Angular应用中触发组件的逻辑。
Angular事件
Angular 组件和 DOM 元素通过事件与外部进行通信, Angular 事件绑定语法对于组件和 DOM 元素来说是相同的 - (eventName)="expression"
。
DOM 元素触发的一些事件通过 DOM 层级结构传播。这种传播过程称为事件冒泡。事件首先由最内层的元素开始,然后传播到外部元素,直到它们到根元素。DOM 事件冒泡与 Angular 可以无缝工作。
Angular事件分为原生事件和自定义事件:
Angular Events 常用列表
(click)="myFunction()"
(dblclick)="myFunction()"
(submit)="myFunction()"
(blur)="myFunction()"
(focus)="myFunction()"
(scroll)="myFunction()"
(cut)="myFunction()"
(copy)="myFunction()"
(paste)="myFunction()"
(keyup)="myFunction()"
(keypress)="myFunction()"
(keydown)="myFunction()"
(mouseup)="myFunction()"
(mousedown)="myFunction()"
(mouseenter)="myFunction()"
(drag)="myFunction()"
(drop)="myFunction()"
(dragover)="myFunction()"
默认处理的事件应从原始HTML DOM
组件的事件映射:
关于原生事件有哪些,可以参照W3C标准事件。
只需删除on
前缀即可。
- onclick ---> (click)
- onkeypress ---> (keypress)
- 等等
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: '<button (click)="myFunction($event)">Click Me</button>',
styleUrls: ['./app.component.css']
})
export class AppComponent {
myFunction(event) {
console.log('Hello World');
}
}
当我们点击按钮时候,控制台就会打印Hello World
。
Angular 允许开发者通过 @Output()
装饰器和 EventEmitter
自定义事件。它不同于 DOM 事件,因为它不支持事件冒泡。
@Component({
selector: 'my-selector',
template: `
<div>
<button (click)="callSomeMethodOfTheComponent()">Click</button>
<sub-component (my-event)="callSomeMethodOfTheComponent($event)"></sub-component>
</div>
`,
directives: [SubComponent]
})
export class MyComponent {
callSomeMethodOfTheComponent() {
console.log('callSomeMethodOfTheComponent called');
}
}
@Component({
selector: 'sub-component',
template: `
<div>
<button (click)="myEvent.emit()">Click (from sub component)</button>
</div>
`
})
export class SubComponent {
@Output()
myEvent: EventEmitter;
constructor() {
this.myEvent = new EventEmitter();
}
}
自定义事件写法和原生Dom事件一样。那么它们需要注意:
- DOM 事件冒泡机制,允许在父元素监听由子元素触发的 DOM 事件
- Angular 支持 DOM 事件冒泡机制,但不支持自定义事件的冒泡。
- 自定义事件名称与 DOM 事件的名称如 (click,change,select,submit) 同名,可能会导致问题。虽然可以使用
stopPropagation()
方法解决问题,但实际工作中,不建议这样使用。 - 自定义事件不要使用
on
前缀,方法名可以使用on
开头,参考风格指南-不要给输出属性加前缀 - 原生事件的
$event
返回是 DOM Events - 自定义事件的
$event
返回是EventEmitter.emit()
传递的值,也可以使用EventEmitter.next()
。
事件修饰
在实际项目中,我们经常需要在事件处理器中调用 preventDefault()
或 stopPropagation()
方法。
还有一个比较少用功能比较强大,
stopPropagation
增强版stopImmediatePropagation()
。
- preventDefault() - 如果事件可取消,则取消该事件,意味着该事件的所有默认动作都不会发生。需要注意的是该方法不会阻止该事件的冒泡。
- stopPropagation() - 阻止当前事件在 DOM 树上冒泡。
- stopImmediatePropagation() - 除了阻止事件冒泡之外,还可以把这个元素绑定的同类型事件也阻止了。
preventDefault()
最常见的例子就是 <a>
阻止标签跳转链接
<a id="link" href="https://www.baidu.com">baidu</a>
<script>
document.getElementById('link').onclick = function(ev) {
ev.preventDefault(); // 阻止浏览器默认动作 (页面跳转)
// 处理一些其他事情
window.open(this.href); // 在新窗口打开页面
};
</script>
在Angular中使用:
preventDefault()
页面直接使用:
<a id="link" href="https://www.baidu.com" (click)="$event..preventDefault(); myFunction()">baidu</a>
还可以使用:
```html
<a id="link" href="https://www.baidu.com" (click)="myFunction($event); false">baidu</a>
stopPropagation()
页面直接使用:
<a id="link" href="https://www.baidu.com" (click)="$event.stopPropagation(); myFunction($event)">baidu</a>
在事件处理方法里面使用和原生一样。
myFunction(e: Event) {
e.stopPropagation();
e.preventDefault();
// ...code
return false;
}
看完 Angular 提供写法,写法太麻烦。
项目中最常用当属stopPropagation()
,懒惰的程序员就想到各种方法:
方法1:
import {Directive, HostListener} from "@angular/core";
@Directive({
selector: "[click-stop-propagation]"
})
export class ClickStopPropagation
{
@HostListener("click", ["$event"])
public onClick(event: any): void
{
event.stopPropagation();
}
}
弄一个阻止冒泡的指令
<div click-stop-propagation>Stop Propagation</div>
方法2:
import { Directive, EventEmitter, Output, HostListener } from '@angular/core';
@Directive({
selector: '[appClickStop]'
})
export class ClickStopDirective {
@Output() clickStop = new EventEmitter<MouseEvent>();
constructor() { }
@HostListener('click', ['$event'])
clickEvent(event: MouseEvent) {
event.stopPropagation();
event.preventDefault();
this.clickStop.emit(event);
}
}
弄一个阻止冒泡的自定义事件指令
<div appClickStop (clickStop)="testClick()"></div>
看起来很不错,就是支持click
事件,我要支持多种事件,我需要些更多的指令。
用过 Vue - 事件修饰( Event modifiers ) 的同学,一定让使用 Angular 的同学很羡慕。
<button v-on:click="add(1)"></button> # 普通事件
<button v-on:click.once="add(1)"></button> # 这里只监听一次
<a v-on:click.prevent="click" href="http://google.com">click me</a> # 阻止默认事件
<div class="parent" v-on:click="add(1)">
<div class="child" v-on:click.stop="add(1)">click me</div> # 阻止冒泡
</div>
那 Angular 可以实现吗?当然
import { Directive, EventEmitter, Output, HostListener, OnDestroy, OnInit, Input } from '@angular/core';
import { Subject, } from 'rxjs';
import { takeUntil, throttleTime} from 'rxjs/operators';
@Directive({
selector: '[click.stop]',
})
export class ClickStopDirective implements OnInit ,OnDestroy{
@Output('click.stop') clickStop = new EventEmitter<MouseEvent>();
/// 自定义间隔
@Input() throttleTime = 1000;
click$: Subject<MouseEvent> = new Subject<MouseEvent>()
onDestroy$ = new Subject();
@HostListener('click', ['$event'])
clickEvent(event: MouseEvent) {
event.stopPropagation();
event.preventDefault();
this.click$.next(event);
}
constructor() { }
ngOnInit() {
this.click$.pipe(
takeUntil(this.onDestroy$),
throttleTime(this.throttleTime)
).subscribe((event) => {
this.clickStop.emit(event);
})
}
ngOnDestroy() {
/// 销毁并取消订阅
this.onDestroy$.next();
this.onDestroy$.complete();
}
}
扩展一个原生事件指令
<div class="parent" (click)="add(1)">
<div class="child" (click.stop)="add(1)">click me</div>
</div>
看起来很美好,还支持防抖骚操作,缺点还是支持一个事件,如果需要多种事件需要写更多的事件指令。
Angular 不支持 (事件名.修饰) 这种语法吗?
如果你用过键盘事件,你就会发现,Angular 提供一系列的快捷操作:
当绑定到Angular模板中的keyup或keydown事件时,可以指定键名。 这使得仅在按下特定键时才很容易触发事件。
<input (keydown.enter)="onKeydown($event)">
还可以将按键组合在一起以仅在触发按键组合时触发事件。 在以下示例中,仅当同时按下Control和1键时才会触发事件:
<input (keyup.control.1)="onKeydown($event)">
此功能适用于特殊键和修饰键,例如Enter
,Esc
,Shift
,Alt
,Tab
,Backspace
和Command
,但它也适用于字母,数字,方向箭头和F键(F1-F12)。
<input (keydown.enter)="...">
<input (keydown.a)="...">
<input (keydown.esc)="...">
<input (keydown.shift.esc)="...">
<input (keydown.control)="...">
<input (keydown.alt)="...">
<input (keydown.meta)="...">
<input (keydown.9)="...">
<input (keydown.tab)="...">
<input (keydown.backspace)="...">
<input (keydown.arrowup)="...">
<input (keydown.shift.arrowdown)="...">
<input (keydown.shift.control.z)="...">
<input (keydown.f4)="...">
这个看起来很不错呀,和 Vue 那个事件修饰写法一致。这种可以 Angular 原生实现,那一定有方法可以做到。
在源码里面由一个突出的导入:
import {EventManagerPlugin} from './event_manager';
而的实现,
export class KeyEventsPlugin extends EventManagerPlugin {}
就是继承了这个抽象类
export abstract class EventManagerPlugin {
constructor(private _doc: any) {}
manager!: EventManager;
abstract supports(eventName: string): boolean;
abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function;
addGlobalEventListener(element: string, eventName: string, handler: Function): Function {
const target: HTMLElement = getDOM().getGlobalEventTarget(this._doc, element);
if (!target) {
throw new Error(`Unsupported event target ${target} for event ${eventName}`);
}
return this.addEventListener(target, eventName, handler);
}
}
抽象类里面我们需要实现supports
和addEventListener
方法。
- supports:传递一个事件名,来验证是否支持,如果不支持,就不会执行事件了
- addEventListener:事件绑定,包装了
Dom.addEventListener()
方法。默认使用冒泡
在 DomEventsPlugin
的类实现:
addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
element.addEventListener(eventName, handler as EventListener, false);
return () => this.removeEventListener(element, eventName, handler as EventListener);
}
在我们使用 Renderer2.listen
绑定事件时候:如果需要销毁事件
// 绑定事件
const fn = Renderer2.listen();
// 销毁事件
fn();
这种操作就是源码是这样的实现的。
listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean):
() => void {
NG_DEV_MODE && checkNoSyntheticProp(event, 'listener');
if (typeof target === 'string') {
return <() => void>this.eventManager.addGlobalEventListener(
target, event, decoratePreventDefault(callback));
}
return <() => void>this.eventManager.addEventListener(
target, event, decoratePreventDefault(callback)) as () => void;
}
关于 Angular Events Plugin 的文章介绍很少,所以很多人不知道可以有以下的骚操作。
我们也来实现事件修饰符:
- once - 只绑定一次,调用完成即销毁。 使用
Renderer2.listen
绑定事件实现 - stop - 阻止冒泡。使用
stopPropagation()
实现。 - prevent - 阻止默认事件。使用
preventDefault()
实现。
新建三个文件:
once.plugin.ts
stop.plugin.ts
prevent.plugin.ts
先从常用的 .stop
开始:
注意:EventManagerPlugin
是一个内部抽象类,所以我们无法扩展它
import { Injectable, Inject } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
const MODIFIER = '.stop';
@Injectable()
export class StopEventPlugin {
manager: EventManager;
supports(eventName: string): boolean {
return eventName.indexOf(MODIFIER) !== -1;
}
addEventListener(
element: HTMLElement,
eventName: string,
handler: Function
): Function {
const stopped = (event: Event) => {
event.stopPropagation();
handler(event);
}
return this.manager.addEventListener(
element,
eventName.replace(MODIFIER, ''),
stopped,
);
}
addGlobalEventListener(element: string, eventName: string, handler: Function): Function {
const stopped = (event: Event) => {
event.stopPropagation();
handler(event);
}
return this.manager.addGlobalEventListener(
element,
eventName.replace(MODIFIER, ''),
stopped,
);
}
}
我们这里使用先去supports
查询,只有事件名里面有.stop
,才会执行StopEventPlugin
。
addEventListener里面调用的EventManager.addEventListener
,我们只需要对事件处理函数进行包装一下即可:
const stopped = (event: Event) => {
event.stopPropagation();
handler(event);
}
在把包装之后的处理函数返还给EventManager.addEventListener
,并且去掉.stop
,防止死循环。
.prevent
基本和.stop
一模一样:
import { Injectable, Inject } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
const MODIFIER = '.prevent';
@Injectable()
export class PreventEventPlugin {
manager: EventManager;
supports(eventName: string): boolean {
return eventName.indexOf(MODIFIER) !== -1;
}
addEventListener(
element: HTMLElement,
eventName: string,
handler: Function
): Function {
const prevented = (event: Event) => {
event.preventDefault();
handler(event);
}
return this.manager.addEventListener(
element,
eventName.replace(MODIFIER, ''),
prevented,
);
}
addGlobalEventListener(element: string, eventName: string, handler: Function): Function {
const prevented = (event: Event) => {
event.preventDefault();
handler(event);
}
return this.manager.addGlobalEventListener(
element,
eventName.replace(MODIFIER, ''),
prevented,
);
}
}
.once
有点特殊:
import { Injectable, Inject } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
const MODIFIER = '.once';
@Injectable()
export class OnceEventPlugin {
manager: EventManager;
supports(eventName: string): boolean {
return eventName.indexOf(MODIFIER) !== -1;
}
addEventListener(
element: HTMLElement,
eventName: string,
handler: Function
): Function {
const fn = this.manager.addEventListener(
element,
eventName.replace(MODIFIER, ''),
(event: Event) => {
handler(event);
fn();
},
);
return () => {};
}
addGlobalEventListener(element: string, eventName: string, handler: Function): Function {
const fn = this.manager.addGlobalEventListener(
element,
eventName.replace(MODIFIER, ''),
(event: Event) => {
handler(event);
fn();
},
);
return () => {};
}
}
fn 返回的就是 return () => this.removeEventListener(element, eventName, handler as EventListener);
,.once
操作就是使用一次就注销事件操作。所以我们先把fn获取到,然后事件调用完成以后取消绑定即可。最后要返回一个空函数,不然手动销毁事件就会抛出错误。
在根模块注册插件:
import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser';
import { PreventEventPlugin } from './prevent.plugin';
import { StopEventPlugin } from './stop.plugin';
import { OnceEventPlugin } from './once.plugin';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: [
AppComponent,
],
providers: [
....,
{
provide: EVENT_MANAGER_PLUGINS,
useClass: PreventEventPlugin,
multi: true,
},
{
provide: EVENT_MANAGER_PLUGINS,
useClass: StopEventPlugin,
multi: true,
},
{
provide: EVENT_MANAGER_PLUGINS,
useClass: OnceEventPlugin,
multi: true,
},
],
bootstrap: [AppComponent]
})
export class AppModule { }
这样我们就可以正常使用了
<a href="https://www.baidu.com" (click.prevent)="onConsole($event)">baidu</a>
<div (click)="onConsole($event)">
标题
<div (click.stop)="onConsole($event)">内容</div>
</div>
<form (submit.stop)="onConsole($event)">
<input name="username">
<button type="submit">提交</button>
</form>
<div (click)="onConsole($event)">
标题
<div (click.once)="onConsole($event)">内容</div>
</div>
事件处理函数:
onConsole($event: Event) {
console.log('onConsole',$event.target)
}
我们已经实现普遍版本的事件修饰,如果想要加上防抖,节流更风骚的操作我们该如何做了,这个留个大家一个悬念,可以思考一下,欢迎和我交流心得。
最后:我们不光可以做事件修饰插件还可以做事件打印日志插件,你看完上面的例子,应该很简单操作了。如果不知道怎么下手,欢迎和我交流心得。
今天就到这里吧,伙计们,玩得开心,祝你好运