Is it possible to use HostListener in a Service? Or how to use DOM events in an Angular service?
Asked Answered
V

3

45

I want to create a service which detects all keyboard input, translates the key strokes into actions based on a configurable mapping, and exposes observables which various elements can bind to to react to specific key presses.

The following is a simplification of my code so far, it worked when HostListener was in a component, but now I've moved it into a service it never fires even though it is definitely initialised. Is it not possible to detect input like this in a service?

import { Injectable, HostListener } from '@angular/core';

import { Subject } from 'rxjs/Subject';

@Injectable()
export class InputService {

    @HostListener('window:keydown', ['$event'])
    keyboardInput(event: any) {
        console.log(event);
    }
}
Vendace answered 20/9, 2016 at 11:33 Comment(1)
I guess it is't not possible. Use window.addEventListener insteadPolymorphous
M
36

Seems like its not possible to use HostListener in a service.

UPDATE

like Stanislasdrg Reinstate Monica wrote, there's a more elegant and more angular way using the renderer..

@Injectable()
export class MyMouseService implements OnDestroy {
  private _destroy$ = new Subject();

  public onClick$: Observable<Event>;

  constructor(private rendererFactory2: RendererFactory2) {
    const renderer = this.rendererFactory2.createRenderer(null, null);

    this.createOnClickObservable(renderer);
  }

  ngOnDestroy() {
    this._destroy$.next();
    this._destroy$.complete();
  }

  private createOnClickObservable(renderer: Renderer2) {
    let removeClickEventListener: () => void;
    const createClickEventListener = (
      handler: (e: Event) => boolean | void
    ) => {
      removeClickEventListener = renderer.listen("document", "click", handler);
    };

    this.onClick$ = fromEventPattern<Event>(createClickEventListener, () =>
      removeClickEventListener()
    ).pipe(takeUntil(this._destroy$));
  }
}

live-demo: https://stackblitz.com/edit/angular-so4?file=src%2Fapp%2Fmy-mouse.service.ts

OLD

You could use the old way window.addEventListener like @yurzui pointed out already.

https://plnkr.co/edit/tc53cvQDfLHhaR68ilKr?p=preview

import {Component, NgModule, HostListener, Injectable} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

@Injectable()
export class MyService {

  constructor() {
    window.addEventListener('keydown', (event) => {
      console.dir(event);
    });
  }

}

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Hello {{name}}</h2>
    </div>
  `,
})
export class App {

  constructor(private _srvc: MyService) {
    this.name = 'Angular2'
  }
}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App ],
  providers: [MyService],
  bootstrap: [ App ]
})
export class AppModule {}
Maestoso answered 20/9, 2016 at 11:46 Comment(2)
I guess this is the only way to do what I want. If I want to use HostListener I'll have to create an invisible InputHandler component and let each component configure it to emit the events they want.Vendace
I did the same as this but used the 'load' event for addEventListener and it does not trigger if I navigate to the component using the router but DOES trigger if i reload the page???Slipcase
D
18

HostListener's can only be added to components/directives, so to add a listener to a service you could use the fromEvent function provided by rxjs.

import { fromEvent } from 'rxjs';

@Injectable()
export class InputService implements OnDestroy {
  // Watch for events on the window (or any other element).
  keyboardInput$ = fromEvent(window, 'keydown').pipe(
    tap(evt => console.log('event:', evt))
  )
  // Hold a reference to the subscription.
  keyboardSub?: Subscription;

  constructor() {
    // Subscribe to the property or use the async pipe.
    // Remember to unsubscribe when you are done if you don't use the async pipe (see other example).
    this.keyboardSub = this.keyboardInput$.subscribe();
  }

  ngOnDestroy() {
    // Destroy the subscription.
    this.keyboardSub?.unsubscribe();
  }
}

You could remove the subscription logic by moving that to the component template, then just have the observable in the service. That would then look something like this:

@Injectable()
export class InputService implements OnDestroy {
  // Watch for events on the window (or any other element).
  keyboardInput$ = fromEvent(window, 'keydown').pipe(
    tap(evt => console.log('event:', evt))
  )
}

@Component({
  selector: 'my-selector',
  providers: [InputService],
  template: `
    <ng-container *ngIf="keyboardInput$ | async">
      <!-- Your content -->
    </ng-container>
  `
})
export class ExampleComponent {
  keyboardInput$ = this.inputService.keyboardInput$;

  constructor(private readonly inputService: InputService){}
}
Davena answered 10/4, 2022 at 22:44 Comment(1)
I like this a lot better than the renderer version.Blanding
S
12

Caution:
Lookout for memory leaks as the listeners don't automatically stop listening.

Original answer:
There is an other way of doing so, by using RendererFactory2 and Renderer2. I am using such a service to monitor idleness and logout the user accordingly. Here is part of the code :

@Injectable()
export class IdleService {

  renderer: Renderer2;
  lastInteraction: Date = new Date();
  definedInactivityPeriod = 10000;

  constructor(
    private rendererFactory2: RendererFactory2,
    private auth: AuthService,
    private router: Router
  ) {
    this.renderer = this.rendererFactory2.createRenderer(null, null);
    this.renderer.listen('document', 'mousemove', (evt) => {
      console.log('mousemove');
      this.lastInteraction = new Date();
    });
    // Subscribing here for demo only
    this.idlePoll().subscribe();
  }

  idlePoll() {
    return interval(1000)
      .pipe(
        tap(() => console.log('here', new Date().getTime() - this.lastInteraction.getTime())),
        takeWhile(() => {
          if ((new Date().getTime() - this.lastInteraction.getTime()) > this.definedInactivityPeriod) {
            this.auth.logout();                        
          }
          return (new Date().getTime() - this.lastInteraction.getTime()) < this.definedInactivityPeriod;
        })
      );
  }

}

By passing null to renderer factory this.rendererFactory2.createRenderer(null, null) you get a hold of the default DOMrenderer and can therefore listen to window events.

Sarracenia answered 8/3, 2019 at 11:13 Comment(3)
works like a charm! Amazing, thank you very much for the post!Isosteric
Works like a charm BUT it doesn't stop listening to those events.Mocha
@Mocha Is right, you need to unsubscribe, as implied by "Subscribing here for demo only".Sarracenia

© 2022 - 2024 — McMap. All rights reserved.