Introduction
In AngularJS (first framework version), we had a great development experience to build our forms, but this experience involves a lot of code in our HTML template and a high coupling between this template and our controller classes (and sometimes, with services too). In Angular 2, this experience was maintained, the two-way binding, validations states features, and so on. A lot of features and concerns were improved in Angular 2, like communication between components, code-reuse, separation of concerns, unit tests and others. To promote some of those benefits in context of forms, Angular 2 brought another option for build forms with a 'model-driven' approach using a reactive technique of development.
Background
For better understanding, along with this article, I'll show a sign up registration form built with both approaches, and discuss about situations when one should be more beneficial than the other.
The form that will be built in both approaches is it:
This form has some required fields and a submit button which remains disabled while the form state is not valid. It will evolve along with this article in terms of user requirements and user experience to show the scenarios when Reactive Model will fit better than template-driven.
Template-driven' Form Code
To create a form using the template-driven approach, we can start with our form
HTML element as follows:
<form class="form-horizontal"
novalidate autocomplete="off"
#signupForm="ngForm">
...
</form>
Notice the signupForm
template variable which is a reference for the form itself (and its state, children elements and data) that will be used in this sample to improve the user experience by enabling the Submit button only when your state is valid:
<button class="btn btn-primary"
(click)="save()"
[disabled]="signupForm.invalid">
Submit
</button>
And with the @ViewChild
decorator, we can access it in our component as follows:
@ViewChild('signupForm') signupForm: NgForm;
...
save(): void {
if (this.signupForm.valid) {
...
}
}
An HTML template for email
input can be written like so:
<div class="form-group"
[ngClass]="{'has-error':
(email.touched || email.dirty) && email.invalid }">
<label class="col-md-2 control-label">Email *</label>
<div class="col-md-8">
<input class="form-control"
type="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+"
[(ngModel)]="user.email"
name="email"
#email="ngModel"/>
<span class="help-block"
*ngIf="(email.touched || email.dirty) && email.errors">
<span *ngIf="email.errors?.required">
Email is required.
</span>
<span *ngIf="email.errors?.pattern">
Email format is invalid
</span>
</span>
</div>
</div>
The code above is a common code related to work with forms inputs validation states and messages in Angular 2 at beginner level. In this sample, we show validation messages according to control´s status in runtime (when user is typing the value for the email
input and we update the user.email
model property simultaneously with the banana-in-the-box syntax [( )]
used to provide two-way-binding behavior.
That´s it for template-driven, so simple syntax, so easy to work with, but in a simple scenario and with a good (not great) user experience.
Building Blocks
Behind the scenes, Angular2 creates a model object called FormGroup
for us when working with template-driven approach, and its FormControl
children for each form input like illustrated below:
Speaking in terms of code, the Model-Driven approach (Reactive Forms) refers to strategy of building our own Model object (FormGroup
and related items) and bind it on Component' HTML. It's implied in a more concise HTML Template and provides more control in our forms (but with more code on it).
The same FormGroup
object which is automatically created and associated with the form can be created manually with ReactiveForms
in our Component as follows:
this.signupForm = new FormGroup({
firstName: new FormControl('', Validators.required),
lastName: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+')]),
password: new FormControl('', [Validators.required, Validators.minLength(6)]),
});
Note: With the FormBuilder
service, this task can be made more easy. To check it, you can access the source sample at the end of this article.
Model-driven' Form Code
To demonstrate this approach, we will recreate the same form, for this task, we need to start by importing the ReactiveFormsModule
(instead of FormsModule
)
from '@angular/forms'
in our Module
and in our Component
, add a signupForm
property of FormGroup
type and initialize it as was shown in the previous section.
In the HTML template, we need to associate this property with our form
element as follows:
<form class="form-horizontal"
novalidate autocomplete="off"
[formGroup]="signupForm">
...
</form>
To replicate the Submit
button behavior as was made on the previous sample with template-syntax, this can be made with the same syntax, but this time, the reference to signupForm
is not to a template variable, but is to a FormGroup
root object.
The same email template that was previously shown can be made as follows:
<div class="form-group"
[ngClass]="{'has-error': (signupForm.get('email').touched ||
signupForm.get('email').dirty)
&& signupForm.get('email').invalid }">
<label class="col-md-2 control-label">Email *</label>
<div class="col-md-8">
<input class="form-control"
type="email"
name="email"
formControlName="email"/>
<span class="help-block"
*ngIf="(signupForm.get('email').touched ||
signupForm.get('email').dirty)
&& signupForm.get('email').errors">
<span *ngIf="signupForm.get('email').errors?.required">
Email is required.
</span>
<span *ngIf="signupForm.get('email').errors?.pattern">
Email format is invalid
</span>
</span>
</div>
</div>
We removed the required
and pattern
validation attributes (both were moved for the component class), the email
template variable was replaced by formControlName
and the [(ngModel)]="user.email"
was removed (we do not have two-way binding anymore) and to access the value for the input in the save
method, e.g., we need to do something like this.signupForm.get('firstName').value
and to modify the form input via the component class, call the patchValue
method:
this.signupForm.patchValue({
email: 'richardleecba@gmail.com'
});
For more complex scenarios (business rules/reusable services), this can be a convenience in favor of immutability and better unit tested application...
When working with ReactiveForms
, even with more code, the difference between these two code strategies is not soo big, the main difference resides in the loose of two-way-binding and the transference of HTML code to validation rules and other logics to our Component (some of these cases) will be exemplified later on in this article), doing it we have a Component better testable, a leverage of separation of concerns with HTML template with less logic code possible and more control over our forms, and with more control, more improvements can be made, and this is the point of the next section.
In the github repository, the final source code for both approaches is:
<div class="form-group"
[ngClass]="{'has-error':
(email.touched || email.dirty) && email.invalid }">
<label class="col-md-2 control-label">Email *</label>
<div class="col-md-8">
<input class="form-control"
type="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+"
[(ngModel)]="user.email"
name="email"
#email="ngModel"/>
<span class="help-block"
*ngIf="(email.touched || email.dirty) && email.errors">
<span *ngIf="email.errors?.required">
Email is required.
</span>
<span *ngIf="email.errors?.pattern">
Email format is invalid
</span>
</span>
</div>
</div>
<div class="form-group"
[ngClass]="{'has-error': emailCurrentErrorMessage }">
<label class="col-md-2 control-label">Email *</label>
<div class="col-md-8">
<input class="form-control"
type="email"
name="email"
formControlName="email"/>
<span class="help-block"
*ngIf="emailCurrentErrorMessage">
{{emailCurrentErrorMessage}}
</span>
</div>
</div>
Give Me More!
This sample form has a good user experience, but it does not mean that it can't be improved or even with a good UX in general, some input interaction with the user can be very boring, in this sample, the email input, e.g., while the user is typing the email value, the message of 'Email format is invalid' is shown to user, but while the user is typing of course, that it's invalid! To improve this, we could evaluate the value which the user typed after he finished typing.
To do this, we need to watch the user inputs and react to this input values changes, for this task, the FormControl
object has a valueChanges
property which returns an Observable
that emits an event every time the value of the control changes, we can subscribe to it to react on this changes, e.g., adding a debounceTime
operator to apply a delay of time after the user stops typing.
ngOnInit() {
const emailControl = this.signupForm.get('email');
emailControl.valueChanges
.debounceTime(1000)
.subscribe(() => this.setEmailMessages(emailControl));
}
Nowadays, a common good UX practice applied on user form registration is to add an input for email confirmation.
It´s not impossible to be made with template-syntax, but when working with ReactiveForms
, this can be more easily made in a more natural way (without the need to create directives or use third-party components). It can be done in some simple steps (after confirmation input was added to the template like email
was done):
Instead of initializing the FormGroup
with email
and confirmEmail
as FormControl
s, we will group them together in a nested FormGroup
inside the signupForm
as follows:
this.signupForm = this.formBuilder.group({
...
emailGroup: this.formBuilder.group({
email: ['', [Validators.required, Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+')]],
confirmEmail: ['', Validators.required],
}, {validator: emailMatcher}),
...
});
And in the template, we create a div
to group the email
and confirmEmail
HTML template (without any modification on its HTML) and a formGroupName
attribute and set on it the FormGroup
's nested element of the root signup form model object as follows:
<div formGroupName="emailGroup"
[ngClass]="{'has-error': customerForm.get('emailGroup').errors }">
<div class="form-group">
...Email syntax without modifications...
</div>
<div class="form-group">
...Confirm email syntax without modifications...
</div>
</div>
Now, let´s handle a more complex scenario, when a new user requirement comes in to add a option to the user be kept in touch via your phone number or email, for this, we will add a phone
input in the form and a radio to user check how we can maintain contact to him after his registration with email and phone options, email continues being required in the form, the phone is optional, but whether the user selects phone as the option to be contacted the phone becomes required too. in other words, we need to handle a runtime validation here.
For this task, we need to add the FormControl
notification
for the radio input and initialize it with a 'email
' as default selected option in our signupForm
initialization and subscribe to its valueChanges
as follows:
this.signupForm.get('notification').valueChanges
.subscribe(value => this.setNotification(value));
The setNotification
is the method which will change the Validator
of a FormControl
at runtime, according to the value
passed to it:
setNotification(notifyVia: string): void {
const phoneControl = this.customerForm.get('phone');
if (notifyVia === 'phone') {
phoneControl.setValidators(Validators.required);
} else {
phoneControl.clearValidators();
}
phoneControl.updateValueAndValidity();
}
In our template, we just need to add the input radio with the phone
and email
options, add the formControlName
attribute and style it as you wish.
<div class="form-group" >
<label class="col-md-2 control-label">Send Notifications</label>
<div class="col-md-8">
<label class="radio-inline">
<input type="radio"
value="email"
formControlName = "notification">Email
</label>
<label class="radio-inline">
<input type="radio"
value="phone"
formControlName = "notification">Phone
</label>
</div>
</div>
That´s it, working with ReactiveForms
, this task becomes very simple and with a great user experience. By the way, watching and reacting to the user input changes in the form, gives to us a lot of control and possibilities to leverage the UX, some of them are shown here, but there are many other things yet, e.g., working with automatic suggestion, runtime user interface reactions, and more...
We can do even more yet, besides FormGroup
and FormControl
, we can work with another special control, the FormArray
, which is used to dynamically duplicate elements in the UI, in scenarios in which the user may inform more than one address, like address for home, work and others.
Work with FormArray
and iterate over it in our Component or in the HTML template is simple, but for not making this article very extensive, I will put this and all other samples in the github repository at the end of this article.
Summary
We change for template-driven approach which is more easy to work with it, to reactive (in model-driven approach) for giving us more control, that results in a better testable form by leveraging a decoupling between the HTML (design/CSS team can work here) and component's business rules (angular/js specialist members) and to improve the user experience with features like reactive transformations, correlated validations and handle complex scenarios as runtime validation rules and dynamic fields duplication.
This repository contains the source code used for the article
The source code shown in this article is available in a repository Github and can be seen at the following link: