This post was last updated 02/09/2016.
One of the features that made Angular 1.x a great tool for developers was how it simplified working with forms. Angular 2 builds on the strong foundation of Angular 1.x and provides even better tools for working with forms and validation. In this blog, we'll take a closer look at some of these upgrades. Also, stay tuned for two other posts in this series that will cover Directives and Observables.
Getting data from users
One of the most common routines employed in applications is getting data from the user, usually with the use of HTML forms. Let's say we want to create a login form with 2 fields, a text field for username, text field for password, and a button for submitting the form.
The Angular 1.x way
The old way of doing this with Angular might look something like this
<div ng-controller="LoginController">
<form>
Username:
<input type="text"
ng-model="user.username"
required />
Password:
<input type="password"
ng-model="user.password"
required />
<button
ng-click="doLogin(user)">
Login
</button>
</form>
</div>
<script>
angular.controller(
'LoginController', function ($scope) {
$scope.doLogin = function (user) {
// user.username will be username,
// user.password will be the password
console.log(user);
}
});
</script>
In case your Angular is a little rusty, lets quickly explain what's going on here. We use the ng-model directive to bind the value of the input field with the controller associated with the form. In the LoginController we can access the values of the form using $scope. Now for a simple form this may be sufficient but for larger complex forms that require validation, and dynamic creation, doing things this way can get messy pretty quickly.
The Angular 2 Way
Angular 2 approaches forms in a much more structured and organized way. Let's take a look at what this same form would look like in Angular 2:
import {FormBuilder, Validators} from 'angular2/common'
import {Component} from 'angular2/core'
@Component({
selector: 'login-form',
template: `
<form [ngFormModel]="LoginForm"
(submit)="performLogin($event)">
Username:
<input ngControl="username" type="text">
Password:
<input ngControl="password" type="password">
<button type="submit">Login</button>
</form>
`
})
export class LoginForm {
constructor(formBuilder: FormBuilder) {
this.loginForm = formBuilder.group({
username: ["", Validators.required],
password: ["", Validators.required]
});
}
performLogin(event) {
console.log(this.loginForm.value);
event.preventDefault();
}
Wow, this looks a lot different - so what exactly is going on here? Well instead of binding a form's inputs to a controller we now use a component to manage the creation of the form and the state of its data. We import FormBuilder, inject it into our component and create a new a group of fields - one for the username, and another for the password. Each of these fields is an instance of a Control object, which is the smallest and most fundamental unit of an Angular 2 form. The Control object has the following properties:
- value, which holds the current value of the field
- errors, a list of any errors associated with the field
- valid, whether or not the field's current value is within an accepted range of data
These are really useful properties and we can use them to refine what our form does and how it should look under particular states and conditions. But how exactly are these properties determined? Let's first take a look at the valid property.
Validators
Validators allow us to define an accepted range of values that a field is allowed to have before it can be submitted. Assigning a Validator to a Control object and then checking the status of the valid property will let us determine if a field is acceptable or not. In the login form example above we use Validators.required, which is just a basic check to make sure the field is not empty. Let's say we had a new requirement - usernames must not exceed 6 characters in length, and if the user enters a username longer than that we should display a message letting them know.
First we need a function to perform the validation routine. If the field fails the validation we need to return an object with a property outlining what type of error happened. If the field passes the validation then return null (for no errors):
function checkUsernameLength(field: Control) {
if (field.value.length <= 6) {
// returning null means the field passes
// the validity check
return null;
} else {
return { overSixCharacters: true };
}
}
When creating the Control object for the username field we pass in this function as a routine to use in the validation process. Validators.compose allows us to use multiple validators:
...
this.loginForm = formBuilder.group({
username: [ "", Validators.compose([
checkUsernameLength,
Validators.required]),
password: ["", Validators.required]
});
...
Now we can harness the value of the valid property in our username Control object to control how our login form should look like if the user has entered in a username that is not valid:
...
Username:
<input ngControl="username" type="text">
Password:
<input ngControl="password" type="password">
<button type="submit">Login</button>
<div *ngIf="!username.valid">
Please enter a valid username
</div>
...
If we had multiple validators on our username field, we can specifically pick out the status of each validation check by using the hasErrors function and giving it the custom error property employed in each particular validation routine.
...
<div *ngIf="username.hasError('overSixCharacters')">
Username must be fewer than 6 characters!
</div>
...
Now the user knows what is specifically invalid about their input.
Validators are really great tools for ensuring the integrity of data coming from the user and controlling how a form should respond to data being incorrect.
Events
Angular 2 also has revamped the event system in forms.
Let's say we wanted our login form to only display the login button once the username and password field are not empty. Angular 2 handles form and field events in a nicely organized and consistent manner by using the Control object.
Each Control object has an EventEmitter which we can use to watch for changes. An EventEmitter returns an Observable, which is a new open standard that Angular 2 uses a lot. We'll expand on Observables later on in this series, but the basic idea behind them for this scenario is that they are like Promises that can send multiple values through a continuous stream.
To use the EventEmitter we reference the Control object for the field we want to listen to. Then we subscribe to the valueChanges event, which will run our callback function every time the username Control object has been changed.
...
this.loginForm = formBuilder.group({
username: ["", Validators.compose([
checkUsernameLength,
Validator.required]),
password: ["", Validators.required]
});
this.usernameField = this.loginForm.controls['username'];
this.usernameField.valueChanges
.subscribe((value: string) => {
console.log("Username changed: ", value);
});
...
Ta-da! Now we are subscribed and observing any changes to the data. As outlined earlier, using Observables allows for Angular 2 to narrow its checking routine when looking for application wide changes in data. Here is where the performance boost really kicks in, we can listen to a lot of event changes on a lot of different forms without having our application slow to a crawl!
By using FormBuilder we can construct rich HTML forms for receiving data, and by using Validators and Observables we can manage the state of the data and the integrity. Cool stuff!
Conclusion
With Angular 2's form builder, working with forms is a straightforward experience. Being able to logically group form controls makes working with complex series of input fields a much more manageable problem. Additionally the ability to get granular validation feedback makes creating smart responsive forms a painless endeavour.