Search code examples
angularformsvalidationangular-forms

Angular 8 - How to prevent reactive form validation when dynamic form groups has been added?


I've created a reusable component for creating/editing an entity of my app. This component holds an reactive form which has some dynamic parts.

The entire form works as expected, but I encountered a problem with validation: Everytime I click on the button to add a dynamic form group to the form, the form is added showing the validation errors. As well, not only the dynamic part is validated, but also the rest of the form.

Seems that, adding the dynamic part, is executing the validation, like if the submit button was pressed, but it didn't.

What I want to achieve is to be able to add dynamic formGroups, without this action itself executes the form's validation.

Am I missing some point?

import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {AbstractControl, FormArray, FormControl, FormGroup, Validators} from '@angular/forms';
import {Plan} from '../shared/domain/plan.model';
import {Milestone} from '../shared/domain/milestone.model';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.scss']
})
export class FormComponent implements OnInit {

  @Input() plan?: Plan;

  planForm = new FormGroup({
    id: new FormControl(
      null,
      [
        Validators.required,
        Validators.minLength(4),
        Validators.pattern('^[a-zA-Z0-9_-]+$')
      ]
    ),
    milestones: new FormArray([])
  });

  constructor() {}

  ngOnInit() {
    this.fillForm();
  }

  fillForm() {
    this.planForm.get('id').setValue(this.plan ? this.plan.getId() : '');
    this.createMilestoneFormControls();

    if (this.plan) {
      this.planForm.get('id').disable();
    }
  }

  createMilestoneFormControls() {
    if (!this.plan) {
      this.addMilestoneToFormArray();
      return;
    }

    this.plan.getMilestones().forEach((milestone: Milestone) => {
      this.addMilestoneToFormArray(milestone);
    });
  }

  addMilestoneToFormArray(milestone?: Milestone) {
    const milestonesFormArray = this.milestonesFormArray;
    milestonesFormArray.push(new FormGroup({
      description: new FormControl(milestone ? milestone.getDescription() : ''),
      startDate: new FormControl(
        milestone ? milestone.getStartDate().toISOString().substring(0, 10) : '',
        [Validators.required]
      ),
      endDate: new FormControl(
        milestone ? milestone.getEndDate().toISOString().substring(0, 10) : '',
        [Validators.required]
      ),
    }));
  }

  get milestonesFormArray() {
    return this.planForm.get('milestones') as FormArray;
  }

  removeMilestoneFromFormArray(i: number) {
    this.milestonesFormArray.removeAt(i);
  }

  getValidationMessage(field: string, fault: string): string {
    return this.validationMessages[field][fault];
  }

  getDateInputValidationMessage(fieldName: string, control: AbstractControl): string {

    if (control.errors.matDatepickerMax) {
      return this.getValidationMessage(fieldName, 'matDatepickerMax');
    }

    if (control.errors.matDatepickerParse) {
      return this.getValidationMessage(fieldName, 'matDatepickerParse');
    }

    if (!control.value && control.errors.required) {
      return this.getValidationMessage(fieldName, 'required');
    }

    return '';
  }

}
<form [formGroup]="planForm" (keydown.enter)="$event.preventDefault()">
    <mat-form-field class="formInput" appearance="outline">
    <mat-label>Relationship identifier</mat-label>
    <input matInput formControlName="id" autocomplete="off" required>
    <mat-error *ngIf="planForm.get('id').invalid">
      <div *ngIf="planForm.get('id').errors.required">{{getValidationMessage('id', 'required')}}</div>
      <div *ngIf="planForm.get('id').errors.minlength">{{getValidationMessage('id', 'minlength')}}</div>
      <div *ngIf="planForm.get('id').errors.pattern">{{getValidationMessage('id', 'pattern')}}</div>
    </mat-error>
  </mat-form-field>
  <div formArrayName="milestones">
    <div *ngFor="let milestone of milestonesFormArray.controls; let i=index"
      [formGroupName]="i.toString()"
      class="milestoneFormBlock">

      <div class="mobile">Milestone #{{i+1}}</div>
      <hr class="milestoneDivider">
      <mat-form-field class="formInputMilestone" appearance="outline" style=" margin-bottom: -1.25em">
        <mat-label>Description</mat-label>
        <input matInput formControlName="description" autocomplete="off">
      </mat-form-field>

      <mat-form-field class="formInputMilestone" appearance="outline" style=" margin-bottom: -1.25em">
        <mat-label>Start Date</mat-label>
        <input
          matInput
          [matDatepicker]="milestoneStartDate"
          formControlName="startDate"
          autocomplete="off"
          required
          placeholder="mm/dd/yyyy"
          [max]="milestone.get('endDate').value">
        <mat-datepicker-toggle matSuffix [for]="milestoneStartDate"></mat-datepicker-toggle>
        <mat-datepicker #milestoneStartDate></mat-datepicker>
        <mat-error *ngIf="milestone.get('startDate').invalid">
          <div>{{getDateInputValidationMessage('startDate', milestone.get('startDate'))}}</div>
        </mat-error>
      </mat-form-field>

      <mat-form-field class="formInputMilestone" appearance="outline" style=" margin-bottom: -1.25em">
        <mat-label>End Date</mat-label>
        <input
          matInput
          [matDatepicker]="milestoneEndDate"
          formControlName="endDate"
          autocomplete="off"
          required
          placeholder="mm/dd/yyyy"
          [min]="milestone.get('startDate').value">
        <mat-datepicker-toggle matSuffix [for]="milestoneEndDate"></mat-datepicker-toggle>
        <mat-datepicker #milestoneEndDate></mat-datepicker>
        <mat-error *ngIf="milestone.get('endDate').invalid">
          <div>{{getDateInputValidationMessage('endDate', milestone.get('endDate'))}}</div>
        </mat-error>
      </mat-form-field>

      <button class="remove-milestone-btn" mat-raised-button color="warn" (click)="removeMilestoneFromFormArray(i)">
        <fa-icon [icon]="['fas', 'trash']"></fa-icon>
      </button>

    </div>
    <button class="add-milestone-btn" mat-fab color="primary" (click)="addMilestoneToFormArray()">
      <fa-icon [icon]="['fas', 'plus']"></fa-icon>
    </button>
  </div>

  <button class="submitBtn" (click)="onSubmit()" mat-raised-button color="primary">
    <div *ngIf="plan; then thenBlock else elseBlock"></div>
    <ng-template #thenBlock>Update relationship</ng-template>
    <ng-template #elseBlock>Create relationship</ng-template>
  </button>

</form>


Solution

  • A button is considered type='submit' if you do not specify this. Therefore the form indeed receives a submit event.

    Change buttons to <button type="button"> when submitting is not intended on the button click.