Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular 事件绑定扩展增强 - Angular Events Plugin #33

Open
jiayisheji opened this issue Sep 7, 2020 · 0 comments
Open

Angular 事件绑定扩展增强 - Angular Events Plugin #33

jiayisheji opened this issue Sep 7, 2020 · 0 comments
Labels
Angular angular相关实践

Comments

@jiayisheji
Copy link
Owner

jiayisheji commented Sep 7, 2020

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事件一样。那么它们需要注意:

  1. DOM 事件冒泡机制,允许在父元素监听由子元素触发的 DOM 事件
  2. Angular 支持 DOM 事件冒泡机制,但不支持自定义事件的冒泡。
  3. 自定义事件名称与 DOM 事件的名称如 (click,change,select,submit) 同名,可能会导致问题。虽然可以使用 stopPropagation()方法解决问题,但实际工作中,不建议这样使用。
  4. 自定义事件不要使用on前缀,方法名可以使用on开头,参考风格指南-不要给输出属性加前缀
  5. 原生事件的$event返回是 DOM Events
  6. 自定义事件的$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)">

此功能适用于特殊键和修饰键,例如EnterEscShiftAltTabBackspaceCommand,但它也适用于字母,数字,方向箭头和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 原生实现,那一定有方法可以做到。

我们去查看源码:https://github.com/angular/angular/blob/master/packages/platform-browser/src/dom/events/key_events.ts

在源码里面由一个突出的导入:

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);
  }
}

抽象类里面我们需要实现supportsaddEventListener方法。

  • 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)
  }

我们已经实现普遍版本的事件修饰,如果想要加上防抖,节流更风骚的操作我们该如何做了,这个留个大家一个悬念,可以思考一下,欢迎和我交流心得。

最后:我们不光可以做事件修饰插件还可以做事件打印日志插件,你看完上面的例子,应该很简单操作了。如果不知道怎么下手,欢迎和我交流心得。

今天就到这里吧,伙计们,玩得开心,祝你好运

@jiayisheji jiayisheji added the Angular angular相关实践 label Sep 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Angular angular相关实践
Projects
None yet
Development

No branches or pull requests

1 participant