Angular - 尝试在使用反应式表单和数组时不需要最后一个表单控件 - 使用添加和删除按钮

问题描述 投票:0回答:1

这不太管用。

我有一个日期对(开始)和(结束)的数组,最初加载三个,并且有两个按钮,添加和删除。添加按钮添加一对新的,但前提是最后一个表单组(日期对所在的组)有效。

这可行,但我希望能够:

在组件加载时使最后一个元素(第三个元素)可编辑,并从该控件中删除 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

我希望删除按钮能够执行相同/类似的操作。

angular angular-reactive-forms angular-dynamic-forms
1个回答
0
投票

进行了很多更改,但要点如下。

运行 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>

Stackblitz 演示

© www.soinside.com 2019 - 2024. All rights reserved.