Aaron
Aaron

Reputation: 109

How to perform validation in reactive form with nested form arrays in table with angular?

Stackblitz link: https://stackblitz.com/edit/angular-ivy-bafyye?file=src/app/components/user-details/user-details.component.ts

I have created nested reactive form for user's car details as below:

user-details.component.ts

export interface User {
  name: string;
  car: Cars[];
}

export interface Cars {
  id: Number;
  company: CarCompany;
  model: CarModel;
  parts: CarPartName[];
  registrationAndBillingDate: RegistrationAndBillingDate[];
}

export interface RegistrationAndBillingDate {
  id: Number;
  registrationDate: Date;
  billingDate: Date;
}

export class CarCompany {
  id: number;
  name: string;
}

export class CarModel {
  id: number;
  name: string;
}

export class CarPartName {
  id: number;
  name: string;
}

@Component({
  selector: 'app-user-details',
  templateUrl: './user-details.component.html',
  styleUrls: ['./user-details.component.css'],
})
export class UserDetailsComponent implements OnInit {
  userDetailsForm: FormGroup;
  submitted = false;
  company: CarCompany[] = [
    { id: 1, name: 'Ford' },
    { id: 2, name: 'Ferrari' },
    { id: 3, name: 'Toyota' },
  ];
  model: CarModel[] = [
    { id: 1, name: 'SUV' },
    { id: 2, name: 'SEDAN' },
  ];
  partName: CarPartName[] = [
    { id: 1, name: 'WHEELS' },
    { id: 2, name: 'FILTERS' },
  ];
  registrationDate: Date;
  expiryDate: Date;
  userDetailsFormJson: any;

  constructor(private fb: FormBuilder) {
    this.userDetailsForm = new FormGroup({});
  }

  ngOnInit() {
    this.createUserDetailsForm();
  }

  createUserDetailsForm() {
    this.userDetailsForm = this.fb.group({
      name: [null, Validators.required],
      cars: this.fb.array([this.createCarsForm()]),
    });
  }

  //car form
  createCarsForm(): FormGroup {
    return this.fb.group({
      carCompany: this.createCarCompnayForm(),
      carModel: this.createCarModelForm(),
      carParts: this.fb.array([this.createCarPartsForm()]),
      carRegistartaionAndBillingDate: new FormArray([
        this.createRegistrationAndBillingDateForm(),
      ]),
    });
  }

  createCarCompnayForm(): FormGroup {
    return this.fb.group({
      id: new FormControl(null, [Validators.required]),
    });
  }

  createCarModelForm(): FormGroup {
    return this.fb.group({
      id: new FormControl(null, Validators.required),
    });
  }

  //form creation for car parts
  createCarPartsForm(): FormGroup {
    return this.fb.group({
      partName: this.createCarPartNameForm(),
      available: new FormControl(null),
    });
  }

  createCarPartNameForm(): FormGroup {
    return this.fb.group({
      id: new FormControl(null, [Validators.required]),
    });
  }

  //form creation for registration and billing cycle
  createRegistrationAndBillingDateForm() {
    return this.fb.group({
      registrationDate: new FormControl(null, Validators.required),
      billingDate: new FormControl(null, Validators.required),
    });
  }

  get form() {
    return this.userDetailsForm.controls;
  }

  get cars() {
    return this.userDetailsForm.get('cars') as FormArray;
  }

  addCars() {
    this.cars.push(this.createCarsForm());
  }

  removeCars(k: Required<number>) {
    this.cars.removeAt(k);
  }

  getRegistrationAndBillingDate(index) {
    return (<FormArray>(
      (<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
        'carRegistartaionAndBillingDate'
      )
    )).controls;
  }

  addRegistrationAndBillingDate(index) {
    (<FormArray>(
      (<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
        'carRegistartaionAndBillingDate'
      )
    )).push(this.createRegistrationAndBillingDateForm());
  }

  removeRegistrationAndBillingDate(index, j: Required<number>) {
    (<FormArray>(
      (<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
        'carRegistartaionAndBillingDate'
      )
    )).removeAt(j);
  }

  addParts(index) {
    (<FormArray>(
      (<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
        'carParts'
      )
    )).push(this.createCarPartsForm());
  }

  getPartsForm(index) {
    return (<FormArray>(
      (<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
        'carParts'
      )
    )).controls;
  }

