Build a Contact Form with VueJS, Firebase & Postmark
Introduction
This article covers the process of setting up a web contact form using a Javascript library, VueJS, alongside Google Firebase Cloud services. The form’s content is saved to Google Firestore and we set up an event handler using Google Cloud’s serverless compute platform (Cloud Functions) to send an email notification via the Postmark API.
Getting Started
The first stage is to add the relevant packages that we will be using.
// Note
// This tutorial assumes you have already set up a VueJs project and // created the route and view for a contact form screen.
Setting up Firebase
If you have not already done so, set up a firebase project within the firebase cloud console. This is home to your project’s cloud firebase functionality; including your database, web hosting and cloud functions.
Next, install Firebase command line tools to your local environment:
>npm i -g firebase-tools
Now, from within your project’s main folder, install and initialise Firebase:
>npm install firebase --save firebase login firebase init
The above commands will take you through a guided setup process. If you are unsure which options to choose, accept the default setting.
// Warning
// The initialisation process will overwrite some files. If you are // using an existing project, be sure to have backups just in case.
If the firebase install was succesful you will now have the relevant packages installed. You can check this within your packages.json file where you should now see the firebase packages inside the dependencies block.
Open your app’s main.js file and add a firebase import statement and require the firestore and main firebase app components.
import { firebase } from '@firebase/app' require('firebase/firestore') require('firebase/app')
In the same file, initialise firebase using the config object provided in the firebase cloud console.
firebase.initializeApp({ apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', databaseURL: 'https://xxxxxxxxx.firebaseio.com', projectId: 'xxxxxxxxx', appId: '1:xxxxxxxxx:web:xxxxxxxxxxxxxxxxxx' })
We will now make the firestore object available throughout the app using a Vue instance property.
Vue.prototype.$firebaseDatabase = firebase.default.firestore()
Build the Contact Form
There are a range of UI frameworks for building beautiful, responsive forms with Vue. I am using the Vuetify form component for this example.
Ensure each form field is assigned a v-model
directive. This will create two-way bindings between the field and the data model.
<v-text-field v-model="email" required :rules="emailRules" :label="Email" type="email" outlined />
v-text-field
is a Vuetify text field component. It is fine to use a standard HTML input here instead. :rules
is also an attribute for Vuetify components. It contains the name of a function that will be used to validate that specific field. The function returns a bool value to which Vuetify automagically responds by setting relevant styling and warning messages.
If you are not using Vuetify, you can instead use Vue form validation.
The corresponding data model is declared in the Vue data:
section of the script export
block.
<script> export default { data: function () { return { name: null, email: null, phone: null, message: null, }}} </script>
Each model is synchronised with the form field to which it has been assigned. If a user types john smith into the name input field, this value will also be stored within the model. This can be seen by adding the following line somewhere to your page:
<p>Name is: {{ name }}</p>
Saving to the Firestore Database
With the form built and validation implemented, we just need to add a submit handler to call our custom submit function. To do this, add a @ submit
parameter to your form tag and set it's value to the name of your custom function.
<v-form ref="form" v-model="valid" method="POST" lazy-validation @submit="saveContactMessage" >
Now, add the following code to the methods:
area of the export block in your page's script section.
methods: { saveContactMessage: function (e) { e.preventDefault() const messagesRef = this.$firebaseDatabase.collection('message') messagesRef.add( { name: this.name, email: this.email, message: this.message, time: new Date(), }, ) this.name= '' this.email = '' this.message = '' this.submitted = true this.snackbar = false }, }
When a user submits the form, the saveContactMessages()
method is called automatically and the forms event object passed to it. Calling preventDefault()
on the event object, prevents the default behaviour (submitting the form to the default target).
const messagesRef = this.$firebaseDatabase.collection('message')
The above section of code assigns a reference to a database collection with the name message. If you haven’t already created the collection manually, within the firebase cloud console, it will be created automatically when the first set of data is saved.
// Info
// Note that this.$firebaseDatabase is a reference to the instance // property that we set previously in the main.js file. The $ prefix // is a convention used by Vue to indicate a property that's
// available to all instances. This is primarly to avoid naming
// conflicts with local variables.
The add()
method is called on the collection reference to which we pass an object containing the data we wish to save. In my example, we save the contents of the three input fields and a Date
object, so we can refer to the time the message was added in the email that we are going to send out to the form's recipient.
Finally, we reset the form’s data to empty strings and trigger any further UI changes to indicate the form has been submitted. For example, it would be a good idea to implement a try/catch
to pick up any errors during the database save process and show a snackbar alert to confirm success or failure to the user. A common reason why a database write might fail is because the database rules do not allow writes. To fix this, open the Firebase Cloud Console and select the Rules tab while in Database view.
/*
* Important
* Firestore security rules provide access control and validation in
* a code style format. For this example we are giving write
* permission to any successful database connection.
* When building a production app, it’s imperative to fully
* understand how firestore security rules work in order to keep
* your app secure. The above snippet is only an example for
* demonstration purposes.
*/
Submitting the form should now result in the form data being saved to the database. This will appear in the Data tab of the Database sections of the Cloud Console.
Postmark Integration
Now that we have successfuly saved form data to Firestore, the next step is to integrate Postmark so that we can send the details in an email to the website owner.
A functions folder should have already been created during the initial Firebase setup. If this is not the case run firestore init
again to reconfigure.
The functions folder will hold javascript code that is deployed to Firebase Cloud Functions and executed when specified events are triggered. In our case, we’ll be listening to the document.create
event on the message collection we have created. Each time a new message is written to the database our function will be triggered.
Open the package.json
file inside your functions folder. Note, this is a different package file than the one in your project's root folder. It is a specific package file for functions only. Upon opening, you should see some default packages already listed under dependencies. These are firebase-admin
and firebase-functions
. If they are not listed, add them using the links below.
admin: https://www.npmjs.com/package/firebase-admin
functions: https://www.npmjs.com/package/firebase-functions
We also require the nodemailer and postmark transport packages.
Your functions/package.json
file should look something like this:
I have also included a striptags package which I use to strip any HTML that has been submitted within the message textarea. However, depending on your specific security requirements, you will likely need to implement further sanitisation; for example, checking the length of strings, unwanted characters, and so on.
Use the code below as a basis for the index.js
file inside your functions folder.
To avoid storing your Postmark API key in the file, the postmark.key
variable should be set as a Cloud Function environment variable using the firebase command line tool as follows:
firebase functions:config:set postmark.key="API-KEY-HERE"
Otherwise, the most noteworthy line in the file is the function event handler:
exports.forwardMessage = functions.firestore.document('/message/{messageId}').onCreate((snapshot, context){}
The exports
object is where we define our cloud function(s). In this case we are listening to the onCreate
trigger. The function will be called whenever a document matching the given path /message/{messageId}
is created within the database. Cloud Firestore also includes onUpdate
and onDelete
events. As well as an onWrite
, which is triggered when any of the events occur.
Finally, deploy the app to Firebase hosting with the following commands.
vue-cli-service build
firebase deploy --only
firebase deploy
To deploy functions to Google Cloud, use the following:
Navigate to your Firebase Hosting endpoint and test your online form.
Check Postmark account area for any processing errors. Otherwise, the email should be sent successfuly. Further logs can be accessed via the Firebase Cloud Console (within the functions menu and the logs tab).
Conclusion
Firebase offers front end developers the opportunity to focus on the client facing design and development while providing a simple but powerful set of tools to store data and trigger backend functions without ever needing to setup and maintain a server. Serverless development means quick deployments and updates, reduced costs (no server overheads — you only pay for the space of your functions) and built-in scalability (automated resources deployed on demand). Downsides to this approach typically focus on the perceived problem of being tied-in to a specific vendor (Google). This particular issue can be laregly mitigated by using The Repository Pattern for a web app’s database interactions and writing an adapter for Firebase. This means that should a different cloud platform be used in the future, a new adapter could be written, rather than rebuilding the entire app.
Originally published at https://blog.matwright.dev on April 13, 2020.