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

Angular中@Input与@Output结合*ngFor使用时的赋值异常问题

Angular中@Input与@Output结合*ngFor使用时的赋值异常问题

嘿,我最近在Angular开发中碰到了个头疼的小问题,想跟大伙唠唠。我写了个时间输入的自定义指令,然后在组件模板里用*ngFor根据字符串数组循环渲染这个指令。指令里用@Input接收初始时间值,@Output用来发射用户修改后的时间。平时操作都挺正常,但当我把指令发射回来的值回写到循环依赖的数组里时,怪事就发生了——数组明明改了,但页面上的输入框和列表都没跟着更新!

先给大伙看看我的代码细节:

组件TS文件(HomeComponent)

import {Component} from '@angular/core';

@Component({
  templateUrl:'./home.component.html',
  styleUrls:['./home.component.css']
})
export class HomeComponent{
  timeArray:string[] = ['01:25','00:00','10:10','00:00','00:00'];

  onTimeChanging(arrayIndex : number,time:string){
    // 问题就出在这一行的赋值操作
    this.timeArray[arrayIndex] = time;
  }
}

组件HTML模板

<div>
  <input [timeInput]="time" (changeTime)="onTimeChanging(arrayIndex,$event)" *ngFor="let time of timeArray; index as arrayIndex">
</div>
<hr>
<ol>
  <li *ngFor="let time of timeArray">{{time}}</li>
</ol>

时间输入指令TS文件

import { Directive,ElementRef,EventEmitter,HostListener,Input, Output } from "@angular/core";

@Directive({
  selector:'[timeInput]'
})
export class TimeInputDirective{
  @Input('timeInput') initialValue: string = '00:00';
  @Output() changeTime = new EventEmitter<string>();

  constructor(private elementRef:ElementRef<HTMLInputElement>){
    this.elementRef.nativeElement.className = 'timeInput';
  }

  ngAfterViewInit(){
    this.elementRef.nativeElement.value = this.initialValue
  }

  @HostListener('keydown', ['$event']) onKeydownHandler(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;
      }
      // 发射更新后的时间值
      this.changeTime.emit(this.elementRef.nativeElement.value);
    }
  }
}

问题根源分析

折腾了半天,我终于搞懂了问题出在哪:

  1. Angular变更检测的特性:Angular默认只会检测数组的引用变化,直接通过索引修改数组元素(this.timeArray[arrayIndex] = time)不会改变数组的引用,所以变更检测不会触发,*ngFor也就不会重新渲染组件和指令。
  2. 指令的@Input未响应更新:指令里只在ngAfterViewInit中给输入框赋值了初始值,后续@Input的initialValue变化时,没有同步更新输入框的value,导致输入框显示的还是旧值。

解决方案

针对这两个问题,我做了两处关键修改:

  1. 让Angular检测到数组变化
    不要直接修改数组索引,而是通过创建新数组的方式改变引用,Angular就能自动触发变更检测了。用扩展运算符实现起来很简单:

    onTimeChanging(arrayIndex : number,time:string){
      this.timeArray = [
        ...this.timeArray.slice(0, arrayIndex),
        time,
        ...this.timeArray.slice(arrayIndex + 1)
      ];
    }
    
  2. 让指令响应@Input的变化
    给指令的initialValue添加setter,这样每当输入值变化时,自动同步到输入框:

    private _initialValue: string = '00:00';
    @Input('timeInput') 
    set initialValue(value: string) {
      this._initialValue = value;
      // 确保元素初始化后再更新value
      if (this.elementRef.nativeElement) {
        this.elementRef.nativeElement.value = this._initialValue;
      }
    }
    get initialValue(): string {
      return this._initialValue;
    }
    

修改完这两处之后,输入框的修改能实时同步到数组,数组变化也能实时更新到输入框和页面列表,一切都正常工作啦!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:59:50