You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular服务中监听Esc按键无效,如何实现该功能?

问题

我在用于对话框的HomeDialogService服务中添加了@HostListener('document:keydown.escape')来监听Esc按键,并将该服务注入到SdTicketComponent组件中使用。但按下Esc键时控制台无任何输出,若将该监听代码移至组件中则能正常工作。请问如何让服务中的Esc按键监听生效?

相关代码

HomeDialogService

@Injectable()
export class HomeDialogService {
  public constructor(private readonly dialogService: DialogService, private readonly userAgent: UserAgentService) {}

  @HostListener('document:keydown.escape', ['$event']) onKeydownHandler(event: KeyboardEvent) {
    console.log(event);
  }

  public open<TComponentParams, TResult>(
    content: Type<DialogBaseComponent<TComponentParams, TResult>>,
    params?: TComponentParams,
    dialogOptions?: HomeDialogSettings | undefined,
  ): { dialogRef: DialogRef; result$: Observable<IDialogResult<TResult>> } {
    const desktopDialogWidth = dialogOptions.kendoDialogSettings.width ?? DESKTOP_DEFAULT_DIALOG_WIDTH;
    const desktopDialogMaxHeight = dialogOptions.kendoDialogSettings.maxHeight ?? DESKTOP_MAX_POPUP_HEIGHT;

    const isMobile = this.userAgent.mobile;

    const dialogRef = this.dialogService.open({
      content,
      ...dialogOptions.kendoDialogSettings,
      width: isMobile ? WIDTH_FULL : desktopDialogWidth,
      maxHeight: isMobile ? MOBILE_MAX_POPUP_HEIGHT : desktopDialogMaxHeight,
    });

    const component = dialogRef.content.instance as DialogBaseComponent<TComponentParams, TResult>;

    if (params) {
      component.params = params;
    }

    if (isMobile) {
      this.configureDialogStylesForNonDesktop(dialogRef);
    }

    const result$ = dialogRef.result.pipe(
      map((result: DialogCloseResult | TResult) => {
        if (result instanceof DialogCloseResult) {
          return {
            status: DialogResultStatus.Cancel,
          };
        }

        return { status: DialogResultStatus.Ok, data: result };
      }),
    );

    return {
      dialogRef,
      result$,
    };
  }
}

SdTicketComponent

export class SdTicketComponent {
  @Input()
  public ticket!: ServiceDeskTicket;

  constructor(
    public readonly userAgent: UserAgentService,
    public readonly startScreenSdTicketsSectionService: StartScreenSdTicketsSectionService,
    private readonly serviceDeskService: ServiceDeskService,
    private readonly dialogService: HomeDialogService,
    private readonly alertService: AlertService,
  ) {}

  public onTicketClick(): void {
    this.dialogService
      .open(
        SdTicketPopupComponent,
        { ticket: this.ticket },
        { kendoDialogSettings: { title: this.ticket.summary, width: 880, cssClass: 'sd-ticket-popup-wrapper' } },
      )
      .result$.pipe(
        filter(result => result.status === DialogResultStatus.Ok),
        map(result => result.data as IServiceDeskIssueRateData),
        switchMap(result => this.serviceDeskService.closeTicket(this.ticket, result)),
        switchMap(() => this.startScreenSdTicketsSectionService.initSectionData(true)),
        untilDestroyed(this),
      )
      .subscribe(_ => {
        this.alertService.showSuccess(`${this.ticket.key} has been closed`);
      });
  }
}
解决方案

@HostListener是Angular专为组件/指令设计的装饰器,用于绑定宿主元素的事件,但服务没有对应的宿主元素,所以这个装饰器在服务中完全不会生效。要在服务里实现全局Esc键监听,有两种实用方案:

方案一:手动注册/移除全局事件

在服务构造函数中直接给document添加事件监听,同时要在服务销毁时移除监听,避免内存泄漏:

@Injectable()
export class HomeDialogService {
  private escapeHandler: (event: KeyboardEvent) => void;

  constructor(private readonly dialogService: DialogService, private readonly userAgent: UserAgentService) {
    // 提前绑定函数上下文,避免this指向问题
    this.escapeHandler = (event) => this.handleEscKey(event);
    document.addEventListener('keydown', this.escapeHandler);
  }

  private handleEscKey(event: KeyboardEvent) {
    if (event.key === 'Escape') {
      console.log(event);
      // 这里可以添加你的业务逻辑,比如关闭当前打开的对话框
    }
  }

  ngOnDestroy() {
    // 服务销毁时移除监听
    document.removeEventListener('keydown', this.escapeHandler);
  }

  // 原有open方法保持不变...
}

注意:如果服务是全局单例(比如在根模块providers中注册),ngOnDestroy可能不会触发,这种情况可以考虑在应用退出时移除监听,或者结合RxJS的生命周期管理方案。

方案二:用RxJS实现响应式监听

利用RxJS的fromEvent创建事件流,这种方式更符合Angular的响应式编程风格,也更容易管理订阅生命周期:

import { fromEvent, Subject, takeUntil, filter } from 'rxjs';

@Injectable()
export class HomeDialogService {
  private destroy$ = new Subject<void>();

  constructor(private readonly dialogService: DialogService, private readonly userAgent: UserAgentService) {
    fromEvent<KeyboardEvent>(document, 'keydown')
      .pipe(
        takeUntil(this.destroy$), // 服务销毁时停止监听
        filter(event => event.key === 'Escape') // 只过滤Esc键
      )
      .subscribe(event => {
        console.log(event);
        // 业务逻辑处理
      });
  }

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

  // 原有open方法保持不变...
}

进阶优化:仅在对话框打开时监听

如果你的需求是只有当对话框处于打开状态时才响应Esc键,可以在open方法中动态创建监听,对话框关闭时自动取消:

import { fromEvent, takeUntil, filter } from 'rxjs';

public open<TComponentParams, TResult>(...): { dialogRef: DialogRef; result$: Observable<IDialogResult<TResult>> } {
  const dialogRef = this.dialogService.open(...);

  // 监听Esc键,直到对话框关闭
  const escSubscription = fromEvent<KeyboardEvent>(document, 'keydown')
    .pipe(
      filter(event => event.key === 'Escape'),
      takeUntil(dialogRef.result) // 对话框关闭时自动停止订阅
    )
    .subscribe(() => {
      console.log('Esc pressed, closing dialog');
      dialogRef.close(); // 自动关闭对话框
    });

  // 可选:对话框关闭时手动取消订阅(takeUntil已经处理,这里是双重保障)
  dialogRef.result.subscribe(() => escSubscription.unsubscribe());

  // 原有其他逻辑保持不变...
}

内容的提问来源于stack exchange,提问作者Eugene Sukh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 01:07:50