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

Angular 17如何为Form Array实现跨FormGroup的日期连续校验?

问题描述

现有表单代码已正常运行,需要新增校验规则:确保Form Array中第x个FormGroup的endDate不大于等于第x+1个FormGroup的startDate。当前已实现单个FormGroup内startDate不能晚于endDate的校验,不知如何实现跨FormGroup的校验逻辑。


当前实现代码

校验器(Validators)

// VALIDATORS
public startDateAfterEndDateMatcher: ValidatorFn =
  this.dateComparisonValidator(
    'startDate',
    'endDate',
    'startDateAfterEndDate',
    (date1: Date, date2: Date) => date1 && date2 && date1 > date2
  );

private dateComparisonValidator(
  fieldName1: string,
  fieldName2: string,
  errorName: string,
  condition: (value1: any, value2: any) => boolean
): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const field1Value = control.get(fieldName1)?.value;
    const field2Value = control.get(fieldName2)?.value;
    console.log('condition', condition(field1Value, field2Value));
    if (condition(field1Value, field2Value)) {
      const errors: ValidationErrors = {};
      errors[errorName] = true;
      return errors;
    }
    return null;
  };
}

表单结构(Form Structure)

private initFormGroup() {
  this.datesInfo = this.formBuilder.group({
    datesArray: this.formBuilder.array(
      (this.datesArray || []).map((_) =>
        this.formBuilder.group(
          {
            startDate: [
              '',
              {
                nonNullable: true,
                validators: [Validators.required],
              },
            ],
            endDate: [
              '',
              {
                validators: [],
              },
            ],
          },
          { validators: [this.startDateAfterEndDateMatcher] }
        )
      )
    ),
  });
}

错误状态匹配器(Error State Matcher)

// ERROR MATCHER
export class SingleErrorStateMatcher implements ErrorStateMatcher {
  private errorCode: string;
  public constructor(errorCode: string, private formGroup?: FormGroup) {
    this.errorCode = errorCode;
  }

  isErrorState(
    control: FormControl | null,
    formGroup: FormGroupDirective | NgForm | null
  ): boolean {
    let parentFormGroup = this.formGroup ?? formGroup;
    console.log('parentFormGroup', parentFormGroup);

    return (
      !!(parentFormGroup?.dirty || parentFormGroup?.touched) &&
      !!(parentFormGroup?.invalid && parentFormGroup?.hasError(this.errorCode))
    );
  }
}

初始化(Initialisation)

// create error state matchers
for (let i = 0; i < this.datesArray.length; i++) {
  this.startDateAfterEndDateMatchers.push(
    new SingleErrorStateMatcher(
      'startDateAfterEndDate',
      this.datesInfo.controls['datesArray'].get(`${i}`) as FormGroup
    )
  );
}

解决方案

要实现跨FormGroup的日期校验,需要给整个Form Array添加自定义校验器,并配套调整错误匹配逻辑。

1. 编写跨FormGroup的数组校验器

新增一个校验器,遍历Form Array中的每个相邻元素,检查前一个的endDate是否大于等于后一个的startDate:

private crossGroupDateValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const formArray = control as FormArray;
    const errors: ValidationErrors = {};

    // 遍历所有相邻的日期组
    for (let i = 0; i < formArray.length - 1; i++) {
      const currentGroup = formArray.at(i) as FormGroup;
      const nextGroup = formArray.at(i + 1) as FormGroup;

      const currentEndDate = currentGroup.get('endDate')?.value;
      const nextStartDate = nextGroup.get('startDate')?.value;

      // 校验条件:当前组endDate >= 下一组startDate时触发错误
      if (currentEndDate && nextStartDate && new Date(currentEndDate) >= new Date(nextStartDate)) {
        // 给对应的组添加错误标记
        currentGroup.setErrors({ ...currentGroup.errors, crossGroupDateConflict: true });
        nextGroup.setErrors({ ...nextGroup.errors, crossGroupDateConflict: true });
        errors['crossGroupDateConflict'] = true;
      } else {
        // 验证通过时清除对应错误
        if (currentGroup.hasError('crossGroupDateConflict')) {
          delete currentGroup.errors!['crossGroupDateConflict'];
          if (Object.keys(currentGroup.errors || {}).length === 0) {
            currentGroup.setErrors(null);
          }
        }
        if (nextGroup.hasError('crossGroupDateConflict')) {
          delete nextGroup.errors!['crossGroupDateConflict'];
          if (Object.keys(nextGroup.errors || {}).length === 0) {
            nextGroup.setErrors(null);
          }
        }
      }
    }

    return Object.keys(errors).length > 0 ? errors : null;
  };
}

