Implementing reusable and reactive forms in Angular

Post Editor

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.

6 min read
post

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.

post
post
6 min read
6 min read

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.

Discuss with community

Share

About the author

author_image
author_image

About the author

Thabo Ambrose

About the author

author_image
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
4 March 20218 min read
Angular Universal: real app problems

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Angularpost
4 March 20218 min read
Angular Universal: real app problems

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Read more
AngularpostAngular Universal: real app problems

4 March 2021

8 min read

Angular Universal is an open-source project that extends the functionality of @angular/platform-server. The project makes server-side rendering possible in Angular. This article will discuss the issues and possible solutions we encountered while developing a real application with Angular Universal.

Read more
Angularpost
3 March 20215 min read
View State Selector  - Angular design pattern

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Angularpost
3 March 20215 min read
View State Selector  - Angular design pattern

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Read more
AngularpostView State Selector  - Angular design pattern

3 March 2021

5 min read

As a web developer you may have noticed a repetitive boiler plate code of displaying a loader while an asynchronous request is being processed, then switching to the main view or displaying an error. Personally, I noticed these repetitions both in my code and other developers I work with. And even worse than the repetitive code is the fact that there are no indications for missing state views (such as unhandled errors or a missing loader). <div *ngIf="data$ | async as data"> <ng-container *ng

Read more
RxJSpost
26 February 20213 min read
RxJS: Why memory leaks occur when using a Subject

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

RxJSpost
26 February 20213 min read
RxJS: Why memory leaks occur when using a Subject

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

Read more
RxJSpostRxJS: Why memory leaks occur when using a Subject

26 February 2021

3 min read

It's not uncommon to see the words 'unsubscribe', 'memory leaks', 'subject' in the same phrase when reading upon RxJS-related materials. In this article, we're going to tackle this fact and by the end of it you should gain a better insight as to why memory leaks occur.

Read more