Dynamically add event listener

前端 未结 4 806
小鲜肉
小鲜肉 2020-11-22 10:21

I am just starting to mess around with Angular 2 and I wonder if anyone can tell me the best way to dynamically add and remove event listeners from elements.

I have

相关标签:
4条回答
  • 2020-11-22 10:54

    I will add a StackBlitz example and a comment to the answer from @tahiche.

    The return value is a function to remove the event listener after you have added it. It is considered good practice to remove event listeners when you don't need them anymore. So you can store this return value and call it inside your ngOnDestroy method.

    I admit that it might seem confusing at first, but it is actually a very useful feature. How else can you clean up after yourself?

    export class MyComponent implements OnInit, OnDestroy {
    
      public removeEventListener: () => void;
    
      constructor(
        private renderer: Renderer2, 
        private elementRef: ElementRef
      ) {
      }
    
      public ngOnInit() {
        this.removeEventListener = this.renderer.listen(this.elementRef.nativeElement, 'click', (event) => {
          if (event.target instanceof HTMLAnchorElement) {
            // Prevent opening anchors the default way
            event.preventDefault();
            // Your custom anchor click event handler
            this.handleAnchorClick(event);
          }
        });
      }
    
      public ngOnDestroy() {
        this.removeEventListener();
      }
    }
    

    You can find a StackBlitz here to show how this could work for catching clicking on anchor elements.

    I added a body with an image as follows:
    <img src="x" onerror="alert(1)"></div>
    to show that the sanitizer is doing its job.

    Here in this fiddle you find the same body attached to an innerHTML without sanitizing it and it will demonstrate the issue.

    0 讨论(0)
  • 2020-11-22 10:59

    Here's my workaround:

    I created a library with Angular 6. I added a common component commonlib-header which is used like this in an external application.

    Note the serviceReference which is the class (injected in the component constructor(public serviceReference: MyService) that uses the commonlib-header) that holds the stringFunctionName method:

    <commonlib-header
        [logo]="{ src: 'assets/img/logo.svg', alt: 'Logo', href: '#' }"
        [buttons]="[{ index: 0, innerHtml: 'Button', class: 'btn btn-primary', onClick: [serviceReference, 'stringFunctionName', ['arg1','arg2','arg3']] }]">
        </common-header>
    

    The library component is programmed like this. The dynamic event is added in the onClick(fn: any) method:

    export class HeaderComponent implements OnInit {
    
     _buttons: Array<NavItem> = []
    
     @Input()
      set buttons(buttons: Array<any>) {
        buttons.forEach(navItem => {
          let _navItem = new NavItem(navItem.href, navItem.innerHtml)
    
          _navItem.class = navItem.class
    
          _navItem.onClick = navItem.onClick // this is the array from the component @Input properties above
    
          this._buttons[navItem.index] = _navItem
        })
      }
    
      constructor() {}
    
      ngOnInit() {}
    
      onClick(fn: any){
        let ref = fn[0]
        let fnName = fn[1]
        let args = fn[2]
    
        ref[fnName].apply(ref, args)
      }
    

    The reusable header.component.html:

    <div class="topbar-right">
      <button *ngFor="let btn of _buttons"
        class="{{ btn.class }}"
        (click)="onClick(btn.onClick)"
        [innerHTML]="btn.innerHtml | keepHtml"></button>
    </div>
    
    0 讨论(0)
  • 2020-11-22 11:01

    Renderer has been deprecated in Angular 4.0.0-rc.1, read the update below

    The angular2 way is to use listen or listenGlobal from Renderer

    For example, if you want to add a click event to a Component, you have to use Renderer and ElementRef (this gives you as well the option to use ViewChild, or anything that retrieves the nativeElement)

    constructor(elementRef: ElementRef, renderer: Renderer) {
    
        // Listen to click events in the component
        renderer.listen(elementRef.nativeElement, 'click', (event) => {
          // Do something with 'event'
        })
    );
    

    You can use listenGlobal that will give you access to document, body, etc.

    renderer.listenGlobal('document', 'click', (event) => {
      // Do something with 'event'
    });
    

    Note that since beta.2 both listen and listenGlobal return a function to remove the listener (see breaking changes section from changelog for beta.2). This is to avoid memory leaks in big applications (see #6686).

    So to remove the listener we added dynamically we must assign listen or listenGlobal to a variable that will hold the function returned, and then we execute it.

    // listenFunc will hold the function returned by "renderer.listen"
    listenFunc: Function;
    
    // globalListenFunc will hold the function returned by "renderer.listenGlobal"
    globalListenFunc: Function;
    
    constructor(elementRef: ElementRef, renderer: Renderer) {
        
        // We cache the function "listen" returns
        this.listenFunc = renderer.listen(elementRef.nativeElement, 'click', (event) => {
            // Do something with 'event'
        });
    
        // We cache the function "listenGlobal" returns
        this.globalListenFunc = renderer.listenGlobal('document', 'click', (event) => {
            // Do something with 'event'
        });
    }
    
    ngOnDestroy() {
        // We execute both functions to remove the respectives listeners
    
        // Removes "listen" listener
        this.listenFunc();
        
        // Removs "listenGlobal" listener
        this.globalListenFunc();
    }
    

    Here's a plnkr with an example working. The example contains the usage of listen and listenGlobal.

    Using RendererV2 with Angular 4.0.0-rc.1+ (Renderer2 since 4.0.0-rc.3)

    • 25/02/2017: Renderer has been deprecated, now we should use RendererV2 (see line below). See the commit.

    • 10/03/2017: RendererV2 was renamed to Renderer2. See the breaking changes.

    RendererV2 has no more listenGlobal function for global events (document, body, window). It only has a listen function which achieves both functionalities.

    For reference, I'm copy & pasting the source code of the DOM Renderer implementation since it may change (yes, it's angular!).

    listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean):
          () => void {
        if (typeof target === 'string') {
          return <() => void>this.eventManager.addGlobalEventListener(
              target, event, decoratePreventDefault(callback));
        }
        return <() => void>this.eventManager.addEventListener(
                   target, event, decoratePreventDefault(callback)) as() => void;
      }
    

    As you can see, now it verifies if we're passing a string (document, body or window), in which case it will use an internal addGlobalEventListener function. In any other case, when we pass an element (nativeElement) it will use a simple addEventListener

    To remove the listener it's the same as it was with Renderer in angular 2.x. listen returns a function, then call that function.

    Example

    // Add listeners
    let global = this.renderer.listen('document', 'click', (evt) => {
      console.log('Clicking the document', evt);
    })
    
    let simple = this.renderer.listen(this.myButton.nativeElement, 'click', (evt) => {
      console.log('Clicking the button', evt);
    });
    
    // Remove listeners
    global();
    simple();
    

    plnkr with Angular 4.0.0-rc.1 using RendererV2

    plnkr with Angular 4.0.0-rc.3 using Renderer2

    0 讨论(0)
  • 2020-11-22 11:07

    I aso find this extremely confusing. as @EricMartinez points out Renderer2 listen() returns the function to remove the listener:

    ƒ () { return element.removeEventListener(eventName, /** @type {?} */ (handler), false); }
    

    If i´m adding a listener

    this.listenToClick = this.renderer.listen('document', 'click', (evt) => {
        alert('Clicking the document');
    })
    

    I´d expect my function to execute what i intended, not the total opposite which is remove the listener.

    // I´d expect an alert('Clicking the document'); 
    this.listenToClick();
    // what you actually get is removing the listener, so nothing...
    

    In the given scenario, It´d actually make to more sense to name it like:

    // Add listeners
    let unlistenGlobal = this.renderer.listen('document', 'click', (evt) => {
        console.log('Clicking the document', evt);
    })
    
    let removeSimple = this.renderer.listen(this.myButton.nativeElement, 'click', (evt) => {
        console.log('Clicking the button', evt);
    });
    

    There must be a good reason for this but in my opinion it´s very misleading and not intuitive.

    0 讨论(0)
提交回复
热议问题