Learn How to Validate Forms with Ionic and Firebase (Client and Server Side)
Are you sure the type of data you’re sending from your Ionic app is the one that’s storing in your Firebase database?
It turns out that for me, it wasn’t.
I was checking out a revenue tracking app I build for my wife, she organizes events, and I was
convinced that all my programming was on point, little did I know that some of the ticket prices
were storing in Firebase as string
instead of number =/
That lead me to dig a little deeper into form validation with Ionic, and not only that but to look on how to validate the server side with Firebase, to make sure things were storing as I thought.
By the end of this post, you’ll learn how to validate your data with both Ionic Framework and Firebase.
You’ll do this in 3 steps:
- STEP #1: You’ll do client-side validation in the Ionic Form.
- STEP #2: You’ll add an extra layer of safety with TypeScript’s type declaration.
- STEP #3: You’ll validate the data server-side with Firebase
The first thing you’ll do is create your app and initialize Firebase, that’s out of the scope of this post (mainly because I’m tired of copy/pasting it).
If you don’t know how to do that you can read about it here first.
After your app is ready to roll, I want you to create a service to handle the data.
Open your terminal and create it like this:
ionic generate service services/firebase
Step #1: Create and Validate the form in Ionic
We’re going to do something simple here. We’re creating a form that will take three inputs, a song’s name, its artist’s name, and the user’s age to make sure the user is over 18yo.
Go to home.ts
and import angular’s form modules:
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
We’ll use FormBuilder
to create the form, so go ahead and inject it into the controller.
addSongForm:FormGroup;
constructor(
private readonly formBuilder: FormBuilder,
private readonly firebaseData: FirebaseProvider
) {}
Now we’re going to be initializing the form and declaring the inputs it’s going to have.
this.addSongForm = formBuilder.group({
songName: ['', Validators.compose([Validators.required, Validators.maxLength(45)])],
artistName: ['', Validators.compose([Validators.required, Validators.minLength(2)])],
userAge: ['', Validators.compose([Validators.required])],
});
Let’s go through a bit of theory about what we just saw.
Angular forms module comes with a lot of pre-built goodies, one of those goodies is the Validators
module, that module comes with pre-configured validators like required
, minLength
, and
maxLength
.
Without you doing any extra work, the Validators module is checking that the songName
input won’t
have more than 45 characters, or that the artist’s name needs at least 2 characters, or that all of
the fields are required.
The cool thing tho is that we can kick it up a notch and create our validators
For example, I want to validate that users are over 18 years old, so I’m going to be requiring the user to fill the age field and validating that field is over 18.
I know there are probably 10 better ways to do this, but remember, that’s not the point :P
We’re going to create a validator that takes the age and makes sure it’s a number greater than or equal to 18.
For that I want you to create a folder called validators
inside your src
folder, and create a
file called age.ts
Open up age.ts
and let’s start creating our validator
The first thing you’ll do in that file is to import the module we’ll need:
import { FormControl } from '@angular/forms';
Then create and export the class, I’m gonna call it AgeValidator
:
export class AgeValidator {...}
And inside the class, we’ll create a method called isValid
:
static isValid(control: FormControl): any {...}
Now inside that method we’ll verify the age:
if (control.value >= 18) {
return null;
}
return { notOldEnough: true };
If the value it’s evaluating is greater than or equal to 18, it’s going to return null, but if it’s not, it will return that object.
Now that the validator is ready, go ahead and import it in home.ts
:
import { AgeValidator } from '../../validators/age';
And add it to the userAge
field initialization inside the constructor:
this.addSongForm = formBuilder.group({
userAge: ['', Validators.compose([Validators.required, AgeValidator.isValid])],
});
The View Form
Now it’s time to go to home.html
and start creating the form, first, delete everything inside the
<ion-content></ion-content>
tags.
And create a form there:
<form [formGroup]="addSongForm" (submit)="addSong()" novalidate></form>
The form is going to have a few things:
[formGroup]="addSongForm"
is the name (and initialization in the ts file) we’re giving the form.(submit)="addSong()"
is telling Ionic that when this form is submitted it needs to run theaddSong()
function.novalidate
tells the browser to turn validation off, that way we handle the validation with the form modules.
After the form is created it’s time to add our first input. First we’ll create the input:
<ion-item>
<ion-label position="stacked">Song Name</ion-label>
<ion-input formControlName="songName" type="text" placeholder="What's the song's name?">
</ion-input>
</ion-item>
Then, we’ll show an error message if the form isn’t valid, so right after that input add a paragraph with the error message:
<ion-item
class="error-message"
*ngIf="!addSongForm.controls.songName.valid
&& addSongForm.controls.songName.touched"
>
<p>The song's name is required to be under 45 characters.</p>
</ion-item>
We’re setting up the error message to hide, and only show if:
- The form field isn’t valid.
AND
- The form field is
dirty
(this just means the user already added value to it)
Let’s also add a CSS class to show a small red line if the field isn’t valid (you know, nothing says form errors like red lines)
<ion-input
[class.invalid]="!addSongForm.controls.songName.valid
&& addSongForm.controls.songName.touched"
>
</ion-input>
That right there adds a CSS class called invalid
if the form isn’t valid and has a value inside.
By the way, that’s one line of CSS
.invalid {
border-bottom: 1px solid #ff6153;
}
In the end, the entire input should look like this:
<ion-item>
<ion-label position="stacked">Song Name</ion-label>
<ion-input
formControlName="songName"
type="text"
placeholder="What's the song's name?"
[class.invalid]="!addSongForm.controls.songName.valid && addSongForm.controls.songName.touched"
>
</ion-input>
</ion-item>
<ion-item
class="error-message"
*ngIf="!addSongForm.controls.songName.valid && addSongForm.controls.songName.touched"
>
<p>The song's name is required to be under 45 characters.</p>
</ion-item>
Now repeat this process 2 times to get the artist’s name:
<ion-item>
<ion-label position="stacked">Artist Name</ion-label>
<ion-input
formControlName="artistName"
type="text"
placeholder="What's the artist's name?"
[class.invalid]="!addSongForm.controls.artistName.valid && addSongForm.controls.artistName.touched"
>
</ion-input>
</ion-item>
<ion-item
class="error-message"
*ngIf="!addSongForm.controls.artistName.valid && addSongForm.controls.artistName.touched"
>
<p>The artist's name has to be at least 2 characters long.</p>
</ion-item>
And to get the user’s age:
<ion-item>
<ion-label position="stacked">How old are you?</ion-label>
<ion-input
formControlName="userAge"
type="number"
placeholder="I'm 30 years old."
[class.invalid]="!addSongForm.controls.userAge.valid && addSongForm.controls.userAge.touched"
>
</ion-input>
</ion-item>
<ion-item
class="error-message"
*ngIf="!addSongForm.controls.userAge.valid && addSongForm.controls.userAge.touched"
>
<p>You must be 18 or older to use this app.</p>
</ion-item>
And finally you’ll add a submit button:
<ion-button expand="block" type="submit">Add Song</ion-button>
Let’s take it a step forward and disable the button until it’s valid:
<ion-button expand="block" type="submit" [disabled]="!addSongForm.valid"> Add Song </ion-button>
And there you have complete form validation working with an Ionic 2 app.
And for many apps, that’s it, that’s all the validation they offer, and that’s OK, kind of.
But we’re going to be taking things to a different level, and we’re going to work on having our form data validated in multiple ways to avoid weird surprises.
So we’ll add 2 extra layers
Step #2: Add TypeScript type declaration
For the type declarations, you’ll start working on your FirebaseData
service, to send the data to
Firebase.
Go ahead and in the firebase-data.ts
file import Firebase:
import { collection, Firestore, addDoc } from '@angular/fire/firestore';
And then just create the function to push the new song to the database:
constructor(private readonly firestore: Firestore) {}
saveSong(songName, artistName, userAge) {
return addDoc(collection(this.firestore, `songs`), { songName, artistName, userAge })
}
That’s a regular addDoc()
function to add objects to a Firebase list, one cool thing I learned in
ES6 for Everyone is that if the object properties and values have
the same name you can just type them once, so:
.add({
songName: songName,
artistName: artistName,
userAge: userAge
});
Becomes:
.add({ songName, artistName, userAge });
And now, add the type declarations, just as easy as:
saveSong(songName: string, artistName: string, userAge: number) {...}
That tells TypeScript that the song’s name has to be a string, the artist’s name has to be a string, and the user’s age needs to be a number.
Now in your home.ts
file just create the addSong()
function to send the data to the provider, it
should be something like this:
addSong(){
if (!this.addSongForm.valid){
console.log("Nice try!");
} else {
this.firebaseData.saveSong(this.addSongForm.value.songName, this.addSongForm.value.artistName,
parseFloat(this.addSongForm.value.userAge)).then( () => {
this.addSongForm.reset();
});
}
}
If the form isn’t valid, don’t do anything, and if it is, then sends the data to the provider, I’m resetting all the input values after it’s saved.
See? We just added a small extra integrity layer for our data with little work.
And now it’s time to do the server side validation to make things EXTRA safe!
Step #3: Add server side data validation
You can do some validation in your security rules, things like making sure exact properties are there, or that they’re equal to something or something else.
For example:
Let’s say you want to make sure that the user’s age is a number and it’s over 18, then you can write a security rule like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/{document=**} {
allow read: if true;
allow write: if request.resource.data.age is number && request.resource.data.age > 18
}
}
}
You can learn a lot more going through the Firebase official docs or looking at the API reference 😃.
That will make sure that you’re always storing the right data if by any chance you send a string
instead of a number for the user’s age, then the addDoc()
method is going to throw a Permission
Denied error.
And there you have it, you now have a complete validation flow, starting from validating your inputs in your app and moving all the way to do server-side validation with Firebase.
Go ahead and have a cookie, you deserve it, you just:
- Used angular form modules to create validation in your form.
- Used TypeScript to add validation layer.
- and used Firebase security rules to do server-side validation.