Angular Reactive Forms - All Needed Use Cases
In this post you will learn what are reactive forms inside Angular and how you can use them every single day with all possible cases.
The most important thing that you must know is that there are 2 different ways to work with forms inside Angular. We have reactive forms and template driven forms.
Most people start by using template forms because they think that they are easier. But actually Reactive forms are leveraging RxJS and this is exactly an Angular way.
If you want to write scalable forms I highly recommend you to look in the direction of reactive forms and not template forms
As you can see here I already prepared for us an empty html form with some css.
<form class="form">
<div class="field">
<label class="label">username</label>
<input type="text" class="input"/>
</div>
<div class="field">
<label class="label">email</label>
<input type="text" class="input"/>
</div>
<div class="field">
<label class="label">password</label>
<input type="text" class="input"/>
</div>
<div>
<button type="submit" class="button">Register</button>
</div>
</form>
This is just HTML. It doesn't have any Angular logic. We have here only inputs and labels. And here is CSS that we will use with our form.
.form {
width: 100%;
background: #fff;
padding: 30px;
max-width: 450px;
border-radius: 10px;
box-shadow: rgba(3, 3, 3, 0.1) 10px 0px 50px;
}
.field {
margin-bottom: 30px;
}
.label {
margin-bottom: 10px;
font-size: 15px;
font-weight: 600;
color: #777;
}
.input {
width: 100%;
padding: 25px 15px;
border: 0;
background: #f0f0f0;
border-radius: 5px;
font-size: 18px;
color: #555;
font-weight: 600;
}
.button {
background: #037ef3;
color: #fff;
font-weight: 600;
width: 100%;
border-radius: 5px;
cursor: pointer;
padding: 25px 15px;
border: 0;
font-size: 18px;
&:hover {
background: #0271da;
}
}
.invalid {
border: 1px solid red;
}
.error {
color: red;
}
Configuring forms
The first step that we must do in order to work with reactive forms is add a module to app.module.ts.
import { ReactiveFormsModule } from '@angular/forms';
...
@NgModule({
imports: [
...
ReactiveFormsModule,
],
})
export class AppModule {}
We don't need to install anything additionally because Angular forms are coming together with Angular.
Creating a form
Our next step is to create a form.
export class AppComponent implements OnInit {
registerForm = this.fb.group({
username: '',
password: '',
email: '',
});
constructor(private fb: FormBuilder) {}
Here we injected FormBuilder
in our component and define a registerForm
. Inside we specify all fields that we have with default values. But it is important to remember that in form typings we can still get string or null in any field. So we should check this later.
Now let's bind our form to HTML.
<form class="form" [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<div class="field">
<label class="label">username</label>
<input type="text" class="input" formControlName="username"/>
</div>
<div class="field">
<label class="label">email</label>
<input type="text" class="input" formControlName="email"/>
</div>
<div class="field">
<label class="label">password</label>
<input type="text" class="input" formControlName="password"/>
</div>
<div>
<button type="submit" class="button">Register</button>
</div>
</form>
To bind a form we added formGroup
attribute to our form as well as ngSubmit
method. To bind every input we add formControlName
attribute to every field.
Now we can add our onSubmit
function.
onSubmit(): void {
console.log(
'submitted form',
this.registerForm.value,
);
}
By accessing value we get a whole object with all fields of our form.
As you can see after filling our form and clicking submit we have access to all fields of the form. Which is exactly how we bind all these inputs to our component.
Client validation
The next step that we for sure need inside our form is client validation. With reactive forms we get it out of the box.
registerForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
});
Here we changed our form definition by specifying not only default value but also validators that we want to check. As we get validators together with Reactive forms it is a declarative way to specify what we want to check in every field.
Now in our submit function we can check if our form is valid.
onSubmit(): void {
console.log(
'submitted form',
this.registerForm.value,
this.registerForm.invalid
);
}
We have invalid
property inside our form which is true in onSubmit
when we didn't pass our validators.
Error fields
But now you for sure want to say that our validation doesn't make sense because we don't see error messages and fields in our template.
<input
type="text"
class="input"
formControlName="username"
[class.invalid]="
registerForm.get('username')?.invalid &&
(registerForm.get('username')?.dirty ||
registerForm.get('username')?.touched ||
isSubmitted)
"
/>
Here we added class invalid
when our username field didn't pass our validators and this field was already changed. If we don't add dirty
and touched
logic then our field will be always highlighted to red.
Also as you can see we used question mark because any of our field can be null.
As you can see our field is highlighted as invalid when we touched it.
Now let's copy the same logic to other fields.
<form class="form" [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<div class="field">
<label class="label">username</label>
<input
type="text"
class="input"
formControlName="username"
[class.invalid]="
registerForm.get('username')?.invalid &&
(registerForm.get('username')?.dirty ||
registerForm.get('username')?.touched)
"
/>
</div>
<div class="field">
<label class="label">email</label>
<input
type="text"
class="input"
formControlName="email"
[class.invalid]="
registerForm.get('email')?.invalid &&
(registerForm.get('email')?.dirty ||
registerForm.get('email')?.touched)
"
/>
</div>
<div class="field">
<label class="label">password</label>
<input
type="text"
class="input"
formControlName="password"
[class.invalid]="
registerForm.get('password')?.invalid &&
(registerForm.get('password')?.dirty ||
registerForm.get('password')?.touched)
"
/>
</div>
<div>
<button type="submit" class="button">Register</button>
</div>
</form>
As you can see the logic is exactly the same. I didn't try to optimize code by creating reusable function just to make it crystal clear but we can move this code to the function and just provide a field name inside.
Now all our fields are highlighted correctly.
But here is a problem. If we don't change our fields and just submit a form than nothing is highlighted. In order to fix that we can create isSubmitted
property.
export class AppComponent implements OnInit {
...
isSubmitted = false;
onSubmit(): void {
...
this.isSubmitted = true;
}
}
We simply set this property in true when we submit our form. And now we can add this logic to our validation.
<input
type="text"
class="input"
formControlName="username"
[class.invalid]="
registerForm.get('username')?.invalid &&
(registerForm.get('username')?.dirty ||
registerForm.get('username')?.touched ||
isSubmitted)
"
/>
Now if our form is submitted we always how validation.
Error messages
Now you might say "But I really want to show some error messages". Here is how we can do it.
<span
*ngIf="
registerForm.get('username')?.hasError('required') &&
(registerForm.get('username')?.dirty ||
registerForm.get('username')?.touched ||
isSubmitted)
"
class="error"
>This field is required.</span
>
We render a span for the required error if username fields can required
error and the field is touched or submitted. Which actually means that for every error we can render additional span.
As you can see our message is rendered if we have that specific error.
Bonus
Now I want to show you how React form can really shine. You need to remember that React forms are built on top of RxJS which means you can easily combine it with different streams or subscribe to changes.
Let's say that we implement a settings form of our current user to update some fields. For example you might want to send an API request when user changed his role.
First of all let's add roleId
to our form which we want to change.
registerForm = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
roleId: [1, Validators.required],
});
As you can see I set it is 1 because for the settings form you would have all fields of the user. Now let's define all possible roles.
roles = [
{ id: 1, title: 'developer' },
{ id: 2, title: 'qa' },
];
And let's render a select to change our role.
<select formControlName="roleId">
<option *ngFor="let role of roles" [ngValue]="role.id">
{{ role.title }}
</option>
</select>
We binded our select to roleId
and rendered a list of roles inside.
But now let's say that we want to be able to send an API request when we changed a role without submitting the whole form. With Reactive forms it is really easy to do.
ngOnInit(): void {
this.registerForm.get('roleId')?.valueChanges.subscribe((roleId) => {
console.log('SEND API REQUEST AND UPDATE ROLE', roleId);
});
}
By using valueChanges
we get access to the observable of the field and we can subscribe to it. This is how on change or our role we can send a separate request to our backend.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done