这不太管用。
我有一个日期对(开始)和(结束)的数组,最初加载三个,并且有两个按钮,添加和删除。添加按钮添加一对新的,但前提是最后一个表单组(日期对所在的组)有效。
这可行,但我希望能够:
在组件加载时使最后一个元素(第三个元素)可编辑,并从该控件中删除 validator.required 条件
然后,当我添加第四对时,我想将第三个结束日期设置为不可编辑,并添加 validator.required 条件,但第四个结束日期应该可以根据所需条件进行编辑。
基本上,最后结束日期应始终是可选的(不是必需的)并且可编辑。
我正在努力让 Angular 做到这一点,由于某种原因,最后一个结束日期元素始终是必需的并且永远不可编辑。
示例代码
protected addDatePair() {
const datesArray = this.datesInfo.get('datesArray') as FormArray;
if (datesArray.length > 0) {
const lastFormGroup = datesArray.at(datesArray.length - 1) as FormGroup;
if (lastFormGroup && lastFormGroup.valid) {
// add date to array in class
this.datesArray.push(9);
// create new end date control
const newEndDateControl = new FormControl(null, {
nonNullable: false,
validators: [],
});
newEndDateControl.reset();
const newFormGroup = this.datePairs();
// swap new control into new form group
newFormGroup.removeControl('endDate');
newFormGroup.addControl('startDate', newEndDateControl);
// add new form group to dates array
datesArray.push(newFormGroup);
// should probably only initialise new element, rather than all again
this.formGetters = this.initFormGetters(this.datesInfo);
// should probably initialise new error state matcher too
// need to add delay since,
// asynchronouse HTML may not have rendered the 4th element to DOM
// (a bit annoying!)
// I'd like to make the last End Date (i) modifiable and (ii) not required
setTimeout(() => {
const inputs = document.querySelectorAll(
'div[formArrayName="datesArray"] ' + 'input[id^="endDate"]'
);
const lastInput = inputs[inputs.length - 1] as HTMLInputElement;
if (lastInput) {
lastInput.removeAttribute('readonly');
lastInput.removeAttribute('required');
lastInput.removeAttribute('aria-required');
}
}, 1000);
} else {
console.log('last formGroup is not valid');
}
} else {
// TO IMPLEMENT
}
}
protected removeLastDatePair() {
// TO IMPLEMENT
}
这是我的堆栈闪电战: https://stackblitz.com/edit/stackblitz-starters-y2b7mc?file=src%2Fmain.ts
我希望删除按钮能够执行相同/类似的操作。
进行了很多更改,但要点如下。
运行 for 循环时,仅使用控件数组而不是数据数组。
我们可以通过运行 for 循环将大部分复杂的代码简化为更小的功能。
我们不应该在 HTML 中使用
required
,因为它们是为模板驱动的验证保留的,但我们在这里使用反应式表单。
请仔细阅读代码,如有疑问请告诉我。
请根据您的用例调整代码,我确信我没有完全实现您想要的!
TS
import { Component, DestroyRef, inject, Signal } from '@angular/core';
import { CommonModule, DatePipe, JsonPipe } from '@angular/common';
import {
AbstractControl,
FormArray,
FormBuilder,
FormControl,
FormGroupDirective,
FormGroup,
NgForm,
FormsModule,
ReactiveFormsModule,
ValidationErrors,
ValidatorFn,
Validators,
} from '@angular/forms';
import {
MatError,
MatFormField,
MatHint,
MatLabel,
MatSuffix,
} from '@angular/material/form-field';
import {
MatDatepicker,
MatDatepickerInput,
MatDatepickerToggle,
} from '@angular/material/datepicker';
import { MatInput } from '@angular/material/input';
import { MatOption } from '@angular/material/autocomplete';
import { MatSelect } from '@angular/material/select';
import {
DateAdapter,
ErrorStateMatcher,
MAT_DATE_FORMATS,
MAT_DATE_LOCALE,
} from '@angular/material/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import {
MomentDateAdapter,
MAT_MOMENT_DATE_ADAPTER_OPTIONS,
} from '@angular/material-moment-adapter';
import 'zone.js';
import { Subscription } from 'rxjs';
export const MY_FORMATS = {
parse: {
dateInput: 'MM/YYYY',
},
display: {
dateInput: 'MM/YYYY',
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'MMMM YYYY',
},
};
type FormGetters = {
startDate: FormControl<string>;
endDate: FormControl<string>;
};
@Component({
selector: 'app-root',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatFormField,
MatLabel,
MatDatepickerInput,
MatInput,
MatError,
MatDatepicker,
MatDatepickerToggle,
MatHint,
MatSuffix,
MatOption,
MatSelect,
JsonPipe,
],
providers: [
{
provide: DateAdapter,
useClass: MomentDateAdapter,
deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS],
},
{ provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
],
templateUrl: `main.html`,
})
export class App {
private formBuilder = inject(FormBuilder);
name = 'Angular';
/* data properties */
datesArray = [1, 2, 3];
/* form properties */
protected datesInfo: FormGroup = this.formBuilder.group({});
protected formGetters: FormGetters[] = [];
errorMatcher = new SingleErrorStateMatcher('startDateAfterEndDate');
/* error state matchers */
readonly startDateAfterEndDateMatchers: SingleErrorStateMatcher[] = [];
/* lifecycle hooks */
protected ngOnInit(): void {
// initialisation
this.initFormGroup();
this.formGetters = this.initFormGetters(this.datesInfo);
}
// INITIALISE FORM
private initFormGroup() {
this.datesInfo = this.formBuilder.group({
datesArray: this.formBuilder.array(
(this.datesArray || []).map((_, i) =>
this.datePairs(this.datesArray.length - 1 === i)
)
),
});
}
get datesArrayControls() {
return (<FormArray>this.datesInfo.get('datesArray'))
.controls as FormGroup[];
}
private datePairs(isLast: boolean): FormGroup {
return this.formBuilder.group({
startDate: [
null,
{
nonNullable: true,
validators: !isLast
? [Validators.required, this.startDateAfterEndDateMatcher]
: [],
},
],
endDate: [
null,
{
nonNullable: true,
validators: !isLast
? [Validators.required, this.startDateAfterEndDateMatcher]
: [],
},
],
});
}
protected addDatePair() {
const datesArray = this.datesInfo.get('datesArray') as FormArray;
(<FormGroup[]>datesArray.controls).forEach((formGroup: FormGroup) => {
const startDateCtrl = formGroup.get('startDate') as FormControl;
const endDateCtrl = formGroup.get('endDate') as FormControl;
startDateCtrl.clearValidators();
startDateCtrl.updateValueAndValidity();
endDateCtrl.clearValidators();
endDateCtrl.updateValueAndValidity();
endDateCtrl.disable();
startDateCtrl.disable();
});
datesArray.push(this.datePairs(true));
}
protected removeLastDatePair() {
const datesArray = this.datesInfo.get('datesArray') as FormArray;
datesArray.controls.splice(datesArray.controls.length - 1, 1);
(<FormGroup[]>datesArray.controls).forEach(
(formGroup: FormGroup, i: number) => {
const startDateCtrl = formGroup.get('startDate') as FormControl;
const endDateCtrl = formGroup.get('endDate') as FormControl;
if (datesArray.controls.length - 1 !== i) {
startDateCtrl.clearValidators();
startDateCtrl.updateValueAndValidity();
endDateCtrl.clearValidators();
endDateCtrl.updateValueAndValidity();
endDateCtrl.disable();
startDateCtrl.disable();
}
}
);
}
// FORM GETTER
private initFormGetters(form: FormGroup) {
const datesArray = this.datesInfo.get('datesArray') as FormArray;
const formGetters: FormGetters[] = [];
datesArray.controls.forEach((control: AbstractControl) => {
if (control instanceof FormGroup) {
const formGetter: FormGetters = {
startDate: control.get('startDate') as FormControl<string>,
endDate: control.get('endDate') as FormControl<string>,
};
formGetters.push(formGetter);
}
});
return formGetters;
}
// 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 field1: FormControl = control.parent?.get(
fieldName1
) as FormControl;
const field2: FormControl = control.parent?.get(
fieldName2
) as FormControl;
const field1Value = field1?.value;
const field2Value = field2?.value;
console.log('condition', condition(field1Value, field2Value));
if (condition(field1Value, field2Value)) {
const errors: ValidationErrors = {};
errors[errorName] = true;
return errors;
}
return null;
};
}
}
// 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 !!(control?.invalid && control?.touched);
}
}
bootstrapApplication(App, {
providers: [provideAnimations()],
});
HTML
<!-- Add Button -->
<button (click)="addDatePair()" [disabled]="datesInfo.invalid">
Add Date Pair
</button>
<!-- Delete Button -->
<button (click)="removeLastDatePair()" [disabled]="datesInfo.invalid">
Delete Date Pair
</button>
<!-- dates info -->
<form [formGroup]="datesInfo" class="form-group">
<!-- dates array -->
<div formArrayName="datesArray">
@for (date of datesArrayControls; track $index) {
<ng-container [formGroupName]="$index">
<!-- start date -->
<mat-form-field class="form-date">
<!-- label -->
<mat-label> Start Date </mat-label>
<!-- input -->
<input
matInput
[matDatepicker]="startDatePicker"
[errorStateMatcher]="errorMatcher"
formControlName="startDate"
autocomplete="off"
/>
<!-- hint -->
<mat-hint>DD/MM/YYYY</mat-hint>
<!-- picker -->
<mat-datepicker-toggle
matIconSuffix
[for]="startDatePicker"
[disabled]="false"
></mat-datepicker-toggle>
<mat-datepicker
#startDatePicker
[startAt]="date?.get('startDate')?.value"
></mat-datepicker>
<!-- errors -->
<mat-error
*ngIf="date?.get('startDate')?.invalid
&& (date?.get('startDate')?.dirty || date?.get('startDate')?.touched)"
>
@if(date?.get('startDate')?.errors?.['required']) { Start Date is
required. }
</mat-error>
<mat-error
*ngIf="date?.get('startDate')?.hasError('startDateAfterEndDate')"
>
Cannot be after End Date
</mat-error>
</mat-form-field>
<!-- end date -->
<mat-form-field class="form-date">
<!-- label -->
<mat-label> End Date </mat-label>
<!-- input -->
<input
matInput
[matDatepicker]="endDatePicker"
[errorStateMatcher]="errorMatcher"
formControlName="endDate"
autocomplete="off"
/>
<!-- hint -->
<mat-hint>DD/MM/YYYY</mat-hint>
<!-- picker -->
<mat-datepicker-toggle
matIconSuffix
[for]="endDatePicker"
[disabled]="false"
></mat-datepicker-toggle>
<mat-datepicker
#endDatePicker
[startAt]="date?.get('startDate')?.value"
></mat-datepicker>
<!-- errors -->
<mat-error
*ngIf="date?.get('endDate')?.invalid && (date?.get('endDate')?.dirty || date?.get('endDate')?.touched)"
>
@if (date?.get('endDate')?.errors?.['required']) { End Date is
required. }
</mat-error>
<mat-error
*ngIf="date?.get('endDate')?.hasError('startDateAfterEndDate')"
>
Cannot be before Start Date
</mat-error>
<mat-error
*ngIf="date?.get('endDate')?.hasError('endDateExceedsStartDate')"
>
End Date cannot exceeds any Start Date
</mat-error>
</mat-form-field>
</ng-container>
}
</div>
</form>