Skip to content

Handling large reactive forms in Angular

Published at
Updated at
Reading time
6m

Update March 2021: I wrote a follow-up article about data handling in large reactive forms: Data Handling in Angular Reactive Forms.


In this article, I'll explain how you can improve large reactive forms in Angular by splitting them into multiple components. I assume that you're familiar with Angular and reactive forms. If not, please go through the official guide first.

What are reactive forms?

Angular has a feature called Reactive forms for handling forms:

Reactive forms provide a model-driven approach to handling form inputs whose values change over time.

Instead of manually binding to all of the inputs of your form, toggling error messages and disabling the submit button until all fields are valid, you can let reactive forms handle that for you.

You can create a FormGroup with form controls, arrays and nested groups and map them to the form inputs in your HTML template. Additionally, you can add validators, write custom validators and act on your form model (e.g. updating values).

Splitting forms into child components

Let's say you have a large reactive form. Handling all these things in one component may result in a very large and hard to maintain file. A better way is to split these form into multiple child components, all wrapped in a parent component. Let's see how this can be done.

Approach 1: ControlValueAccessor (CVA)

Angular Forms has a ControlValueAccessor interface (docs) that your child component could implement. This allows the parent component to use formControlName on your component as you do on input elements.

<!-- /src/app.component.html -->
<form [formGroup]="form">
  <!-- formControlName can be used in components that implement ControlValueAccessor -->
  <child-component formControlName="child"></child-component>
</form>

While this is clean approach it has some drawbacks:

  • Complexity: The API is a bit unusual compared to what you normally do when working with components (example: you need to implement a function that receives a callback function that your component needs to call when the input field was touched).

  • Resetting Form: If you need to reset your form, this does currently not work properly with the ControlValueAccessor. There is an open issue #15741 on GitHub. The issue was created over three years ago.

If you are happy with the ControlValueAccessor API and don't need to reset the form I recommend this approach. I won't go into details but if you are interested you can read more about the implementation in this article by Gugan Arumugam.

Approach 2: Provide parent FormGroup to child

The second approach as described in this Stack Overflow answer is a shared FormGroup between parent and child component. The parent component creates a form group and provides it to the child. The child can then extend this form with new controls or groups.

<!-- src/app/parent.component.html  -->
<child-component [form]="formGroup"></child-component>
/* src/app/child.component.ts */
export class ChildComponent implements OnInit {
  @Input()
  form: FormGroup; // Access the parent form group

  ngOnInit() {
    // Extend the parent form group
    this.form.addControl(...));
  }
}

This approach is much easier to implement and very flexible. What I don't like about it is that the child component directly manipulates an input value (the form group). A component should only read input values but not manipulate them.

Approach 3: Providing the child FormGroup to the parent

The third approach is similar to the previous one: we share a form group between the parent and child component. The difference here is that the child does not edit the parents form group. Instead, the child component creates its own form and provides it to the parent component.

How can the child component provide the form to its parent? One way to do this is using @ViewChild as described by Dave Bush. In his example the parent component uses ViewChild to get the instance of the child component, accesses the form from the child and adds it to its own form.

/* src/app/child.component.ts */
class ChildComponent {
  form = this.formBuilder.group({ childInput: "" });
}
/* src/app/parent.component.ts */
@ViewChild(ChildComponent) childComponent: ChildComponent;

ngAfterViewInit() {
  this.form.addControl('childForm', this.childComponent.form);
  this.childComponent.form.setParent(this.form);
}

I see two problems with this implementation:

  • If you have more than one child component you will need to repeat the code above for each child.

  • The parent component directly accesses the form property of the child component. This results in a strong coupling between these two components. The parent components knows how the form is stored and also assumes that the form is ready during view initialization. What happens if the child component is asynchronous (i.e. loads some data from the backend before the form is created)?

We can solve these problems by using events. Instead of accessing the child component directly, the child component creates its own form group (1), emits an event with the created form group (2) and the parent component listens to that event and adds the child form group to its own form (3).

Relation of parent and child component

This approach results in less code and the child component does not need to expose the form directly as a property. Let's see how we can implement that.

Implementation

Let's say we have a child component PersonalDataForm. The components creates a form group (line 6) and emits the formReady event (line 11):

/* src/app/personal-data-form/personal-data-form.component.ts */
export class PersonalDataFormComponent implements OnInit {
  @Output()
  formReady = new EventEmitter<FormGroup>();

  form: FormGroup = this.fb.group({ ...});

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.formReady.emit(this.form);
  }
}

The parent component subscribes to that event and adds the child form to its own form:

<!-- src/app.component.html -->
<form [formGroup]="form">
  <app-personal-data-form (formReady)="addChildForm('personal', $event)"></app-personal-data-form>
  <button type="submit" [disabled]="form.invalid">Submit</button>
</form>
/* src/app.component.ts */
export class AppComponent {
  form: FormGroup = this.fb.group({});

  constructor(private fb: FormBuilder) {}

  addChildForm(name: string, group: FormGroup) {
    this.form.addControl(name, group);
  }
}

The addChildForm accepts a name parameter which is used to add the group to the form. This allows us to reuse the method for all child components.

Here is a complete example with two child components:

Note on data handling and conversion

What we didn't cover is data handling. Let's say you read some data for your form from the backend. You may need to convert that data into a different format for displaying it in the form. When submitting the form you will need to convert the form values back into the original format.

With child forms and the approach we used this can be done very easily. The parent form can pass the backend data to the child component. The child is then responsible for converting the data and initializing the form. Whenever the form value changes, the child can convert the form values back to the original format and provide it to the parent by emitting an event.

Data flow between parent and child

When submitting the form, the parent component already has the final data object received in step 6. It does not need to read the value from the form again.

Want to read more about data handling? Read my follow-up article Data Handling in Angular Reactive Forms.

Wrap Up

Splitting forms into child components makes them easier to read and maintain.

We discovered different approaches for handling large reactive forms in Angular and implemented it by attaching the form group of a child component to the form of the parent component.

Refactoring existing large forms into separate components is relatively easy with this approach as child components just use reactive form groups and and no additional API (like ControlValueAccessor).