Angular中ngFor循环的自定义date-range组件ngModel同步异常问题
问题描述
基于Angular Material DatePicker开发了date-range输入组件,用*ngFor循环渲染多个组件,绑定[(ngModel)]到ranges[i].data,每个组件设置了唯一name。但选择其中一个组件的日期时,所有组件的日期都会同步变化。试过用ranges[index]代替模板变量range,也给name绑定了索引,问题依旧。
相关代码
父组件模板
<date-range *ngFor="let range of ranges; let i = index;" [(ngModel)]="ranges[i].data" [required]="true" name="range-{{i}}"></date-range>
date-range.component.html
<div class="{{classes}}"> <mat-form-field appearance="outline"> <mat-label>{{label}}</mat-label> <mat-date-range-input [formGroup]="range" [rangePicker]="picker" [disabled]="disabled"> <input matStartDate formControlName="start" [placeholder]="startPlaceholder || 'jj/mm/aaaa'" [required]="startRequired || required" [readonly]="startReadonly || readonly" [disabled]="startDisabled || disabled" (dateChange)="writeValue({start: $event.value, end: null})"> <input matEndDate formControlName="end" [placeholder]="endPlaceholder || 'jj/mm/aaaa'" [required]="endRequired || required" [readonly]="endReadonly || readonly" [disabled]="endDisabled || disabled" (dateChange)="writeValue({start: value.start, end: $event.value})"> </mat-date-range-input> <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle> <mat-date-range-picker #picker></mat-date-range-picker> <mat-hint *ngIf="hint" class="text-sm text-muted">{{hint}}</mat-hint> </mat-form-field> </div>
date-range.component.ts
type Data = { start: Date|null, end: Date|null } @Component({ selector: 'date-range', templateUrl: './date-range.component.html', styleUrls: ['../form-field.component.css'], providers: [MakeProvider(DateRangeComponent)] }) export class DateRangeComponent extends AbstractValueAccessor { range = new FormGroup({ start: new FormControl<Date | null>(null), end: new FormControl<Date | null>(null), }); @Input() label:string=""; /* 省略其他Input属性 */ override _value:any = { start: null, end: null } override get value() { return this._value || { start: null, end: null }; } @Input() override set value(v: any) { if (!v) { v = { start: null, end: null } } let change = false; if (!this.areEqual(v.start, this.value.start)) { this.range.controls.start.setValue(v.start); change = true; } if (!this.areEqual(v.end, this.value.end)) { this.range.controls.end.setValue(v.end); change = true; } if (change){ this._value = v; this.onChange(v); } } override writeValue(value: any): void { if (!value) return; this.range.controls.start.setValue(value.start); this.range.controls.end.setValue(value.end); super.writeValue(value) } areEqual(v1:any, v2:any):boolean { if (!v1) return !v2; if (!v2) return !v1; if (v1 instanceof Date && v2 instanceof Date) { if (isNaN(v1.valueOf()) || isNaN(v2.valueOf())) return false; v1.setHours(0,0,0,0); v2.setHours(0,0,0,0); return v1.getTime() === v2.getTime(); } return false; } }
value-accessor.ts
export abstract class AbstractValueAccessor implements ControlValueAccessor { _value: any = ''; get value(): any { return this._value; }; set value(v: any) { if (v !== this._value) { this._value = v; this.onChange(v); } } writeValue(value: any) { this._value = value; this.onChange(value); } onChange = (_:any) => {}; onTouched = () => {}; registerOnChange(fn: (_:any) => void):void { this.onChange = fn; } registerOnTouched(fn: () => void):void { this.onTouched = fn; } } export function MakeProvider(type : any){ return { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => type), multi: true }; }
问题根源与修复方案
核心问题
- ControlValueAccessor实现逻辑混乱:组件内部通过
dateChange直接调用writeValue,违反了值访问器的数据流规则;父类与子类的writeValue、valuesetter重复触发onChange,导致循环更新。 - 可能存在对象引用复用:如果
ranges数组初始化时用fill填充同一个data对象,会导致所有组件绑定同一引用,修改一个同步所有。
修复步骤
1. 修正组件内部数据流监听
移除模板中dateChange事件绑定,改用FormGroup的valueChanges监听内部值变化:
// 在date-range.component.ts中添加ngOnInit生命周期 ngOnInit(): void { this.range.valueChanges.subscribe((value) => { if (!this.areEqual(value.start, this._value.start) || !this.areEqual(value.end, this._value.end)) { this._value = {...value}; this.onChange(this._value); } }); }
修改后的date-range.component.html输入框:
<input matStartDate formControlName="start" [placeholder]="startPlaceholder || 'jj/mm/aaaa'" [required]="startRequired || required" [readonly]="startReadonly || readonly" [disabled]="startDisabled || disabled"> <input matEndDate formControlName="end" [placeholder]="endPlaceholder || 'jj/mm/aaaa'" [required]="endRequired || required" [readonly]="endReadonly || readonly" [disabled]="endDisabled || disabled">
2. 修复writeValue方法
子类writeValue无需调用父类方法,添加emitEvent: false避免触发valueChanges循环:
override writeValue(value: any): void { if (!value) { value = { start: null, end: null }; } if (!this.areEqual(value.start, this.range.controls.start.value)) { this.range.controls.start.setValue(value.start, { emitEvent: false }); } if (!this.areEqual(value.end, this.range.controls.end.value)) { this.range.controls.end.setValue(value.end, { emitEvent: false }); } this._value = {...value}; }
3. 修正value setter逻辑
外部设置值时无需触发onChange,仅更新内部状态:
@Input() override set value(v: any) { if (!v) { v = { start: null, end: null }; } let change = false; if (!this.areEqual(v.start, this._value.start)) { this.range.controls.start.setValue(v.start, { emitEvent: false }); change = true; } if (!this.areEqual(v.end, this._value.end)) { this.range.controls.end.setValue(v.end, { emitEvent: false }); change = true; } if (change) { this._value = {...v}; } }
4. 确保ranges数组元素独立
初始化时用map创建独立对象,避免引用复用:
// 错误写法(复用引用) // this.ranges = Array(3).fill({ data: { start: null, end: null } }); // 正确写法(每个元素都是新对象) this.ranges = Array(3).map(() => ({ data: { start: null, end: null } }));
5. 修正父类AbstractValueAccessor
父类writeValue不应触发onChange,仅更新内部值:
export abstract class AbstractValueAccessor implements ControlValueAccessor { _value: any = ''; get value(): any { return this._value; }; set value(v: any) { if (v !== this._value) { this._value = v; this.onChange(v); } } writeValue(value: any) { this._value = value; // 移除this.onChange(value); } onChange = (_:any) => {}; onTouched = () => {}; registerOnChange(fn: (_:any) => void):void { this.onChange = fn; } registerOnTouched(fn: () => void):void { this.onTouched = fn; } }
内容的提问来源于stack exchange,提问作者galalem
相关产品推荐
相关产品推荐

