Recently I starting playing around with Reactive Forms in Angular 9. One thing that immediately bugged me is that the controls within a form group are not strongly typed/referenced.
As an example, imagine that you create a FormGroup like this after injecting FormBuilder:
private buildForms() {
this.formGroup = this.formBuilder.group({
control1: new FormControl("", [Validators.required]),
control2: new FormControl("", [Validators.required])
});
}
And your Html may look like this:
<form class="main-form row" [formGroup]="formGroup">
<mat-form-field>
<mat-label>Control1</mat-label>
<input formControlName="control1" matInput placeholder="Enter control1 value">
<mat-error *ngIf="true && formGroup.get('control1').errors">
<div *ngIf="formGroup.get('control1').errors.required">Control1 is required</div>
</mat-error>
</mat-form-field>
</form>
There are lots of potential issues with this Html. The “formControlName” (property) could not exist in the FormGroup. And the worst part, imho, is that usage of “formGroup.get(‘control1’)” to view errors and any other properties on the control. In my own experience I find that this code is extremely fragile. Due to the way that Reactive Forms work, there aren’t a lot of options to deal with this fragility.
In your TS or Html, you could simply make assumptions about the existence of controls on the FormGroup – essentially treating the class type as “any:”
let control1 = this.formGroup.controls.control1;
let control2 = this.formGroup.controls.control2;
I don’t like this because it doesn’t really address the problem. My solution for this was to create a separate model with a few factory methods to get closer to a strongly typed/safe set of classes for my forms.
The factories themselves would be simple – create our FormControls and create our FormGroup based on the FormControls.
import { FormControl, Validators, FormGroup, FormBuilder } from "@angular/forms"
import { MyViewModel } from "./my-view.model";
// Factory to create FormControls for main form
interface IFormControlsFactory {
createFormControls(viewModel: MyViewModel): MainFormControls;
createFormGroup(formControls: MyFormControls): FormGroup;
}
// Factory to create FormGroup for main form using given MyFormControls
interface IFormGroupFactory {
createFormGroup(formControls: MyFormControls): FormGroup;
}
export class FormControlsFactory implements IFormControlsFactory {
createFormControls(viewModel: MyViewModel): MainFormControls {
let formControls = new MainFormControls({
control1: new FormControl(viewModel.prop1, [Validators.required]),
control2: new FormControl(viewModel.prop2, [Validators.required])
});
return formControls;
}
}
// Factory implementation
export class FormGroupFactory implements IFormGroupFactory {
constructor(private formBuilder: FormBuilder) { }
createFormGroup(formControls: MyFormControls): FormGroup {
let formGroup = this.formBuilder.group(formControls);
return formGroup;
}
}
And our simple model(s) would contain the FormControl defns with the premise that we have a compile-time aware safe reference to each control. We don’t rely on an “any” object – we have a strongly typed object.
// This is a simple model to provide strong-typing of our FormControls
export class MyFormControls {
control1: FormControl;
control2: FormControl;
// Helper to let us get the property name/key as a string
public controlName = (prop) =&gt; Object.keys(this).find(key => this[key] === prop);
public constructor(init?: Partial<MyFormControls>) {
Object.assign(this, init);
}
}
// Set of key properties
export class MyFormControlsKeys {
public static readonly control1 = "control1";
public static readonly control2 = "control2";
}
In our code, now, we can build our references to the FormControls / FormGroup pretty succinctly with the factories. Assuming a “formConrols” member is added to our component..
private buildForms() {
this.formControls = new FormControlsFactory().createFormControls(this.viewModel);
this.formGroup = new FormGroupFactory(this.formBuilder).createFormGroup(this.formControls);
While this approach doesn’t guarantee existence (yes, a property could still be null – unless we set defaults in the FormControls constructor), it provides us with a few benefits.
Our Form, FormGroup, and FormControl creation is consolidated. If you’re working with large forms, it removes a lot of that code and moves it to specific models. I personally like reducing the complexity and number of lines of code in my components. Moving all of this code to specific models/factories makes a lot of sense to me.
Another benefit is that it eliminates using string literals all over the place in your code. Back to the Html, we get rid of most string literals since we have member/property references to the controls. Our revamped Html, for example, would look something like this:
<form class="main-form row" [formGroup]="formGroup">
<mat-form-field>
<mat-label>Control1</mat-label>
<input [formControlName]="fromControls.controlName(formControls.control1)" matInput placeholder="Enter control1 value">
<mat-error *ngIf="formControls.control1.errors">
<div *ngIf="formControls.control1.errors.required">Control1 is required</div>
</mat-error>
</mat-form-field>
</form>
That’s it for now – I found this to be a worth while approach to share since it reduces the chances of running into issues within templates and such.