Typescript is an open-source programming language which adds optional static typing to Javascript. To give you a flavor of the benefits of TypeScript, let’s have a very quick look at some of the things that TypeScript brings to the table:
In this article, we will learn how to build forms with typescript. We will cover as much as it takes to build business applications that collect user information. Here is a breakdown of what you should expect from this article:
This article is an excerpt from the book, TypeScript 2.x for Angular Developers, written by Chris Nwamba.
We want to try to utilize TypeScript as much as possible, as it simplifies our development process and makes our app behavior more predictable. For this reason, we will create a simple data class to serve as a type for the form values.
First, create a new Angular project to follow along with the examples. Then, use the following command to create a new class:
ng g class flight
The class is generated in the app folder; replace its content with the following data class:
export class Flight { constructor( public fullName: string, public from: string, public to: string, public type: string, public adults: number, public departure: Date, public children?: number, public infants?: number, public arrival?: Date, ) {} }
This class represents all the values our form (yet to be created) will have. The properties that are succeeded by a question mark (?) are optional, which means that TypeScript will throw no errors when the respective values are not supplied.
Before jumping into creating forms, let's start with a clean slate. Replace the app.component.html file with the following:
<div class="container"> <h3 class="text-center">Book a Flight</h3> <div class="col-md-offset-3 col-md-6"> <!-- TODO: Form here --> </div> </div>
Run the app and leave it running. You should see the following at port 4200 of localhost (remember to include Bootstrap):
Now that we have a contract that we want the form to follow, let's now generate the form's component:
ng g component flight-form
The command also adds the component as a declaration to our App module:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FlightFormComponent } from './flight-form/flight-form.component';
@NgModule({
declarations: [
AppComponent,
// Component added after
// being generated
FlightFormComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
What makes Angular forms special and easy to use are functionalities provided out-of-the-box, such as the NgForm directive. Such functionalities are not available in the core browser module but in the form module. Hence, we need to import them:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
// Import the form module
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { FlightFormComponent } from './flight-form/flight-form.component';
@NgModule({
declarations: [
AppComponent,
FlightFormComponent
],
imports: [
BrowserModule,
// Add the form module
// to imports array
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Simply importing and adding FormModule to the imports array is all we needed to do.
The perfect time to start showing some form controls using the form component in the browser is right now. Keeping the state in sync between the data layer (model) and the view can be very challenging, but with Angular it's just a matter of using one directive exposed from FormModule:
<!-- ./app/flight-form/flight-form.component.html --> <form> <div class="form-group"> <label for="fullName">Full Name</label> <input type="text" class="form-control" [(ngModel)]="flightModel.fullName" name="fullName" > </div> </form>
Angular relies on the name attribute internally to carry out binding. For this reason, the name attribute is required.
Pay attention to [(ngModel)]="flightModel.fullName"; it's trying to bind a property on the component class to the form. This model will be of the Flight type, which is the class we created earlier:
// ./app/flight-form/flight-form.component.ts
import { Component, OnInit } from '@angular/core';
import { Flight } from '../flight';
@Component({
selector: 'app-flight-form',
templateUrl: './flight-form.component.html',
styleUrls: ['./flight-form.component.css']
})
export class FlightFormComponent implements OnInit {
flightModel: Flight;
constructor() {
this.flightModel = new Flight('', '', '', '', 0, '', 0, 0, '');
}
ngOnInit() {}
}
The flightModel property is added to the component as a Flight type and initialized with some default values.
Include the component in the app HTML, so it can be displayed in the browser:
<div class="container"> <h3 class="text-center">Book a Flight</h3> <div class="col-md-offset-3 col-md-6"> <app-flight-form></app-flight-form> </div> </div>
This is what you should have in the browser:
To see two-way binding in action, use interpolation to display the value of flightModel.fullName. Then, enter a value and see the live update:
<form> <div class="form-group"> <label for="fullName">Full Name</label> <input type="text" class="form-control" [(ngModel)]="flightModel.fullName" name="fullName" > {{flightModel.fullName}} </div> </form>
Here is what it looks like:
Let's get hands-on and add the remaining form fields. After all, we can't book a flight by just supplying our names.
The from and to fields are going to be select boxes with a list of cities we can fly into and out of. This list of cities will be stored right in our component class, and then we can iterate over it in the template and render it as a select box:
export class FlightFormComponent implements OnInit { flightModel: Flight; // Array of cities cities:Array<string> = [ 'Lagos', 'Mumbai', 'New York', 'London', 'Nairobi' ]; constructor() { this.flightModel = new Flight('', '', '', '', 0, '', 0, 0, ''); } }
The array stores a few cities from around the world as strings. Let's now use the ngFor directive to iterate over the cities and display them on the form using a select box:
<div class="row"> <div class="col-md-6"> <label for="from">From</label> <select type="text" id="from" class="form-control" [(ngModel)]="flightModel.from" name="from"> <option *ngFor="let city of cities" value="{{city}}">{{city}}</option> </select> </div> <div class="col-md-6"> <label for="to">To</label> <select type="text" id="to" class="form-control" [(ngModel)]="flightModel.to" name="to"> <option *ngFor="let city of cities" value="{{city}}">{{city}}</option> </select> </div> </div>
Neat and clean! You can open the browser and see it right there:
The select drop-down, when clicked, shows a list of cities, as expected:
Next, let's add the trip type field (radio buttons), the departure date field (date control), and the arrival date field (date control):
<div class="row" style="margin-top: 15px"> <div class="col-md-5"> <label for="" style="display: block">Trip Type</label> <label class="radio-inline"> <input type="radio" name="type" [(ngModel)]="flightModel.type" value="One Way"> One way </label> <label class="radio-inline"> <input type="radio" name="type" [(ngModel)]="flightModel.type" value="Return"> Return </label> </div> <div class="col-md-4"> <label for="departure">Departure</label> <input type="date" id="departure" class="form-control" [(ngModel)]="flightModel.departure" name="departure"> </div> <div class="col-md-3"> <label for="arrival">Arrival</label> <input type="date" id="arrival" class="form-control" [(ngModel)]="flightModel.arrival" name="arrival"> </div> </div>
How the data is bound to the controls is very similar to the text and select fields that we created previously. The major difference is the types of control (radio buttons and dates):
Lastly, add the number of passengers (adults, children, and infants):
<div class="row" style="margin-top: 15px"> <div class="col-md-4"> <label for="adults">Adults</label> <input type="number" id="adults" class="form-control" [(ngModel)]="flightModel.adults" name="adults"> </div> <div class="col-md-4"> <label for="children">Children</label> <input type="number" id="children" class="form-control" [(ngModel)]="flightModel.children" name="children"> </div> <div class="col-md-4"> <label for="infants">Infants</label> <input type="number" id="infants" class="form-control" [(ngModel)]="flightModel.infants" name="infants"> </div> </div>
The passengers section are all of the number type because we are just expected to pick the number of passengers coming onboard from each category:
Angular greatly simplifies form validation by using its built-in directives and state properties. You can use the state property to check whether a form field has been touched. If it's touched but violates a validation rule, you can use the ngIf directive to display associated errors.
Let's see an example of validating the full name field:
<div class="form-group">
<label for="fullName">Full Name</label>
<input
type="text"
id="fullName"
class="form-control"
[(ngModel)]="flightModel.fullName"
name="fullName"
#name="ngModel"
required
minlength="6">
</div>
We just added three extra significant attributes to our form's full name field: #name, required, and minlength. The #name attribute is completely different from the name attribute in that the former is a template variable that holds information about this given field via the ngModel value while the latter is the usual form input name attribute.
In Angular, validation rules are passed as attributes, which is why required and minlength are there.
Yes, the fields are validated, but there are no feedbacks to the user on what must have gone wrong. Let's add some error messages to be shown when form fields are violated:
<div *ngIf="name.invalid && (name.dirty || name.touched)" class="text-danger"> <div *ngIf="name.errors.required"> Name is required. </div> <div *ngIf="name.errors.minlength"> Name must be at least 6 characters long. </div> </div>
The ngIf directive shows these div elements conditionally:
The following two screenshots show these error outputs in the browser:
A different error is shown when a value is entered but the value text count is not up to 6:
We need to consider a few factors before submitting a form:
To make sure that the form is valid, we can disable the Submit button:
<form #flightForm="ngForm"> <div class="form-group" style="margin-top: 15px"> <button class="btn btn-primary btn-block" [disabled]="!flightForm.form.valid"> Submit </button> </div> </form>
First, we add a template variable called flightForm to the form and then use the variable to check whether the form is valid. If the form is invalid, we disable the button from being clicked:
To handle the submission, add an ngSubmit event to the form. This event will be called when the button is clicked:
<form #flightForm="ngForm" (ngSubmit)="handleSubmit()"> ... </form>
You can now add a class method, handleSubmit, to handle the form submission. A simple log to the console may be just enough for this example:
export class FlightFormComponent implements OnInit {
flightModel: Flight;
cities:Array<string> = [
...
];
constructor() {
this.flightModel = new Flight('', '', '', '', 0, '', 0, 0, '');
}
// Handle for submission
handleSubmit() {
console.log(this.flightModel);
}
}
We discussed about collecting user inputs via forms. We covered important features of forms, such as typed inputs, validation, two-way binding, submission, and so on. All these interesting methods will prepare you for getting started with building business applications.
If you liked our article, you may read our book TypeScript 2.x for Angular Developers, to learn to use typed DOM events and event handling among other interesting things to do with Typescript.
Typescript 2.9 release candidate is here
How to install and configure TypeScript
How to work with classes in Typescript