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
相关产品推荐
相关产品推荐