  removeParts(index, l: Required<number>) {
    (<FormArray>(
      (<FormArray>this.userDetailsForm.get('cars')).controls[index].get(
        'carParts'
      )
    )).removeAt(l);
  }

  onSubmit() {
    this.submitted = true;
    this.userDetailsFormJson = this.userDetailsForm.getRawValue();
  }
}

user-details.component.html(ui portion)

<div>
  <div>
    <div>
      <div [formGroup]="userDetailsForm">
        <fieldset>
          <legend>User Details</legend>
          <div>
            <table>
              <tr></tr>
              <tr>
                <td>
                  <p>Name</p>
                </td>
                <td>
                  <input
                    size="35"
                    type="text"
                    formControlName="name"
                    style="width: min-content"
                    placeholder="enter user name"
                  />
                </td>
              </tr>
            </table>
          </div>
        </fieldset>

        <fieldset>
          <legend>Cars</legend>
          <button class="btn btn-outline-primary" (click)="addCars()">
            Add New Car
          </button>
          <ng-container formArrayName="cars">
            <div class="row mt-2">
              <div class="table-responsive">
                <table class="table-bordered table_car">
                  <thead>
                    <tr>
                      <th>Company</th>
                      <th>Model</th>
                      <th>Registration and Billing</th>
                      <th>Parts</th>
                      <th></th>
                    </tr>
                  </thead>
                  <tbody *ngFor="let o of cars.controls; let k = index">
                    <tr class="table_car-tr" [formGroupName]="k">
                      <td>
                        <div formGroupName="carCompany">
                          <select formControlName="id" required>
                            <option [ngValue]="null" disabled>
                              Select Car Company
                            </option>
                            <option
                              *ngFor="let comp of company"
                              [ngValue]="comp.id"
                            >
                              {{ comp.name }}
                            </option>
                          </select>
                        </div>
                      </td>

                      <td>
                        <div formGroupName="carModel">
                          <select formControlName="id" required>
                            <option [ngValue]="null" disabled>
                              Select Car Model
                            </option>
                            <option
                              *ngFor="let mod of model"
                              [ngValue]="mod.id"
                            >
                              {{ mod.name }}
                            </option>
                          </select>
                        </div>
                      </td>
                      <td>
                        <table
                          class="table-responsive exp"
                          style="display: block"
                          formArrayName="carRegistartaionAndBillingDate"
                        >
                          <thead>
                            <tr>
                              <th>Registration Date</th>
                              <th>Billing Date</th>
                              <th>
                                <button
                                  style="height: 24px"
                                  (click)="addRegistrationAndBillingDate(k)"
                                >
                                  +
                                </button>
                              </th>
                            </tr>
                          </thead>
                          <tbody
                            *ngFor="
                              let reg of getRegistrationAndBillingDate(k);
                              let j = index
                            "
                          >
                            <tr
                              class="registration_and_billing_date-table-tr"
                              [formGroupName]="j"
                            >
                              <td>
                                <input
                                  formControlName="registrationDate"
                                  type="date"
                                />
                              </td>
                              <td>
                                <input
                                  formControlName="billingDate"
                                  type="date"
                                />
                              </td>
                              <td>
                                <button
                                  style="height: 24px"
                                  (click)="
                                    removeRegistrationAndBillingDate(k, j)
                                  "
                                >
                                  x
                                </button>
                              </td>
                            </tr>
                          </tbody>
                        </table>
                      </td>

                      <td>
                        <table class="table table-sm" formArrayName="carParts">
                          <thead>
                            <tr>
                              <th>Part Name</th>
                              <th>Available</th>
                              <th>
                                <button
                                  style="height: 24px"
                                  (click)="addParts(k)"
                                >
                                  +
                                </button>
                              </th>
                            </tr>
                          </thead>

                          <tbody
                            *ngFor="let part of getPartsForm(k); let l = index"
                          >
                            <tr [formGroupName]="l">
                              <td>
                                <div formGroupName="partName">
                                  <select formControlName="id">
                                    <option [ngValue]="null" disabled>
                                      Select Part
                                    </option>
                                    <option
                                      *ngFor="let pName of partName"
                                      [ngValue]="pName.id"
                                    >
                                      {{ pName.name }}
                                    </option>
                                  </select>
                                </div>
                              </td>
                              <td>
                                <input
                                  type="checkbox"
                                  id="licensed"
                                  formControlName="available"
                                  (value)="(true)"
                                  selected="true"
                                />
                              </td>
                              <td>
                                <button
                                  style="height: 24px"
                                  (click)="removeParts(k, l)"
                                >
                                  x
                                </button>
                              </td>
                            </tr>
                          </tbody>
                        </table>
                      </td>

                      <td>
                        <button style="height: 24px" (click)="removeCars(k)">
                          x
                        </button>
                      </td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </div>
          </ng-container>
        </fieldset>
      </div>
      <div class="card-footer text-center">
        <button (click)="onSubmit()" class="btn btn-primary">Submit</button>
      </div>
    </div>
    <div class="card mt-2">
      <div class="card-header">Result</div>
      <div class="card-body">
        <code>
          <pre>
              {{ userDetailsFormJson | json }}
          </pre>
        </code>
      </div>
    </div>
  </div>
