1
votes

EDIT : I managed to get data passthrough and validation working perfectly, by rewriting the email-phone-input.component.ts to use ControlContainer, get the FormGroup from the parent, along with FormArray controls. Will update the repo with working code and answer the question.

I am trying to get validators working for Form Group containing objects sent from a child component with Form Arrays.

Current hierarchy - Individual email component, individual phone component (that utilises an external package), then an email + phone component containing FormArray for both email and phone, and then a master form with a single formcontrol that gets the data from the email + phone component.

I've got the data coming through all the way, but I can't figure out how to get the validators to the master form.

Link to stackblitz containing the code + demo. https://stackblitz.com/github/rushvora/nested-form-playground

Side note - Validators.required isn't working for the email input, when adding a new Form Control to the Form Array.

app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'nested-form-playground';
  form: FormGroup;
  emailsAndPhones: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      notifications: this.fb.group({
        emailsAndPhones: []
      })
    });
    this.emailsAndPhones = this.form.get('notifications.emailsAndPhones') as FormGroup;
  }

  submit() {
    console.log(this.form.valid, this.form.status);
  }
}

app.component.html

<div class="container-fluid mt-3">
  <div class="card">
    <form [formGroup]="form" (ngSubmit)="submit()">
      <div class="card-header bg-light">
        <h4>Nested Form Testing.</h4>
      </div>
      <div class="card-body" formGroupName="notifications">
        <app-email-phone-input formControlName="emailsAndPhones"></app-email-phone-input>
      </div>
      <div class="card-footer bg-light">
        <button>Submit</button>
      </div>
    </form>
  </div>
</div>

email-phone-input.component.ts

import { Component, OnInit, OnDestroy, forwardRef } from '@angular/core';
import {
  FormGroup, FormBuilder, FormArray, FormControl, ControlValueAccessor,
  Validator, NG_VALUE_ACCESSOR, NG_VALIDATORS, Validators, AbstractControl
} from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-email-phone-input',
  templateUrl: './email-phone-input.component.html',
  styleUrls: ['./email-phone-input.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => EmailPhoneInputComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => EmailPhoneInputComponent),
      multi: true
    }
  ]
})
export class EmailPhoneInputComponent implements OnInit, ControlValueAccessor, Validator, OnDestroy {
  emailsAndPhonesForm: FormGroup;
  emails: FormArray;
  phones: FormArray;
  private destroy$ = new Subject();

  constructor(private fb: FormBuilder) { }

  ngOnInit() {

    this.emailsAndPhonesForm = this.fb.group({
      emails: this.fb.array([]),
      phones: this.fb.array([])
    });
    this.emails = this.emailsAndPhonesForm.get('emails') as FormArray;
    this.phones = this.emailsAndPhonesForm.get('phones') as FormArray;
  }

  public onTouched: () => void = () => { };

  writeValue(val: any): void {
    val && this.emailsAndPhonesForm.setValue(val, { emitEvent: false });
  }

  registerOnChange(fn: any): void {
    this.emailsAndPhonesForm.valueChanges
      .pipe(takeUntil(this.destroy$))
      .subscribe(fn);
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  validate(_: AbstractControl) {
    let emailsValidity = {};
    let phonesValidity = {};
    this.emails.valid ? emailsValidity = null : { invalidForm: {valid: false, message: `Email is invalid.`}};
    this.phones.valid ? phonesValidity = null : { invalidForm: {valid: false, message: `Phone is invalid.`}};
    console.log('Emails and Phones Form in validate: ');
    console.log(this.emailsAndPhonesForm);
    console.log('Emails in validate: ');
    console.log(this.emails);
    console.log('Phones in validate: ');
    console.log(this.phones);
    if (emailsValidity && phonesValidity) {
      const combinedValidity = { invalidForm: {valid: false, message: `Email & phone are invalid.`}};
      return combinedValidity;
    } else if (emailsValidity) {
      return emailsValidity;
    } else if (phonesValidity) {
      return phonesValidity;
    } else {
      return null;
    }
  }

  addEmail() {
    this.emails.push(new FormControl('', Validators.email));
  }

  addPhone() {
    this.phones.push(new FormControl('', Validators.required));
    console.log('Emails and Phones Form in addPhone: ');
    console.log(this.emailsAndPhonesForm);
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

}

email-phone-input.component.html

<!-- Array of email inputs -->
<ng-container [formGroup]="emailsAndPhonesForm">
  <ng-container formArrayName="emails">
    <ng-container *ngFor="let email of emails.controls; index as i">
      <div class="row">
        <app-email-input [index]="i+1" [formControlName]="i"></app-email-input>
      </div>
    </ng-container>
    <button type="button" (click)="addEmail()">Another Email</button>
  </ng-container>
  <ng-container formArrayName="phones">
    <ng-container *ngFor="let phone of phones.controls; index as i">
      <div class="row">
        <div class="form-group">
          <label>Phone {{ i + 1 }}</label>
          <app-phone-input [formControlNameCustom]="i"></app-phone-input>
        </div>
      </div>
    </ng-container>
    <button type="button" (click)="addPhone()">Another Phone</button>
  </ng-container>
</ng-container>
<!-- Array of phone inputs -->
<div class="jumbotron">
  <div *ngFor="let email of emails.value">
    {{email?.email }}
  </div>
  <div *ngFor="let phone of phones.value">
    {{phone?.internationalNumber }}
  </div>
</div>
1

1 Answers

0
votes

I fixed the issue by using ControlContainer to get the parent form group from the parent/master component. In this manner, I was able to update the validity, set the validators in the child component for the parent form controls directly. The base components still need to be implemented using CVA, here the email input and the 3rd party phone package does use CVA to implement their controls, but the layers above them all the way to the top layer just need to use ControlContainer, in order to pass the master form group/controls between components.

Thanks to this answer for leading me to the solution - Create a reusable FormGroup

Updated code - https://github.com/rushvora/nested-form-playground