'Splitting complex Reactive Form into sub components doesn't validate or link to parent

I have a massive form that i am trying to break down into child components to make it more reusable.

However I don't know how to link it all back to the parent efficiently. I've been doing some research and noticed people using FormGroupDirective, in conjuction with ControlValueAccessor but cannot pin down a working example to learn from.

Scenario is simple, I want to capture staff and their direct reports, and that can be 1..N levels deep.

Below is the simplified setup of the form and setup i got to so far to demonstrate what I am trying to do.

Before all of this was in a single page which is not good practice.

Department.TS

    export class DepartmentComponent implements OnInit {
      departmentForm: FormGroup;
      constructor(private fb: FormBuilder) {}
    
      ngOnInit(): void {
        this.departmentForm= this.fb.group({
          name: [],
          directReports: this.fb.array([]),
        });
      }
    
      get directReports(): FormArray {
        return this.departmentForm.controls['directReports'] as FormArray;
      }
    
      addStaff() {
        this.directReports.push(this.newStaff());
      }
    
      newStaff() {
        var staffMember = this.fb.group({
          name: [],
          directReports: [],
        });
    
        return staffMember ;
      }
    }

Department.HTML

    <ng-container [formGroup]="departmentForm">
       <mat-form-field>
          <mat-label>Name</mat-label>
          <input matInput formControlName="name" />
        </mat-form-field>
    
      <ng-container formArrayName="directReports">
        <app-staff *ngFor="let staff of directReports.controls" formGroupName="staff">
        </app-staff>
      </ng-container>
    
    </ng-container>
    
    <button (click)="addStaff()">Add Staff</button>
    
    <pre>
        {{ departmentForm.value | json }}
    </pre>

Child component

    export class StaffComponent implements OnInit {
          staffForm: FormGroup;
          constructor(private fb: FormBuilder) {}
        
          ngOnInit(): void {
            this.staffForm= this.fb.group({
              name: [],
              directReports: this.fb.array([]),
            });
          }
        
          get directReports(): FormArray {
            return this.staffForm.controls['directReports'] as FormArray;
          }
        
          addStaff() {
            this.directReports.push(this.newStaff());
          }
        
          newStaff() {
            var staffMember = this.fb.group({
              name: [],
              directReports: [],
            });
        
            return staffMember ;
          }
        }

Child HTML

    <ng-container [formGroup]="staffForm">
        <mat-form-field>
            <mat-label>Name</mat-label>
            <input matInput formControlName="name" />
        </mat-form-field>
    
        <ng-container formArrayName="directReports">
            <app-staff *ngFor="let staff of directReports.controls" formGroupName="staff">
            </app-staff>
        </ng-container>
            
    </ng-container>
    
    <button (click)="addStaff()">Add Staff</button>


Solution 1:[1]

The easer way is pass as @Input the FormGroup/FormArray

If your app-staff is like

    export class StaffComponent {
        staffForm:FormGroup
        @Input('staffForm') set _(value){
            this.staffForm=value as FormGroup //<--I change this line
        }
    }
    <div [formGroup]="staffForm">
       <input formControlName="name">
       <input formControlName="directReports">
    </div>

You can pass the "formGroup" in parent

    <ng-container *ngFor="let staff of directReports.controls;let i=index">
        <app-staff [staffForm]="directReports.at(i)">
        </app-staff>
    </ng-container>

See how you pass the "formGroup". In this way you define the whole formGroup "departmentForm" in parent. When you pass as @Input a complex object, you pas a "reference" so, any change in any property of the object is "reflected" in the object.

A stackblitz

Update if we want that our component can be deleted we can add in StaffComponent some like

  <button (click)="delete()">delete</button>

  delete()
  {
    const parent=this.staffForm.parent as FormArray;
    const index=parent.controls.findIndex(x=>x==this.staffForm)
    parent.removeAt(index)
  }

Or use an @Output if we want the "parent" was who really remove the element.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1