How to implement dynamic reactive sub-forms nested into one Parent Form component in Angular?

657 Views Asked by At

I have found this guide to implement sub-forms in Angular.

My parent form holding all the child forms looks like this:

export class ParentFormComponent implements OnInit {

    valueArray: Array<string> = [
        'value1',
        'value2',
        'value3',
    ];

    @ViewChild(ChildFormComponent, {static: true}) childFormComponent: ChildFormComponent;
    //more ViewChildren with childFormComponents
   
    parentForm: FormGroup = this.fb.group({});

    constructor(private fb: FormBuilder) {
    }

    ngOnInit(): void {
        this.parentForm= this.fb.group({
            childformgroup: this.childFormGroup.createGroup(this.valueArray),
            //more childFormGroups
        })
    }
}

This is the ChildFormComponent nested in the parent Form:

export class ChildFormComponent implements OnInit {

    valueArray: Array<string> = [];
    
    childForm: FormGroup = this.fb.group({});
    
    constructor(private fb: FormBuilder) {
    }

    updateValidation(arrayValueCheckbox: Checkbox) {
        //code to add or remove Validators for FormControls
    }

    createGroup(valueArray: Array<string>) {
        this.valueArray= valueArray;
        this.addFormControls(valueArray);
        return this.childForm;
    }

    //create some FormGroup Elements
    addFormControls(valueArray: Array<string>) {
        this.valueArray.forEach(arrayValue=> {
            this.childForm.addControl(arrayValue + 'Checkbox', new FormControl());
            this.childForm.addControl(arrayValue + 'Calendar', new FormControl({ value: '',
                disabled: true }));
            this.childForm.addControl(arrayValue + 'Textbox', new FormControl({ value: '',
                disabled: true }));
        });
    }

    ngOnInit(): void {
    }
}

The parent HTML:

<input type="checkbox" ... #checkbox>

<div class="fadein" [hidden]="!checkbox.checked">
    <childform-omponent></childform-omponent>
</div>

This method works quite well, but it has some flaws:

  1. The validation of the parent form will still be invalid if the childForm is invalid. For example: If the user does not check the checkbox and does not fill the required Formcontrols of the childForm

  2. The childForm component will be rendered, even if i don't need it. It's just hidden.

On the other hand, the changed values from the child input fields (textbox etc.) will still be present, if the parent checkbox will be unchecked and checked again.

Currently i am trying to find a solution using the ng-container. This would solve the second flaw mentioned before:

<input type="checkbox" ... #checkbox>

<ng-container *ngIf="!checkbox.checked">
    <childform-component></childform-component>
</ng-container>

Using the ng-container, the childform will not be loaded beforehand, so the createGroup function will thorw an error:

childformgroup: this.childFormGroup.createGroup(this.valueArray),

At this point i have no clue how to implement my dynamic nested forms with the ng-container and dynamically add the necessary formgroup to the parents form.

Questions:

  1. What do I have to change in my current implementation if I want to make use of the ng-container?

  2. How can I call the createGroup() function from my child components when it's not loaded yet?

  3. Whats the best approach to add and remove childFormGroups dynamically to my ParentForm?

    1. I want to keep the input values if the child has been destoryed (checkbox checked -> added values to input fields -> checkbox checked twice -> values should still exist)

    2. The Validation has to be dynamic based on the childformgroups.

To be honest: I have messed around with lifecycle hooks, References and some other stuff. Nothing did the trick.

1

There are 1 best solutions below

0
Nikola Salim On

(Could you please paste the full error message you got when attempting to create the child's formGroup? It might help debug the issue :) )

From my understanding, if you'd like to dynamically render the child component using ng-container + *ngIf, we need to make sure that the parent component has access to the ChildFormComponent by the time we call the child's createGroup() method.

So instead of instantiating all formGroups in the parent component's ngOnInit, I'd build only the parentForm first, and then dynamically add the childformgroup whenever the checkbox input is checked.

For that, you can make use of the (change) event (which will also help with change detection) to call a method that will handle toggling the form and dynamically adding the childformgroup to the parentForm.

So your parent component's template would look something like:

<input type="checkbox" #checkbox (change)="toggleChildForm()">

<ng-container *ngIf="showChildForm">
    <childform-component></childform-component>
</ng-container>

Now for the changes in your parent component's logic:

  • the first change I suggest is accessing the child component using the @ViewChildren decorator. It provides the changes Observable, which you can use to make sure that the child component is accessible;
  • then we can subscribe to it and dynamically add the childformgroup;

Here's how it'd look like:

 valueArray: Array<string> = [
    'value1',
    'value2',
    'value3',
  ];

  @ViewChildren(ChildComponent) childFormComponent: QueryList<ElementRef> | undefined;

  parentForm: FormGroup = this.fb.group({});
  showChildForm: boolean = false;

  constructor(private fb: FormBuilder) {
  }

  ngOnInit(): void {
  }
  
  toggleChildForm(){
    this.showChildForm = !this.showChildForm;
    if (this.showChildForm) {
      // dynamically add the child's formGroup
      this.childFormComponent?.changes
        .pipe(take(1)) // makes sure to subscribe only once
        .subscribe((component) => {
          this.parentForm.addControl('childformgroup', component.first.createGroup(this.valueArray))
          this.parentForm.updateValueAndValidity()
        });
    } else {
      // dynamically remove child's formGroup
      this.parentForm.removeControl('childformgroup')
      this.parentForm.updateValueAndValidity()
    }
  }

Hopefully this answers questions 1) and 2).

For 3.1) I believe you'd have to handle this manually; I suggest creating a property to store the current values in the parent component. Since we now have access to the child component's form, you can make use of the FormGroup's valueChanges Observable to get the current values. Then we can pass these values down as an additional argument to createGroup() and build the new form with the stored values.

3.2) since the parent component now handles creating and removing the childformgroup entirely, the validation can be set when creating the FormControl's, like:

this.childForm.addControl(arrayValue + 'Calendar', new FormControl({ value: '', disabled: true }, Validators.required));