Dynamically add event listener
Asked Answered
C

4

160

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 a component set up. When a certain element in the template is clicked I want to add a listener for mousemove to another element of the same template. I then want to remove this listener when a third element is clicked.

I kind of got this working just using plain Javascript to grab the elements and then calling the standard addEventListener() but I wondered if there was a more "Angular2.0" way of doing this that I should be looking into.

Chrysanthemum answered 29/1, 2016 at 8:53 Comment(1)
How about using an RXJS way of doing it ? Sth on the lines of what's suggested in dev.to/angular/ain-t-nobody-needs-hostlistener-fg4 let stopListeningEvent: Subject<boolean> = new Subject(); fromEvent(document, 'mousemove').takeUntil( stopListeningEmits).subscribe(() => { console.log('mouse move event detected'); }); signalStopListening() { stopListeningEvent.next(true); }Synge
M
289

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

Monikamoniker answered 29/1, 2016 at 10:41 Comment(12)
This is only my second day with Angular2 and I had barely started getting my head around v1 so a lot of this is rather new confusing. You have given me a good load of stuff to read up on though so I'm closing this one down and will doubtless be back soon with LOTS more related questions. Cheers for the detailed response :)Chrysanthemum
@Chrysanthemum you can check HostListener as well. In the docs, check the Attribute directives under Respond to user action to see how host is used too.Monikamoniker
@EricMartinez is there a way to stop listening to either listen or listenGlobal? (same as removeEventListener)Dyna
@user1394625 yes, as you can see in the answer the ngOnDestroy code, both listen and listenGlobal return a function that when called/executed the listener is removed. So as you see this.func is holding the function returned by renderer.listen and when I do this.func() I'm removing the listener. The same goes for listenGlobal.Monikamoniker
@EricMartinez got one more questions for you...how can I access the 'event' inside the function to preventDefault() or stopPropagation()Dyna
@EricMartinez, is there a way I could conditionally use [(ngModel)] or (blur)? I guess I'm sort of asking about a dynamic way of setting ngModelOptions equivalent in Angular 2.Italianism
@EricMartinez how do you listenGlobal('document.getElementById("myId")', ...)? I can't find any doc on listenGlobalSniff
@EricMartinez how can I do that on document's concrete component? With listenGlobal('document.getElementById("myId")', ...) ?Fiddling
I used this answer to recognize an outer click for a costumized bootstrap dropdown. I need this because I have more complex components than an item in it and don't want to close the dropdown by clicking in once. The problem is that I open the dropdown by another button outside of it. In the showDropDown() method I register the listener and in the hideDropDown() I remove it. The problem now is the event is raised although I am registering it in the showDropDown method the first time. I think it is because the document click event is raised after the method, but how can I get the event afterMadeira
This solution in combination with the focusin and focusout events is a godsend for handling focus and blur when you can't get a handle on the element that you need to. In my case, I'm wrapping a third party datepicker and it does not emit blur and focus events.Rheingold
@EricMartinez, how do listen to click event when the element is not available initially and only available after ngif condition to true? modified example from your with Angular 4.0.0-rc.3 using Renderer2Abbreviate
Is it possible to listen for DOM events from service or is it considered a bad practice? — Using service to listen for DOM events in AngularSusi
A
9

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.

Alible answered 18/3, 2020 at 16:23 Comment(4)
does the removeEventListener have an interface? as I understand you could also run removeEventListener.unsubscribe() ?Venery
@Venery unscubscribe is typically something used for Observables, listeners don't have an unsubscribe method. The way to cleanup listeners is either remove them from the EventManager (eventManager.remove(listener);) or calling the returned method as mentioned above.Alible
What if I add listeners dynamically to multiple elements? I can't figure out how one would do that with your code example. For now, I have chosen to add the unlisten methods to an array. Then in the onDestroy() function I do: this.myUnlisteners.forEach(item,idx) => { this.myUnlisteners[idx](); });Epigeous
@Epigeous Sounds good to me. A small simplification could be to just use item immediately: this.myUnlisteners.forEach(item => item());Alible
Q
5

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.

Quire answered 26/2, 2018 at 9:34 Comment(3)
If you were adding a listener, why would you expect that the function returned by adding that listener would invoke that listener? That doesn't make much sense to me. The whole point of adding a listener is to respond to events which you can't necessarily trigger programatically. I think if you expected that function to invoke your listener, you might not be understanding listeners completely.Splenitis
@Quire mate this is really confusing, thanks for pointing this out!Blubberhead
It returns this so you can also remove the listener again when you destroy your component later on. When adding listeners it is considered good practice to remove them later when you don't need them anymore. So 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 clean up after yourself?Alible
S
0

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>
Seftton answered 26/6, 2018 at 18:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.