loading...
Cover image for Building recursively nested @angular reactive forms: a clean approach

Building recursively nested @angular reactive forms: a clean approach

julianobrasil profile image Juliano Updated on ・6 min read

The use case

Side Note: this post was inspired by this StackOverflow question

Let's talk about the below behavior:

Alt Text

If you look carefully at the above animated gif you'll notice 2 important things about the displayed form behavior:

  1. It's dynamically created
  2. It can be recursively nested indefinitely

A static image will help us to better understand the scenario:

Alt Text

I've highlighted some repeating parts on the form above. The red parts are very interesting, so take your time looking at them. Have you notice something with them? It's recursively nested. You can see that Group 2 has a Group 1 inside it and both of them are structurally identical. In fact, Group 2 and the nested Group 1 have the same elements: conditions and the same action buttons bar.

How would we build up a dynamically, recursively nested form like the one show in the figure above? What if we want to create and fill-up the form based on some data you load from a server? Keep reading and we'll get there in a minute.

The complete code discussed here is available at Stackblitz.com:

The pseudo-component-tree

We'll build the following structure (pseudo-component-tree => each label started with app represents an angular component):

app-group-control
    |
   app-action-buttons-bar
    |
   conditions:
     app-condition-form
     app-condition-form
     app-condition-form
     ...
    |
   groups:
     app-group-control
     app-group-control
     app-group-control
     ...

In the above pseudo component tree, app-group-control is an angular reactive form that contains an action bar and 2 FormArray controls: conditions and groups.

I want you to be a curious developer and notice that groups is a collection of app-group-control components. Imagine this: you're gonna a build a component: app-group-control. In its template, among other things, it has a collection of... itself. It's a composite component.

Let's dive a little in each part of this component.

Action bar

Alt Text

Inside app-group-control the first thing we'll find is this action bar (ActionButtonsBarComponent in Stackblitz demo) that allows us to:

  • Add a app-group-control to groups FormArray (let's call it a "nested group")
  • Add a condition to the conditions FormArray
  • When the user clicks on the delete group button, it will ask the component that hosts this instance of app-group-control to remove it

This is a very simple component with 3 @Ouput()-decorated properties:

  @Output()
  remove = new EventEmitter<void>();
  @Output()
  addGroup = new EventEmitter<void>();
  @Output()
  addCondition = new EventEmitter<void>();

Condition

Alt Text

The condition component (ConditionFormComponent in Stackblitz demo) is also a very simple component. But, it has an important thing you should notice: it implements the ControlValueAccessor interface.

The ControlValueAccessor interface is what makes it possible for you to turn your components into angular FormControls. And like any FormControl, so they can be part of forms, and receive the formControlName directive and be controlled by @angular/forms functions.

The ControlValueAccessor is also, what will make it possible for us to nest forms in the most possible simple way. I've seen a lot of people doing crazy things to nest forms, like passing the parent FormGroup to a child component in order to reuse it in the child component's form. Maybe it's a matter of style, but I'd strongly recommend anyone to just turn that child component (that contains the nested form) into a FormControl. By doing that you'll be flatting the form's structure. I'd suggest two Kara Erickson videos if you want to learn more cool stuff about angular forms. Finish reading this post and, after that, scroll your page back to check out these amazing videos.

The wrapper form component

Alt Text

Finally we have this wrapper component, that contains the form that wraps everything (GroupControlComponent in Stackblitz demo).

Basically, its template is:

<form [formGroup]="_form">
  <app-action-buttons-bar (remove)="remove.emit()"
                          (addGroup)="_addGroup()"
                          (addCondition)="_addCondition()">
  </app-action-buttons-bar>
  <ng-container formArrayName="conditions">
    <app-condition-form *ngFor="let c of _conditionsFormArray?.controls; index as j" 
                        (remove)="_deleteCondition(j)"
                        [formControlName]="j" 
                        [formLabel]="'Condition ' + (j+1)">
    </app-condition-form>
  </ng-container
  <ng-container formArrayName="groups">
    <app-group-control *ngFor="let s of _groupsFormArray?.controls; index as i"
                        (remove)="_deleteGroupFromArray(i)" 
                        [formControlName]="i"
                        [formLabel]="'Nested Group '+ (i + 1) + ':'">
    </app-group-control>
  </ng-container>
</form>

If you check the code in Stackblitz, you'll notice that <app-condition-form> has its own form inside. <app-group-control> also has its own form (we're actually looking at it in the code above. Thanks to ControlValueAccessor, nesting these two forms inside a parent form was a piece of cake. Take a look again... we're not nesting only two forms in a completely transparent way. Considering that conditions and groups are FormArrays, we're, in fact, nesting an indefinite number of forms inside the parent form, without the need of any hack, using a simple, clean and robust approach. Pretty cool, don't you think?

The recursion

Like if it wasn't awesome enough to nest an indefinite number of forms in the first nested level, we're nesting host component instances inside itself... for an indefinite number of nesting levels down. It deserves a huge OMG!!! I hope this can give you a measure of the power of angular component-based structure.

Create the form based on incoming data

As a part of the implementation of the ControlValueAccessor interface, we implemented the writeValue(...). It is called by angular whenever the FormControl.setValue() or FormControl.patchValue() is executed by the host form. And this is a perfect place to analyze the incoming data and create/remove/fill-up controls of our form according to the incoming data.

If you take a look at this modified stackblitz demo, you'll see how we can achieve this with two tiny snippets (both of them in the writeValue(...) functions of the GroupControlComponent):

writeValue(value: Record<string,any>) {
  ...
  // Here we create the `conditions` controls based on 
  //   existent `conditions` attribute in the value
  if (value && value.conditions && value.conditions.length) {
    this._conditionsFormArray.clear();
    value.conditions.forEach(c => this._addCondition());
  }
  ...
  // Here we create the `groups` controls based on 
  //   existent `groups` attribute in the value
  if (value && value.groups && value.groups.length) {
    this._groupsFormArray.clear();
    value.groups.forEach(g => this._addGroup());
  }
  ...
}

So, in the AppComponent (in the demo), when we call this._form.patchValue(data); in the buildFormFromData() method, our form is built automatically to accommodate data in the right fields. With ControlValueAccessor in place, this was pretty easy, wasn't it?

Alt Text

Final Considerations

If you try to build this same form without angular ControlValueAccessor, I can guarantee you painful headaches during all the process. For some reason, the ControlValueAcessor is one of the most underestimated angular features among developers. Not using it makes the overall process of building complex forms an overwhelming task.

Give my schematics library a try

BTW, I've built an schematics libray that generates the skeleton of a component with the ControlValueAccessor implementation for you. If you want to, fell free to try it in your projects (it's a devDependency, so it won't add any nano dependency to your project): https://www.npmjs.com/package/@julianobrasil/schematics-components

Credits: Cover image from https://undraw.co

Posted on by:

julianobrasil profile

Juliano

@julianobrasil

I'm a full-stack web developer, passionate about all kinds of technologies. Formerly bachelor and MSc in Electrical Engineering.

Discussion

markdown guide