Angular Reactive Forms with Type

Sergio Wilson-Navarro
Apr 14 · 5 min read

The Challenge

This one is mainly part of an opinion. One that I have found to assist me in promptly interpreting and creating a structure easily to derived. I enjoy things that aid my System 1 — brain’s intuitive and unconscious thinking mode — syntactically and or organizationally. However, this is but one opinion, and perhaps Angular as a library community will come up with one by far more compelling.

I created a library that is centered around the idea of Reactive Forms, and to Reactive Form in a way that feels complete and integrated. I do not frame other ways to do forms or advocate for a specific one. I just happen to discuss the approach that I enjoy and brings me joy.

For those that poses familiarity of Reactive Forms, the below is a very familiar view.

this.formGroup = this.formBuilder.group({
username: [null, Validators.required],
password: [null, [Validators.required, Validators.minLength(6)]]
});

Great, we could parsed how our form(s) would look like on the other side, the template side. However, It feels incomplete that type safety is not integrated within the API experience. Forms are a very important, and in light of that importance type safety should be exercised and enforced.

The lack of type safety induces various challenges. However, I will not entertain or dwell on this point too far, but take the below can-do for instance:

this.formGroup.setValue({
username: 'user',
password: 'password',
message: 'Is this a formcontrol???'
});

It seems odd that as a principle the form group is not aware of the form controls that are part of its tree, and this lack of awareness is exteriorized across all the interactions within the form group as well.

An Alternative

As stated, I introduced a library to enforce types. The spirit is not to change Reactive Forms but to extended it in a way that feels complete, and brings about a better experience.

npm install reactive-forms-typed --save

And into the angular module

import { ReactiveFormsTypedModule } from 'reactive-forms-typed';

@NgModule ({....
imports: [...,
ReactiveFormsTypedModule,
…]
})

This would enabled us to utilized formbuilder but with some nuance — integrating type awareness declaratively . For instance, like so:

login.component.ts
login.form.ts

The result is a very different encounter

A common tree structure, I used in my projects when introducing this library looks like the following:

login-component
│ login.component.html
│ login.component.scss
| login.component.ts
| login.form.ts

Forms are very important to the project ecosystem, This is why I love giving their contract(s) their own space, own file. It ease my intuition, provides clarity and it serves me as visual aid for communicating expectation and how my models would be shaped as.

The code simply gain awareness across interactions with the API

Beyond Declaration

We could also introduce type awareness in other areas like in validator(s). For this endeavor, I have introduced a generic helper implementation with this library and this mean we could declare validators like so:

form: NgTypeFormGroup<IRegisterForm>;

constructor(
private formTypeBuilder: FormTypeBuilder
) {}

ngOnInit() {

this.form = this.formTypeBuilder.group<IRegisterForm>({
username: [null, [Validators.required, Validators.email]],
password: [null, [Validators.required, Validators.minLength(6)]],
confirmPasword: [null,
[(c: NgTypeFormControlValidator<string, IRegisterForm>) => {
if (c && c.parent && c.parent.value.password === c.value) {
return null;
}
return { notMatch: true };
}]
]
});
...

Beyond Types

Staying within the spirit of common things we do with forms. One of the action items we deal, declaratively, is the introduction of error states in form components.

<!-- in material angular  -->
<mat-error *ngIf="emailFormControl.hasError('email') && !emailFormControl.hasError('required')">
Please enter a valid email address
</mat-error>
<!-- in bootstrap -->
<small class="text-danger" *ngIf="emailFormControl.hasError('email') && !emailFormControl.hasError('required')">
Please enter a valid email address
</small>

However, the above is quite verbose and redundant, specially when introducing required error states for every single form control item. Needless to say, the markup could grow in size relative to the amount of validators, resulting in error states. It induces an scaling challenge.

Wouldn’t it be simple for the element to just know where to display the error message and end of the story? Thankfully, I have introduced a directive that could become useful for this endeavor

<form [formGroup]="form" >

<mat-form-field class="block">
<input matInput type="text" placeholder="Username" formControlName="username">
<mat-error formControlOnErrorItem="username"></mat-error>
</mat-form-field>
...

Note that in formControlOnErrorItem we specified the formControlName which is the form control of use.

Nonetheless, This is a post about Reactive Forms with types. In order to stay true to the precept of integrating types as first principle; I have introduced a helper that would enable the declaration of error states at the form group level, like so:

form: NgTypeFormGroup<IRegisterForm>;

constructor(
private formTypeBuilder: FormTypeBuilder
) {}

ngOnInit() {

this.form = this.formTypeBuilder.group<IRegisterForm>({
username: [null, [Validators.required, Validators.email]],
password: [null, [Validators.required, Validators.minLength(6)]],
confirmPasword: [null,
[(c: NgTypeFormControlValidator<string, IRegisterForm>) => {
if (c && c.parent && c.parent.value.password === c.value) {
return null;
}
return { notMatch: true };
}]
]
});

this.form.setFormErrors({
username: {
required: 'Username is required',
email: 'Username must be a valid email'
},
password: {
required: 'Password is required',
minlength: 'Password is invalid'
},
confirmPasword: {
notMatch: 'Password must match'
}
});
...

Experience the example here

Enjoy coding, and stay strongly typed my friends 😄

Geek Culture

Proud to geek out.

Sign up for Geek Culture Hits

By Geek Culture

Subscribe to receive top 10 most read stories of Geek Culture — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store