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); } } }
问题根源分析
折腾了半天,我终于搞懂了问题出在哪:
- Angular变更检测的特性:Angular默认只会检测数组的引用变化,直接通过索引修改数组元素(
this.timeArray[arrayIndex] = time)不会改变数组的引用,所以变更检测不会触发,*ngFor也就不会重新渲染组件和指令。 - 指令的@Input未响应更新:指令里只在
ngAfterViewInit中给输入框赋值了初始值,后续@Input的initialValue变化时,没有同步更新输入框的value,导致输入框显示的还是旧值。
解决方案
针对这两个问题,我做了两处关键修改:
让Angular检测到数组变化
不要直接修改数组索引,而是通过创建新数组的方式改变引用,Angular就能自动触发变更检测了。用扩展运算符实现起来很简单:onTimeChanging(arrayIndex : number,time:string){ this.timeArray = [ ...this.timeArray.slice(0, arrayIndex), time, ...this.timeArray.slice(arrayIndex + 1) ]; }让指令响应@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
相关产品推荐
相关产品推荐

