Implementing reusable and reactive forms in Angular

In this article we will learn about two ways to implement reactive and reusable forms that have the capability to be used as sub-forms and also be used as standalone forms.

Implementing reusable and reactive forms in Angular

In this article we will learn about two ways to implement reactive and reusable forms that have the capability to be used as sub-forms and also be used as standalone forms.

I assume you know what forms are and you have worked with Angular reactive forms.

We will look at two approaches to achieve this:

  1. Using the ControlContainer which enables use to pass a parent form down to it’s sub-forms which are implemented as components.
  2. And the @ViewChild which will help us get a class instance of the component which in this case will be a form component instance.

Angular provides two approaches to configure and implement forms, these are reactive forms and template-driven, in this article we will be referring to reactive forms.

The forms UI Demo

A YouTube video showing the UI of the form for this tutorial

As demonstrated on the above video, we have one big form with 2 sub-forms:

  • HeroComponent (Parent)
  • PowersComponent (Sub-form queried using the @ViewChild decorator)
  • HobbiesComponent (Sub-form implemented using the ControlContainer class)

Implementing a sub-form using the @ViewChild decorator

Let’s start off with the best approach that uses a decorator that enables the parent form (HeroComponent) to query the component class instance of our child/sub-form which is the PowersComponent, see code below:

HeroComponent - Parent form

// hero.component.html 

<form [formGroup]="heroForm">
  <nb-card>
    <nb-card-header>Hero</nb-card-header>
    <nb-card-body class="col">
      <input
        formControlName="heroName"
        type="text"
        nbInput
        placeholder="Hero name"
      />
      <input formControlName="aka" type="text" nbInput placeholder="AKA" />
    </nb-card-body>
  </nb-card>

  <nb-card>
    <nb-card-header>Super Power</nb-card-header>
    <nb-card-body class="col">
      <app-powers></app-powers>
    </nb-card-body>
  </nb-card>

  <nb-card>
    <nb-card-header>Hobbies</nb-card-header>
    <nb-card-body class="col">
      <app-hobbies
        [parentForm]="heroForm"
        [formGroup]="heroForm.get('hobbies')"
      ></app-hobbies>
    </nb-card-body>
  </nb-card>
  <button (click)="logFormData()" nbButton status="primary">Submit</button>
</form>
// hero.component.ts

import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { PowersComponent } from '../powers/powers.component';

@Component({
  selector: 'app-hero',
  templateUrl: './hero.component.html',
  styleUrls: ['./hero.component.scss']
})
export class HeroComponent implements OnInit {
  @ViewChild(PowersComponent, { static: true }) public powersComponent: PowersComponent;

  public heroForm: FormGroup;
  constructor(private formBuilder: FormBuilder) {
  }

  public ngOnInit(): void {
    this.heroForm = this.formBuilder.group({
      heroName: ['', Validators.required],
      aka: ['', Validators.required],
      powers: this.powersComponent.createFormGroup(),
      hobbies: this.formBuilder.group({
        favoriteHobby: ['', Validators.required]
      })
    })
  }

  public logFormData(): void {
    console.log(this.heroForm.value);
  }

}

As you can see in the HeroComponent’s template that the PowersComponent needs no further inputs

<nb-card>
  <nb-card-header>Super Power</nb-card-header>
    <nb-card-body class="col">    
       <app-powers></app-powers> // here  
    </nb-card-body>
</nb-card>

But if you can check the HeroComponent class, you will notice how we get the instance of the Powers sub-form component class and calling the createFormGroup public member function to return us the PowersComponent FormGroup configuration instance.

@ViewChild(PowersComponent, { static: true }) public powersComponent: PowersComponent;

Note how the @ViewChild options uses the static: true to resolve the component instance as soon as possible so that we have the sub-form instance of the PowersComponent class.

And check line 21 of the HeroComponent.ts file that creates the PowersComponent form.

powers: this.powersComponent.createFormGroup(),

That is it on how to create a reusable and reactive sub-form using the @ViewChild decorator, which I’m inclined to because I find it straightforward to implement and easier to maintain as the sub-form and the parent form are decoupled from one another and provides the below benefits:

  • The parent form needs not to know about the sub-form’s instance at all. All it needs is, for the sub-form component class to have a createFormGroup public member function that returns a FormGroup’s instance.
  • A change on the sub-form’s form configuration does not affect the parent form’s configuration or its template in any way.

Cool, so what about setting up the unit test for such a setup?

Easy-peasy, check the code below

Unit test for @ViewChild approach