2. 更新表单初始化,给Form Array添加校验器

修改initFormGroup方法,给datesArray添加上述跨组校验器:

private initFormGroup() {
  this.datesInfo = this.formBuilder.group({
    datesArray: this.formBuilder.array(
      (this.datesArray || []).map((_) =>
        this.formBuilder.group(
          {
            startDate: [
              '',
              {
                nonNullable: true,
                validators: [Validators.required],
              },
            ],
            endDate: [
              '',
              {
                validators: [],
              },
            ],
          },
          { validators: [this.startDateAfterEndDateMatcher] }
        )
      ),
      // 给整个Form Array添加跨组校验器
      { validators: [this.crossGroupDateValidator()] }
    ),
  });
}

3. 调整Error State Matcher支持跨组错误

修改SingleErrorStateMatcher,支持检测crossGroupDateConflict错误:

export class SingleErrorStateMatcher implements ErrorStateMatcher {
  private errorCodes: string[];
  public constructor(errorCodes: string[], private formGroup?: FormGroup) {
    this.errorCodes = errorCodes;
  }

  isErrorState(
    control: FormControl | null,
    formGroup: FormGroupDirective | NgForm | null
  ): boolean {
    let parentFormGroup = this.formGroup ?? formGroup;
    if (!parentFormGroup) return false;

    // 检查是否包含指定的任意错误码
    const hasError = this.errorCodes.some(code => parentFormGroup?.hasError(code));

    return (
      !!(parentFormGroup.dirty || parentFormGroup.touched) &&
      !!hasError
    );
  }
}

然后更新初始化错误匹配器的代码:

// create error state matchers
for (let i = 0; i < this.datesArray.length; i++) {
  this.startDateAfterEndDateMatchers.push(
    new SingleErrorStateMatcher(
      ['startDateAfterEndDate', 'crossGroupDateConflict'], // 同时检测两种错误
      this.datesInfo.controls['datesArray'].get(`${i}`) as FormGroup
    )
  );
}

4. 处理动态增删表单组的情况

如果后续需要动态添加/删除日期组,每次操作后需要重新触发校验,并更新错误匹配器:

// 示例:添加新的日期组
addDateGroup() {
  const newGroup = this.formBuilder.group(
    {
      startDate: ['', { nonNullable: true, validators: [Validators.required] }],
      endDate: [''],
    },
    { validators: [this.startDateAfterEndDateMatcher] }
  );
  (this.datesInfo.get('datesArray') as FormArray).push(newGroup);
  
  // 添加对应的错误匹配器
  this.startDateAfterEndDateMatchers.push(
    new SingleErrorStateMatcher(
      ['startDateAfterEndDate', 'crossGroupDateConflict'],
      newGroup
    )
  );
  
  // 手动触发一次校验
  (this.datesInfo.get('datesArray') as FormArray).updateValueAndValidity({ emitEvent: true });
}

// 示例:删除指定索引的日期组
removeDateGroup(index: number) {
  (this.datesInfo.get('datesArray') as FormArray).removeAt(index);
  this.startDateAfterEndDateMatchers.splice(index, 1);
  
  // 手动触发一次校验
  (this.datesInfo.get('datesArray') as FormArray).updateValueAndValidity({ emitEvent: true });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 21:47:11