Create a Contact Form in Angular using Cloud Functions for Firebase

I’m a junior developer just learning the ins and outs of Angular. Thankfully there are huge amount of great resources on the web for learning.

Of course, not everything can be found online. Such is the case with any type of specific walkthrough to create a functional Contact Form in the Angular Framework. Functional is really the key aspect of this tutorial.

Creating forms in Angular is well-documented. Sending the information from a form somewhere isn’t that difficult either. Since Angular is designed (mostly) as a client-side framework, there isn’t an out-of-the-box solution for processing contact form data and sending it as an email.

Thankfully, Cloud Functions for Firebase is our deus ex machina that allows this to work perfectly!


Let’s get started!

For the sake of brevity, I’m going to assume you’re coming here to learn how to create a functional contact form in Angular, and not how to get going with Angular.

Using the Angular CLI, create a new project. This is going to be a bare bones contact form, so no CSS or additional packages will be used. It’s going to look crappy. You’ve been warned.

In a terminal, add firebase and angularfire2 to your project. Create a new Firebase project, and then follow the instructions from the Angular CLI team to add AngularFire2 to your project.

In your Firebase Database Rules, set them to this:

{
"rules": {
".read": "auth != null",
".write": "auth != null",
"messages": {
".write": true
}
}
}

app.module.ts

import { ReactiveFormsModule } from '@angular/forms';
import { AngularFireModule } from 'angularfire2';
import { environment } from '../environments/environment';
@NgModule({
imports: [
...
ReactiveFormsModule,
AngularFireModule.initializeApp(environment.firebase)
...
]
})

app.component.ts

import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AngularFire } from 'angularfire2';
...
export class AppComponent {
form: FormGroup;
  constructor(private fb: FormBuilder, private af: AngularFire) {
this.createForm();
}
  createForm() {
this.form = this.fb.group({
name: ['', Validators.required],
email: ['', Validators.required],
message: ['', Validators.required],
});
}
  onSubmit() {
const {name, email, message} = this.form.value;
const date = Date();
const html = `
<div>From: ${name}</div>
<div>Email: <a href="mailto:${email}">${email}</a></div>
<div>Date: ${date}</div>
<div>Message: ${message}</div>
`;
    let formRequest = { name, email, message, date, html };
    this.af.database.list('/messages').push(formRequest);
this.form.reset();
}
}

app.component.html

<h1>Information Request</h1>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input type="text" placeholder="Name" formControlName="name">
<input type="text" placeholder="Email" formControlName="email">
<textarea placeholder="Message" formControlName="message"
</textarea>
<button type="submit" [disabled]="form.invalid">Submit</button></form>

Here’s how our sweet Contact Form should look:

Contact Form (in Chrome)

Once you submit that form, here’s how your Database should look:

messages/{pushKey}

Great! You’ve got data flowing from your Angular contact form to your Firebase database! Hopefully you’re comfortable with the walkthrough up to this point and haven’t encountered anything new. If you’re unsure about something, ask a question in the comments and I can try and explain what I did.


Cloud Functions for Firebase

Firebase is such a fantastic tool. However, everything that you might do with your data is processed through your client-side code. Firebase does provide an Admin-level SDK for Node.js that you can run on a server, but if your goal is to remain serverless, that’s not the option for you!

Cloud Functions for Firebase provides a way for you to respond to Firebase events using Node.js without needing access to a server.

The setup for Cloud Functions is pretty simple, but depending on what event you plan on responding to and how you want to respond to it, there can be a bunch of gotchas. Some of the steps below may be unnecessary or may require some extra deductions on your part. You know how it goes…it works on my machine.

Follow the generic setup for Cloud Functions down to the heading: Add the addMessage() function. Don’t go to that next step! Don’t do it!

You should now have your functions folder setup with index.js, package.json, and node_modules in it. In your index.js file, you should have the three lines of code from the setup. Keep your terminal open to the functions folder as we’ll use that to run some commands later on.

Configure nodemailer

Add the nodemailer package to your project with yarn add (or npm install) nodemailer. Then, add the following code to the index.js file:

const nodemailer = require('nodemailer');
const gmailEmail = encodeURIComponent(functions.config().gmail.email);
const gmailPassword = encodeURIComponent(functions.config().gmail.password);
const mailTransport = nodemailer.createTransport(`smtps://${gmailEmail}:${gmailPassword}@smtp.gmail.com`);

Here, we’re adding nodemailer to the project, and then setting a few variables. You may be wondering, what is functions.config().gmail.email? Something really cool about Firebase is that you can set configuration variables using the command line. These are variables that aren’t exposed in your code anywhere, but that Cloud Functions can pick up on.

In your terminal, set your gmail email and password with the following command:

firebase functions:config:set gmail.email="myusername@gmail.com" gmail.password="secretpassword"

You should get a message that says Functions config updated. if all went well. You can check your Functions config by typing firebase functions:config:get. It’ll spit back out a .json-looking response with your current config.

Now, when your index.js file is processed, that functions.config().gmail.email will grab the values from the config object and nodemailer will use them to send the email!

Create the Cloud Function

Underneath your require statements and configuration, add the following code:

exports.sendContactMessage = functions.database.ref('/messages/{pushKey}').onWrite(event => {
const snapshot = event.data;
// Only send email for new messages.
if (snapshot.previous.val() || !snapshot.val().name) {
return;
}

const val = snapshot.val();

const mailOptions = {
to: 'test@example.com',
subject: `Information Request from ${val.name}`,
html: val.html
};
  return mailTransport.sendMail(mailOptions).then(() => {
return console.log('Mail sent to: test@example.com'))
};
});

sendContactMessage is the name of the Cloud Function. The event that we want it to respond to is the database.ref().onWrite event. This event is fired any time data is changed in the database. The {pushKey} acts like a variable so we can access the data below the push key without needing to know the exact key itself.

First we’ll initialize a variable, snapshot and then write a check to figure out if the onWrite event is adding new data. If the data already existed (change) or if the data no longer exists (deletion), we’ll simply return and exit the Cloud Function.

Assuming it’s a new message, we’ll initialize val to be an object that contains the message data. From there we’ll create our mailOptions object that nodemailer directs us to create. There are a bunch of different ways to send the message, so check out the docs!

In the to: field you can hard code where the email is being sent. Assuming you’d like to have all contact forms sent to you or someone in particular, this is fine. Otherwise, you could query the database and grab an email dynamically.

Finally, we’ll send the email using nodemailer’s sendMail method with our mailOptions object passed in. This returns a promise, so when that comes back, we’ll send a message to the console that the email was delivered successfully.

Send the Function to Firebase

After we’ve set up our index.js it’s time to upload our Function to Firebase. Do this with the firebase deploy --only functions command in the terminal. There are a couple things to note with this procedure:

  1. This takes a while. One function isn’t terrible, but three, four, 10 will take multiple minutes. Take a few minutes to stretch and walk around.
  2. Whatever existing Functions you’ve got uploaded will be overwritten. This includes functions that you haven’t changed. If you have got 10 functions and only changed one, it’ll still upload and overwrite all 10. Maybe this is something Google will optimize in the future, similar to how you can now upgrade an Android app using just the delta instead of downloading an entirely new app.
  3. Existing functions that are not apart of your new index.js file will be removed. If you’re using a test project in the Firebase console with other Functions from a different index.js file, and you upload this new project, those existing Functions will just be deleted and replaced with the newly deployed Functions.

Not so fast — gotcha!

Okay, I know you’ve made it this far, we’ve got an Angular project up and running, a Firebase connection established, Cloud Functions deployed and ready to be fired off on that first Database write, but this is the Google ecosystem…based on what exactly we’re trying to do, we have some final, additional things to setup and configure.

  1. Switch to the Blaze plan in Firebase. Because we are using a non-Google API, we can’t use this on the free plan. You can read the full pricing details but we should be talking fractions of a cent to do what we’re doing.
  2. Read the Gmail-specific gotchas. Because we’re using a Google service to send email….well, just read the nodemailer doc and you’ll see what needs to be done. It’s basically flipping a few switches.
  3. There are limits, you know. You can only send 500 emails a day using Gmail, or 2000 if you’re using a G Suite address. That seems pretty reasonable for a contact form, but if you’re doing any kind of mass email, nodemailer can definitely help, but Gmail is not the way to send it.

Send that Contact Form request!

We should now be up and running with everything. It’s time to fill out that form, hit submit and watch the email come in. If there are any errors, you can check out the Functions tab in Firebase and click on the Logs section to see anything that comes in.

If it works, you’ll actually get the email in your Inbox before the logs even register the Function execution has started.


Thanks for reading through this tutorial on how to create a functional contact form in Angular using Cloud Functions for Firebase. If you have any questions or get stuck at a step, please let me know in the comments below. Hopefully it’s a typo or some small mistake that I can correct in the tutorial so others don’t get hung up on it, too!