In this section, you will build an app to demonstrate form validation using ngForm
and ngControl
. Here is a screenshot of the form:
If the user tries to submit without providing valid information, the form will show the following error:
Basically, the Name field is required. The Phone field is the number type, but is optional. The Comment field is required and the user must enter at least four characters. Of course, this is just for demonstration of the input length. The user, finally, must agree to the terms and conditions via the toggle input.
After a successful validation, the user will be taken to the second screen with a summary of the previous screen, as illustrated in the following screenshot:
This app example could work either in a browser or on a physical device. However, you can optionally connect your physical device to verify the Phone field for number keypad.
Observe the following the instructions:
MyFormValidation
app using the blank
template, as shown, and go to the MyFormValidation
folder:$ ionic start MyFormValidation blank --v2 $ cd MyFormValidation
./src/app/app.module.ts
file and replace the content with the following code:import { NgModule } from '@angular/core'; import { IonicApp, IonicModule } from 'ionic-angular'; import { MyApp } from './app.component'; import { HomePage } from '../pages/home/home'; import { ThankyouPage } from '../pages/thankyou/thankyou'; import { MyFormService } from '../services/myform'; @NgModule({ declarations: [ MyApp, HomePage, ThankyouPage ], imports: [ IonicModule.forRoot(MyApp) ], bootstrap: [IonicApp], entryComponents: [ MyApp, HomePage, ThankyouPage ], providers: [MyFormService] }) export class AppModule {}
You may realize that there is a common service to be used across the app, called MyFormService
here. This example also has a second page, called ThankyouPage
.
$ mkdir ./src/services
myform.js
file in the component's directory that you just created, as follows:import {Injectable} from '@angular/core'; @Injectable() export class MyFormService { public name: string = ''; public phone: number; public comment: string = ''; constructor() { } }
This example will keep the service component simple for demonstration purposes.
./src/pages/home/home.html
template, as shown:<ion-header> <ion-navbar color="primary"> <ion-title> Contact Form </ion-title> </ion-navbar> </ion-header> <ion-content> <p class="center"> <ion-icon class="large lighter" primary name="contact"></ion-icon> </p> <form #f="ngForm" novalidate (ngSubmit)="onSubmit(f)"> <ion-list> <ion-item> <ion-label floating>Name</ion-label> <ion-input type="text" name="name" required [(ngModel)]="data.name"></ion-input> </ion-item> <div [hidden]="f.controls.name && (f.controls.name.valid || (f.controls.name.pristine && !isSubmitted))" class="note danger"> Name is required </div> <ion-item> <ion-label floating>Phone</ion-label> <ion-input type="tel" name="phone" [(ngModel)]="data.phone"></ion-input> </ion-item> <ion-item> <ion-label floating>Comment</ion-label> <ion-input type="text" required minlength=4 name="comment" [(ngModel)]="data.comment"></ion-input> </ion-item> <div *ngIf="(isSubmitted && f.controls.comment && f.controls.comment.pristine) || ((f.controls.comment) && (f.controls.comment.dirty && f.controls.comment.errors))" class="note danger"> Please enter {{ 4 - (f.controls.comment.errors.minlength ? f.controls.comment.errors.minlength.actualLength : 0) }} more characters </div> <ion-item class="tos"> <ion-toggle item-left [(ngModel)]="data.tos" name="tos" type="button" (click)="noSubmit($event)"></ion-toggle> <ion-label item-right>Agree to terms and conditions</ion-label> </ion-item> <div [hidden]="(!isSubmitted) || (f.controls.tos && data.tos)" class="note danger"> Please check Agree! </div> </ion-list> <div class="center"> <button ion-button type="submit" round outline>Submit</button> </div> </form> </ion-content>
This is probably the most complicated part of the form validation process because there are many places where you have to embed validation logic for the input.
./src/pages/home/home.scss
file with the following code:.center { text-align: center; } ion-icon.large { font-size: 7em; } ion-icon.lighter { opacity: 0.5; } ion-list > .item:first-child { border-top: 0; } ion-list > .item:last-child, ion-list > ion-item-sliding:last-child .item { border-bottom: 0; } .tos { padding-top: 10px; ion-toggle { padding-left: 0px; } .item-inner { border-bottom: 0; } } .item ion-toggle { padding-left: 0; } .note.danger { padding-left: 16px; color: #d14; }
./src/pages/home/home.ts
for editing with the following code:import { Component } from '@angular/core'; import { NavController } from 'ionic-angular'; import { ThankyouPage } from '../thankyou/thankyou'; import { MyFormService } from '../../services/myform'; @Component({ selector: 'page-home', templateUrl: 'home.html' }) export class HomePage { private data: any; private isSubmitted: Boolean = false; constructor(public nav: NavController, private formData: MyFormService) { this.nav = nav; this.formData = formData; this.data = { name: '', phone: '', comment: '', tos: false } } onSubmit(myForm) { this.isSubmitted = true; console.log('onSubmit'); console.log(myForm); if ((myForm.valid) && (myForm.value.tos)) { this.formData.name = this.data.name; this.formData.phone = this.data.phone; this.formData.comment = this.data.comment; this.nav.push(ThankyouPage); } } noSubmit(e) { e.preventDefault(); } }
You may note that there isn't much validation code in the JavaScript part. This means that the template takes care of a lot of the validations. There is also an import
command for a thankyou
page, which you will have to create next.
thankyou
folder, as follows: $ mkdir ./src/pages/thankyou
thankyou.js
file in the component's directory that you just created, as shown:import { Component } from '@angular/core'; import { MyFormService } from '../../services/myform' @Component({ templateUrl: 'thankyou.html' }) export class ThankyouPage { constructor(private formData: MyFormService) { this.formData = formData; } }
This page just renders the data from the MyFormService
service. So, you can keep it very simple.
thankyou.html
in the ./src/pages/thankyou
, folder, as illustrated:<ion-header> <ion-navbar color="secondary"> <ion-title> Thank You </ion-title> </ion-navbar> </ion-header> <ion-content> <h6 class="padding"> You submitted the following information </h6> <div class="my-table"> <ion-row> <ion-col width-25 class="my-label">Name</ion-col> <ion-col width-75>{{ formData.name }}</ion-col> </ion-row> <ion-row> <ion-col width-25 class="my-label">Phone</ion-col> <ion-col width-75>{{ formData.phone }}</ion-col> </ion-row> <ion-row> <ion-col width-25 class="my-label">Comment</ion-col> <ion-col width-75>{{ formData.comment }}</ion-col> </ion-row> </div> </ion-content>
thankyou.scss
in the ./src/pages/thankyou
folder, as shown:h6.padding { color: #4C555A; padding: 10px; } .my-label { text-align: right; font-weight: bold; } .my-table { ion-row { color: #4C555A; padding: 0; height: 30px; } ion-row + ion-row { margin-top: 0; } ion-row:nth-child(odd) ion-col { background: #F9FAFB; } }
./app/app.scss
file to ensure that you include both the .scss
files in the two pages, as follows:@import '../pages/home/home'; @import '../pages/thankyou/thankyou';
$ ionic serve
Let's start with the home.html
file, where most of the validation code is located. If you look at the structure of this page, it's very typical. You have <ion-navbar>
with <ion-title>
. The <form>
element must be inside the <ion-content>
area.
form
has the following attributes:
<form #f="ngForm" novalidate (ngSubmit)="onSubmit(f)">
To assign a local variable on the fly, you use the #
sign. This means that you want the f
variable to refer to ngForm
, which is automatically created from Angular 2. This is a special object that contains everything related to the current form. You are advised to use novalidate
to bypass the default HTML5 validation because you are using Angular 2 for validation instead. Otherwise, the form
will acquire conflicts. The (ngSubmit)
is pretty much an event to trigger the onSubmit(f)
function whenever the button
with type=submit
is touched or clicked. When you submit the form, it will pass the f
variable along so that you can process the object inside the onSubmit
method.
The form
template consists of just <ion-list>
and <ion-item>
. You just need to know how to validate each input and display the error. Let's use the Name
field as the first example. This is the <ion-input>
for Name
:
<ion-input type="text" name="name" required [(ngModel)]="data.name"></ion-input>
The following is the error displayed:
<div [hidden]="f.controls.name && (f.controls.name.valid || (f.controls.name.pristine && !isSubmitted))" class="note danger"> Name is required </div>
To validate, you must assign name
a local variable name. This is to refer to that input using f.controls.name
in other areas. Recall that the f
variable has been declared previously as the ngForm
object. Here is a view of how the ngForm
is structured:
You can view this using the Chrome Developer console because the code actually gives this output when you submit the form.
The error message Name is required
will be hidden when either of the following conditions takes place:
isSubmitted
. The f.controls.name.pristine
variable means that the input has not been modified. The opposite of this would be f.controls.name.dirty
.f.controls.name.valid
variable is true
. However, you cannot check this right away because, if the input is empty, the name
object does not exist yet. That's why you need to check for the existence of f.controls.name
before checking for the valid
Boolean.There is no need to check the phone requirement; so, you just need to assign name
and a model, as shown:
<ion-input type="tel" name="phone" [(ngModel)]="data.phone"></ion-input>
For the Comment
field, there is a need to validate using both required
and minlength=4
, as follows:
<ion-input type="text" required minlength=4 name="comment" [(ngModel)]="data.comment"></ion-input>
You may think required
is unnecessary because, if the length is zero, Angular 2 will trigger an error flag. However, that is not true. When the user doesn't enter anything in the input, the input will have no length because the variable doesn't exist. That's why you need to check for both scenarios.
The error message for the Comment
field is quite interesting because it shows the number of characters the user needs to enter, as shown in the following code:
<div *ngIf="(isSubmitted && f.controls.comment && f.controls.comment.pristine) || ((f.controls.comment) && (f.controls.comment.dirty && f.controls.comment.errors))" class="note danger"> Please enter {{ 4 - (f.controls.comment.errors.minlength ? f.controls.comment.errors.minlength.actualLength : 0) }} more characters </div>
The main idea here is that you only want to show this div
when the form is submitted and it's pristine via f.controls.comment.pristine
. This means that the user has not entered anything in the form. You also want to show the message when the form is dirty and has errors via f.controls.comment.errors
. If you inspect the console, you can see a list of many detailed errors under the f.controls.comment.errors
object. In order to tell the user how many characters they have left to enter, you have to first check f.controls.comment.errors.minlength
because, if that variable doesn't exist, there is no error or the comment
input is empty. If you do not check for this, you will get a parse error later on.
In your home.ts
file, the onSubmit
method must toggle the isSubmitted
Boolean to true
, as shown in the following code snippet:
onSubmit(myForm) { this.isSubmitted = true; console.log('onSubmit'); console.log(myForm); if ((myForm.valid) && (myForm.value.tos)) { this.formData.name = this.data.name; this.formData.phone = this.data.phone; this.formData.comment = this.data.comment; this.nav.push(ThankyouPage); } }
Then, you have to do a general check for myForm.valid
and myForm.value.tos
. You may wonder why we are checking for tos
here instead of validating it inside the template. The reason is that there is no way to validate a toggle button in Angular 2 since it doesn't make sense to do so as it cannot be required
. Therefore, you have to do a custom validation here to make sure it's true
in the form. This means that the user has checked the Agree to terms and conditions toggle.
This is a minor detail that could be a bug in Ionic 2 (currently Beta 21):
noSubmit(e) { e.preventDefault(); }
For each toggle button, it acts as a type=submit
button by default since there is no type
attribute assigned. That's why you need to cancel the submit
event by calling preventDefault()
.
Refer to the W3 website, at https://www.w3.org/TR/html-markup/button.html, for information about the default behavior of the button
element.
The thankyou
page is very self-explanatory because you just parse the formData
object in the template by getting the data from the MyFormService
service.
Check out the following links for more information:
form
from the Angular 2 documentation, you can visit https://angular.io/docs/ts/latest/guide/forms.html and https://angular.io/docs/ts/latest/api/forms/index/NgForm-directive.html18.224.64.89