在一个服务中使用HostListener是否可行?或者如何在Angular服务中使用DOM事件?

41

我想创建一个服务,可以检测所有的键盘输入,根据可配置的映射将按键转换为操作,并公开可供各种元素绑定以对特定按键做出反应的可观察对象。

以下是我目前代码的简化版本,在组件中使用HostListener时它可以工作,但现在我已经将其移动到了一个服务中,即使它明确初始化,它也永远不会触发。在服务中无法像这样检测输入吗?

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

1
我猜这是不可能的。请改用window.addEventListener - yurzui
3个回答

36

似乎在服务中无法使用HostListener

更新

正如Stanislasdrg Reinstate Monica所写,有一种更加优雅且更加Angular的方式,使用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$));
  }
}

现场演示:https://stackblitz.com/edit/angular-so4?file=src%2Fapp%2Fmy-mouse.service.ts

OLD

您可以像@yurzui已经指出的那样使用旧的方式window.addEventListener

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 {}

我想这是我想要做的唯一方法。如果我想使用HostListener,我必须创建一个不可见的InputHandler组件,并让每个组件配置它以发出他们想要的事件。 - trelltron
我做了和这个一样的事情,但是使用了'load'事件来添加监听器,如果我通过路由导航到组件,它不会触发,但是如果我重新加载页面,它会触发??? - blueprintchris

14

HostListener只能添加到组件/指令中,如果要向服务添加侦听器,可以使用rxjs提供的fromEvent函数。

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

您可以通过将订阅逻辑移动到组件模板中来删除订阅逻辑,然后只需在服务中使用可观察对象。这将类似于以下内容:

@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){}
}

我更喜欢这个版本,而不是渲染器版本。 - Ken Hadden

12

注意:
当监听器不再需要时,需要手动停止监听以避免内存泄漏

原始回答:
还有一种方法是使用RendererFactory2Renderer2。 我正在使用这样的服务来监视闲置并相应地注销用户。 以下是部分代码:

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

}

通过将null传递给渲染器工厂this.rendererFactory2.createRenderer(null, null),您可以获得默认的DOM渲染器,因此可以监听窗口事件。


1
非常好用!太棒了,非常感谢您的帖子! - over.unity
运行得很顺利,但它不会停止监听这些事件。 - Junaid
@Junaid 是正确的,你需要取消订阅,正如“仅用于演示订阅此处”所暗示的那样。 - Standaa - Remember Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接