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

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
  };
}

问题根源与修复方案

核心问题

  1. ControlValueAccessor实现逻辑混乱:组件内部通过dateChange直接调用writeValue,违反了值访问器的数据流规则;父类与子类的writeValue、value setter重复触发onChange,导致循环更新。
  2. 可能存在对象引用复用:如果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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 09:57:57