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

Angular自定义TimeInput指令多实例同步响应问题求助

Angular时间输入指令多实例同步问题解决方案

问题描述

我开发了一个用于时间输入的Angular自定义指令TimeInputDirective,单个使用时功能完全正常,但在同一页面中使用多个该指令实例时,对任意一个实例的操作都会同步作用于所有实例。推测是document:keydown级别的HostListener被共享,导致所有实例同时响应,现寻求让各实例独立工作的解决方案。

原指令代码

import { Directive, ElementRef, HostListener, Input } from "@angular/core";
@Directive({  
selector: '[timeInput]',
})
export class TimeInputDirective {
@Input('timeInput') initialValue: string | undefined;
constructor(private elementRef: ElementRef<HTMLInputElement>) {
this.elementRef.nativeElement.className = 'timeInput';
}
ngAfterViewInit() {
this.elementRef.nativeElement.value = this.initialValue ? this.initialValue : '00:00';  
}

@HostListener('document:keydown', ['$event']) onKeyupHandler(event: KeyboardEvent) {
if (event.key != 'Tab') {
  event.preventDefault();
  let value = this.elementRef.nativeElement.value;
  let valuePart;

  switch (event.key) {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < value.indexOf(':')) {
        valuePart = (value.substring(0, value.indexOf(':')) + event.key).substring(3, 1);
        valuePart = valuePart > '24' ? '0' + valuePart.substring(2, 1) : valuePart;

        this.elementRef.nativeElement.value = `${valuePart}:${value.substring(value.indexOf(':') + 1)}`;

        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      } else {
        valuePart = (value.substring(value.indexOf(':') + 1) + event.key).substring(3, 1);
        valuePart = valuePart > '59' ? '00' : valuePart;

        this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:${valuePart}`;

        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
    case 'ArrowUp':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < value.indexOf(':')) {
        valuePart = (+value.substring(0, value.indexOf(':')) + 1).toString();
        valuePart = ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1) > '24' ? '00' : ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1);

        this.elementRef.nativeElement.value = `${valuePart}:${value.substring(value.indexOf(':') + 1)}`;

        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      } else {
        valuePart = (+value.substring(value.indexOf(':') + 1) + 1).toString();

        if (valuePart <= '59') {
          this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:${('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1)}`;
        } else {
          var hourPart = Math.floor(+valuePart / 60) + (+value.substring(0, value.indexOf(':')));

          if (hourPart > 24) {
            this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:00`;
          } else {
            this.elementRef.nativeElement.value = `${('0' + hourPart).substring(hourPart.toString().length + 1, hourPart.toString().length - 1)}:${('0' + (+valuePart % 60)).substring((+valuePart % 60).toString().length + 1, (+valuePart % 60).toString().length - 1)}`
          }
        }

        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
    case 'ArrowDown':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < value.indexOf(':')) {
        valuePart = (+value.substring(0, value.indexOf(':')) - 1).toString();
        valuePart = ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1) < '00' ? '24' : ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1);

        this.elementRef.nativeElement.value = `${valuePart}:${value.substring(value.indexOf(':') + 1)}`;

        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      } else {
        valuePart = (+value.substring(value.indexOf(':') + 1) - 1).toString();
        valuePart = ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1) < '00' ? '59' : ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1);

        this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:${valuePart}`;

        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
    case 'ArrowLeft':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart > this.elementRef.nativeElement.value.indexOf(':')) {
        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      }
      break;
    case 'ArrowRight':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < this.elementRef.nativeElement.value.indexOf(':')) {
        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
    case 'Delete':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < value.indexOf(':')) {
        this.elementRef.nativeElement.value = `00:${value.substring(value.indexOf(':') + 1)}`;
        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      } else {
        this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:00`;
        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
  }
}


@HostListener('click') onClickHandler() {
if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < this.elementRef.nativeElement.value.indexOf(':')) {
  this.elementRef.nativeElement.selectionStart = 0;
  this.elementRef.nativeElement.selectionEnd = 2;
} else {
  this.elementRef.nativeElement.selectionStart = 3;
  this.elementRef.nativeElement.selectionEnd = 5;
}
}
}

解决方案

核心问题

你当前监听的是**document全局的keydown事件**,每个指令实例都会注册这个全局事件,导致任何键盘操作都会触发所有实例的事件处理逻辑,进而同步修改所有input的值。

修复步骤

  1. 将全局事件改为宿主元素事件:把@HostListener('document:keydown', ['$event'])修改为@HostListener('keydown', ['$event']),这样事件只会绑定到当前指令所在的input元素,只有当该input获得焦点时才会响应键盘事件。
  2. 可选优化:增加焦点判断:在事件处理函数开头,判断当前input是否处于焦点状态,进一步避免误触发。

修改后的完整代码

import { Directive, ElementRef, HostListener, Input } from "@angular/core";
@Directive({  
selector: '[timeInput]',
})
export class TimeInputDirective {
@Input('timeInput') initialValue: string | undefined;
constructor(private elementRef: ElementRef<HTMLInputElement>) {
this.elementRef.nativeElement.className = 'timeInput';
}
ngAfterViewInit() {
this.elementRef.nativeElement.value = this.initialValue ? this.initialValue : '00:00';  
}

// 修改1:去掉document前缀,监听宿主元素的keydown事件
@HostListener('keydown', ['$event']) onKeyupHandler(event: KeyboardEvent) {
  // 修改2:可选,判断当前元素是否聚焦,避免其他元素触发时执行逻辑
  if (document.activeElement !== this.elementRef.nativeElement) {
    return;
  }

if (event.key != 'Tab') {
  event.preventDefault();
  let value = this.elementRef.nativeElement.value;
  let valuePart;

  switch (event.key) {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < value.indexOf(':')) {
        valuePart = (value.substring(0, value.indexOf(':')) + event.key).substring(3, 1);
        valuePart = valuePart > '24' ? '0' + valuePart.substring(2, 1) : valuePart;

        this.elementRef.nativeElement.value = `${valuePart}:${value.substring(value.indexOf(':') + 1)}`;

        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      } else {
        valuePart = (value.substring(value.indexOf(':') + 1) + event.key).substring(3, 1);
        valuePart = valuePart > '59' ? '00' : valuePart;

        this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:${valuePart}`;

        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
    case 'ArrowUp':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < value.indexOf(':')) {
        valuePart = (+value.substring(0, value.indexOf(':')) + 1).toString();
        valuePart = ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1) > '24' ? '00' : ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1);

        this.elementRef.nativeElement.value = `${valuePart}:${value.substring(value.indexOf(':') + 1)}`;

        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      } else {
        valuePart = (+value.substring(value.indexOf(':') + 1) + 1).toString();

        if (valuePart <= '59') {
          this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:${('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1)}`;
        } else {
          var hourPart = Math.floor(+valuePart / 60) + (+value.substring(0, value.indexOf(':')));

          if (hourPart > 24) {
            this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:00`;
          } else {
            this.elementRef.nativeElement.value = `${('0' + hourPart).substring(hourPart.toString().length + 1, hourPart.toString().length - 1)}:${('0' + (+valuePart % 60)).substring((+valuePart % 60).toString().length + 1, (+valuePart % 60).toString().length - 1)}`
          }
        }

        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
    case 'ArrowDown':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < value.indexOf(':')) {
        valuePart = (+value.substring(0, value.indexOf(':')) - 1).toString();
        valuePart = ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1) < '00' ? '24' : ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1);

        this.elementRef.nativeElement.value = `${valuePart}:${value.substring(value.indexOf(':') + 1)}`;

        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      } else {
        valuePart = (+value.substring(value.indexOf(':') + 1) - 1).toString();
        valuePart = ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1) < '00' ? '59' : ('0' + valuePart).substring(valuePart.length + 1, valuePart.length - 1);

        this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:${valuePart}`;

        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
    case 'ArrowLeft':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart > this.elementRef.nativeElement.value.indexOf(':')) {
        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      }
      break;
    case 'ArrowRight':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < this.elementRef.nativeElement.value.indexOf(':')) {
        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
    case 'Delete':
      if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < value.indexOf(':')) {
        this.elementRef.nativeElement.value = `00:${value.substring(value.indexOf(':') + 1)}`;
        this.elementRef.nativeElement.selectionStart = 0;
        this.elementRef.nativeElement.selectionEnd = 2;
      } else {
        this.elementRef.nativeElement.value = `${value.substring(0, value.indexOf(':'))}:00`;
        this.elementRef.nativeElement.selectionStart = 3;
        this.elementRef.nativeElement.selectionEnd = 5;
      }
      break;
  }
}


@HostListener('click') onClickHandler() {
if (this.elementRef.nativeElement.selectionStart == null ? 0 : this.elementRef.nativeElement.selectionStart < this.elementRef.nativeElement.value.indexOf(':')) {
  this.elementRef.nativeElement.selectionStart = 0;
  this.elementRef.nativeElement.selectionEnd = 2;
} else {
  this.elementRef.nativeElement.selectionStart = 3;
  this.elementRef.nativeElement.selectionEnd = 5;
}
}
}

效果说明

修改后,每个TimeInputDirective实例的键盘事件仅绑定到自身对应的input元素,只有当该input获得焦点时,键盘操作才会触发对应的处理逻辑,多个实例之间完全独立,不会再出现同步修改的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 21:48:08