// hero.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HobbiesStubComponent } from './component-stubs/hobbies-stub.component';

import { HeroComponent } from './hero.component';

describe('HeroComponent', () => {
  let component: HeroComponent;
  let fixture: ComponentFixture<HeroComponent>;
  const formBuilder: FormBuilder = new FormBuilder();
  const powersComponent = jasmine.createSpyObj('PowersComponent', ['createFormGroup']);

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [HeroComponent, HobbiesStubComponent],
      providers: [{ provide: FormBuilder, useValue: formBuilder }],
      imports: [FormsModule, ReactiveFormsModule]
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HeroComponent);
    component = fixture.componentInstance;
    component.powersComponent = powersComponent;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

If you check line 11 and 25 of the code above, you will notice how we create a fake PowersComponent with a fake function that will mock the createFormGroup function using Jasmine’s createSpyObj function.

const powersComponent = jasmine.createSpyObj('PowersComponent'['createFormGroup'];  // line 11
...
component.powersComponent = powersComponent; // line 25

Without the above in the HeroComponent spec file, you will get an error when running the tests that:

TypeError: Cannot read property 'createFormGroup' of undefined

Implementing a sub-form using the ControlContainer

This approach is the confusing one and also has a lot of work, but is also viable.

In the HeroComponent ‘s template, we have the markup of our other sub-form, the HobbiesComponent

<app-hobbies [parentForm]="heroForm [formGroup]="heroForm.get('hobbies')"
></app-hobbies>

As you can see this form markup uses the [formGroup] directive that comes with the ControlContainer, allowing the parent form to pass down sub-form to the sub-form itself, if needed.
Also note that the sub-form is expecting an input which should be the parent form, this also enables us to have access to the parent form within the sub-form if needed, see source of this sub-form below.

HobbiesComponent - Uses ControlContainer

// hobbies.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { ControlContainer, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-hobbies',
  templateUrl: './hobbies.component.html',
  styleUrls: ['./hobbies.component.scss']
})
export class HobbiesComponent implements OnInit {
  public hobbiesForm: FormGroup;
  @Input() parentForm: FormGroup;

  constructor(private controlContainer: ControlContainer) { }

  public ngOnInit(): void {
    this.hobbiesForm = this.controlContainer.control as FormGroup;
  }


  public logForms(): void {
    console.log('Hobbies form', this.hobbiesForm);
    console.log('Parent (Hero) form', this.parentForm);
  }

}

The unit test for this set up is as follows.

Create a basic stub of the HobbiesComponent that will be an entry in the declarations for the parent form TestBed.

Component stub

// hobbies-stub.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-hobbies',
  template: ''
})
export class HobbiesStubComponent {
}

That’s all we need in the parent form to set up the test for a sub-form using the ControlContainer.

And below is the spec for the HobbiesComponent.

HobbiesComponent tests

// hobbies.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ControlContainer, FormBuilder, Validators } from '@angular/forms';

import { HobbiesComponent } from './hobbies.component';

describe('HobbiesComponent', () => {
  let component: HobbiesComponent;
  let fixture: ComponentFixture<HobbiesComponent>;
  const formBuidler: FormBuilder = new FormBuilder();

  const hobbyForm = formBuidler.group({
    favoriteHobby: ['', Validators.required]
  })

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [HobbiesComponent],
      providers: [{ provide: ControlContainer, useValue: hobbyForm }],
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HobbiesComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

If you look at the source code above, you will see that we are creating a dummy FormGroup using a mock FormBuilder defined at line 9. We then use our dummy FormGroup as a token for the ControlContainer on line 18.

There are no apparent benefits of using the latter approach, but it works with some drawbacks in respect to maintainability.

  • If the sub-form changes, maybe a property name was renamed, the parent form will need to have a sub-form configuration that coincides with that change for the sub-form.
  • If the parent form changes on the sub-form configuration, the sub-form will need to know this, so to update the template e.g., formControlName value, or any other code in the sub-form that uses the the sub-form FromGroup’s instance.
  • Having to write stubs for all your sub-form components to avoid any warnings in the tests.
  • If you want to reuse this form in any other parent form, that form must have a configuration for this sub-form
hobbies: this.formBuilder.group({
  favoriteHobby: ['', Validators.required]
})

Summary

The first approach that uses the the @ViewChild decorator is the best and is entirely reusable and encapsulated and easy to maintain.

The latter approach works but has many drawbacks.
But at the end of the day, it is up to you, because its you thinking, your code.

I hope you enjoyed reading and that you learned something.
You can checkout the complete source code for this on GitHub.