Angular Reactive Forms is a feature provided by the Angular framework that allows you to create and manage complex forms reactively. Reactive forms provide a model-driven approach to handling form inputs whose values change over time.
Why use Reactive forms?
Model-Driven approach
With Reactive forms, you create a form model, controls, and validations in the TypeScript component and bind it to the template.
Declarative dynamic behavior
Once the form model is created and is wired with the template, any changes made to the form controls are automatically reflected in the form's state, without the need for explicit event handling.
Observables
Reactive Forms in Angular heavily utilize observables. Each form control is represented as an observable stream of values that you can operate on (using Rx.js operators).
Form Controls
Each form field is a form control that is part of the form group. You declare the controls within the component and set a default value and validation rules for each. Controls and validations can be dynamically added or removed as well.
Creating your first form
Step 1: Importing ReactiveFormsModule in your App Module:
@NgModule({
imports: [
// other imports...
ReactiveFormsModule
],
// other declarations and providers...
})
export class AppModule { }
If you're using Standalone components then you import the module into the imports array:
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent
Step 2: Setup a form using FormBuilder
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent implements OnInit {
myForm!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.myForm = this.fb.group({ // Form Group
username: [''], // Form Control
email: [''], // Form Control
password: [''], // Form Control
});
}
onSubmit() {
console.log('Form values: ', this.myForm.value);
}
}
Step 3 - Binding Form to the template
As we can see the form group contains a list of form controls. We're going to use the form group and the controls to bind each field in the HTML template.
<form [formGroup]="myForm" (ngSubmit)="onSubmit()">
<div>
<label>
Username:
<input formControlName="username" placeholder="YOUR PLACEHOLDER">
</label>
</div>
<div>
<label>
Email:
<input formControlName="email" placeholder="">
</label>
</div>
<div>
<label>
Password:
<input formControlName="password" placeholder="*******">
</label>
</div>
<button type="submit">Create</button>
</form>
Using (ngSubmit)="onSubmit()"
we'll call the onSubmit() function in the TypeScript every time we click the submit button.
Step 4 - Adding Validations
When creating new form controls you can set the starter (default) value to the control:
this.myForm = this.fb.group({
username: ['E.g. SuperUser'], // Form Control
...
});
Following the starter value, you can add a set of synchronous validations:
this.myForm = this.fb.group({
username: ['E.g. SuperUser', [Validators.required]],
email: ['', [Validators.required, Validators.email]],
password: [
'',
[
Validators.required,
Validators.minLength(3),
Validators.pattern('<REGEX>'),
],
],
});
There is a set of built-in validations:
- required
- valid email
- min/max length
- follow a specific pattern, etc.
But you can also create your custom validators
Additionally, you can add Async validators that validate data against APIs.
this.myForm = this.fb.group({
username: [
'',
[
/* Sync validators */
],
[
/* Async validators */
],
],
....
The typical use case of an async validator would be the search functionality. As you type your input into a search field, Angular will send data to the backend, check if there is a match, and return the response in real-time, e.g., check if a username exists while typing on the sign-up form.
Step 5 - Applying form validations in the HTML template
Now we tell Angular to display an error for each control when it is invalid.
<div>
<label>
Email:
<input formControlName="email" placeholder="">
<div
style="color: red;"
*ngIf="myForm.get('email')?.invalid">
Please provide a valid email address.
</div>
</label>
</div>
To keep errors from popping up as soon as the page loads, you can apply validations only when certain conditions are met:
- form control is invalid
- form control is clicked on (touched)
- form control has been typed on (dirty)
<div>
<label>
Email:
<input formControlName="email" placeholder="">
<div
style="color: red;"
*ngIf="myForm.get('email')?.invalid && (myForm.get('email')?.dirty || myForm.get('email')?.touched)">
Please provide a valid email address.
</div>
</label>
</div>
What I like to do is create getter functions for each control and wrap the validation logic:
get isInvalidEmail(): boolean {
return !!(
this.myForm.get('email')?.invalid &&
(this.myForm.get('email')?.dirty || this.myForm.get('email')?.touched)
);
}
And apply it in the template:
<div>
<label>
Email:
<input formControlName="email" placeholder="">
<div
style="color: red;"
*ngIf="isInvalidEmail">
Please provide a valid email address.
</div>
</label>
</div>
More Reactive form goodies
Inspect form
Read form value
this.myForm.value
Check form validity
this.myForm.valid // true / false
Inspect form interactions
this.myForm.status // INVALID / VALID
this.myForm.dirty // true / false
this.myForm.touched // true / false
this.myForm.pristine // true / false
this.myForm.untouched// true / false
Look up form controls
this.myForm.get('username') // Abstract Control
this.myForm.get('email') // Abstract Control
this.myForm.get('password') // Abstract Control
Alter Form Behavior
Clear form values
this.myForm.reset();
Clear errors
this.myForm.setErrors(null);
Mark as dirty
this.myForm.markAsDirty();
Mark as touched
this.myForm.markAsTouched();
Update validity
this.myForm.updateValueAndValidity();
As well as add/remove controls and validators.
Using the same methods for FormGroup, you can inspect or alter individual controls:
this.myForm.get('username')?.value;
this.myForm.get('username')?.valid;
this.myForm.get('username')?.touched;
this.myForm.get('username')?.dirty;
this.myForm.get('username')?.markAsDirty();
this.myForm.get('username')?.patchValue('New value');
this.myForm.get('username')?.setErrors(null);
this.myForm.get('username')?.hasError('required') // true / false
// and so on...
Use of Rx.js
Form Groups and Abstract Controls have a valueChanges
property that converts form value into an Observable stream.
this.myForm.get('password')?.valueChanges
.pipe(
/* Rx.js operators */
)
.subscribe((data: string) => {
console.log('data :>> ', data);
})
An example scenario would be to replace each character with an asterisk:
import { debounceTime, distinctUntilChanged, map } from 'rxjs';
const passwordControl = this.myForm.get('password');
passwordControl?.valueChanges
.pipe(
// replace all characters with asterisk
map(currentValue => currentValue.replace(/./g, '*'))
)
.subscribe((newValue: string) => {
// Observables are immutable, which is why we need to update form control with new value
// We're using { emitEvent: false } to prevent triggering valueChanges after patchValue
passwordControl.patchValue(newValue, { emitEvent: false })
})
// P.S. <input type="password" does this automatically
Or emit values to an Observable only after user has stopped typing:
this.myForm.get('searchText')?.valueChanges
.pipe(
// emit 500ms after user stopped typing
debounceTime(500),
// emit only when value changes
distinctUntilChanged()
)
.subscribe((data: string) => {
console.log('data :>> ', data);
})
If you prefer not to use Observables, you can always use getRawValue()
method.
const val = this.myForm.get('password')?.getRawValue();
console.log('val :>> ', val);
Next Chapter ➡️
That's all for today!
For everything else awesome, follow me on Dev.to and on Twitter to stay up to date with my content updates.
Top comments (0)