</div>

I have also included json output in the end of the form so that it could be easier to understand how the form data is structured.

JSON structure is as below:

 {
    "name": "User",
    "cars": [{
            "carCompany": {
                "id": 1
            },
            "carModel": {
                "id": 1
            },
            "carParts": [{
                "partName": {
                    "id": null
                },
                "available": null
            }],
            "carRegistartaionAndBillingDate": [{
                "registrationDate": null,
                "billingDate": null
            }]
        },
        {
            "carCompany": {
                "id": 1
            },
            "carModel": {
                "id": 1
            },
            "carParts": [{
                "partName": {
                    "id": null
                },
                "available": null
            }],
            "carRegistartaionAndBillingDate": [{
                "registrationDate": null,
                "billingDate": null
            }]
        }
    ]
  }

I want to have validation in a cars table for company and model such that one company can have one car model in a row. If same car have same model in next row as well then the row needs to show message saying duplicate value or something similar.

Example: If car table has company value of Ford and model value of SUV and if in second row again if car company value is Ford and model value is SUV, it needs to say duplicate.

Also I want to validate dates section as well where registration date should be smaller than billing date. If billing date is smaller than registration date needs to throw validation error. I have tried it within html form as below:

<div style="color: red;font-size: 10px;margin-left: 10px;text-align: center;"


*ngIf=" reg.controls.registrationDate.value >reg.controls.billingDate.value">
                       Invalid Billing Date
</div>

Is there better way of doing this? And also I want to have validation in table in Part column car table for Part Name column. If part names are repeated I want to have validation text thrown. For example: I have Wheels and Filter as parts name. If Wheels is already there and if user again selects wheels validation should be kicked in. I am not able to figure out how validation should be done properly. So any kind of solution or suggestion would be great.

Upvotes: 2

Views: 2281

Answers (1)

Arnaud Denoyelle
Arnaud Denoyelle

Reputation: 31225

Validations that imply several components can be handled with a custom validator, that I would place on the FormArray

A validator is a function that takes an AbstractControl (usually, a FormControl but here, it will be the FormArray) and returns a ValidationErrors if something goes wrong or null if everything is fine.

ValidationErrors is any key/value object you want so you can pass information about the constraint violation. For example, it can be :

{
  minLength: 2,
  maxLength: 20
}

You can get the error via formControl.errors / formArray.errors. Typically, you want to use the name of the validator as a key and some detail about the constraint violation as the value.

Here is a proposition of implementation :

  • map the FormArray to the IDs of the models
  • filter the null elements
  • map it to a Set (which inherently deletes the duplicates) and compare its size to the size of the array
  • if the size does not match, there are some duplicates, return a ValidationErrors
  duplicateCarValidator(control: AbstractControl): ValidationErrors {
    const modelIds = control.value
      .map((car) => car.carModel?.id)
      .filter((id) => id !== null && id !== undefined);
    if (new Set(modelIds).size !== modelIds.length) {
      return {
        duplicates: true,
      };
    } else {
      return null;
    }
  }

And add it like any validator :

cars: this.fb.array(
  [this.createCarsForm()],
  [this.duplicateCarValidator]
),

Here is a StackBlitz with some added console.log : here

For registration date and billing date, this is another validator that imply 2 fields so it can be handled by another custom validator.

You can decide to place it :

  • either on one of the 2 fields (registration/billing)
  • on the car FormGroup

The logic remains the same

Upvotes: 1

Related Questions