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:
- Using the ControlContainer which enables us to pass a parent form down to it’s sub-forms which are implemented as components.
- And the @ViewChild which 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
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:
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
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 a 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.
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’s TestBed.
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.
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 your thinking, your code.
I hope you enjoyed reading and that you learned something.
You can checkout the complete source code for this on GitHub