Appearance
How to Make a Responsive Contact Form Using Vue.js and Go β
Written by: Δ°rem Kuyucu
Having a contact form on your website can make life easier for some of your clients. That's why in this post we'll show you how we made ours. And no, we won't use one of those SaaS form handling, potentially GDPR violating, privacy nightmare products. You will have the control of your data π«΅ and you will need to host the backend somewhere, like Digilol managed servers.
The Frontend β
We implemented two components for the contact form: the form itself and a modal pop-up.
Form Component β
We use Vitepress for our website so we used its button component and theme as well.
Here is the code for the contact form component. Omitted some irrelevant parts because it originally contained a vertical divider and alternative contact method logos.
DGContact.vue:
jsx
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { VPButton } from 'vitepress/theme'
import DGModal from './DGModal.vue'
const props = defineProps<{
formEndpoint: string
}>()
</script>
<script lang="ts">
import axios from 'axios'
export default {
data() {
return {
form: {
name: '',
email: '',
service: '',
message: ''
},
modalOpen: false,
result: '',
altEmail: ''
}
},
methods: {
async submit() {
axios.post(this.formEndpoint, this.form)
.then(response => {
this.modalOpen = true;
this.result = "Received! Thank you."
this.altEmail = '';
})
.catch(error => {
this.modalOpen = true;
this.altEmail = `mailto:hello@domain.tld?subject=${this.form.service}&body=${this.form.message}`
if (error.request.status == 429) {
this.result = "Slow down! You made too many requests, try again later."
} else {
this.result = "Something went wrong. Please contact us directly or try again later.";
}
});
}
}
}
</script>
Here we use Axios to make a POST request to our backend on submit. Also display a modal saying received on success and on failure the modal displays an additional button that launches an email client when clicked.
jsx
<template>
<div id="contact" class="contact-section">
<div class="contact-content">
<h2 class="contact-title">Contact us</h2>
<div class="contact-wrapper">
<div class="contact-form">
<DGModal :show="modalOpen" @close="modalOpen = false" :altEmail="altEmail">
<template #header>
<h3>{{ result }}</h3>
</template>
</DGModal>
<form @submit.prevent="submit">
<p>Leave us a message and we'll respond within 48 hours.</p>
<div class="form-element top name-container">
<label for="name">Name</label>
<input type="text" name="name" id="name" placeholder="Name Surname" v-model="form.name" required />
</div>
<div class="form-element top email-container">
<label for="email">Email</label>
<input type="email" name="email" id="email" placeholder="name@example.net" v-model="form.email" required />
</div>
<div class="form-element">
<label for="service-selector">I am interested in</label>
<select id="service-selector" name="service" v-model="form.service" required>
<option value="" disabled>Pick a service</option>
<option value="Software Development">Software Development</option>
<option value="Consulting">Consulting</option>
</select>
</div>
<div class="form-element">
<label for="message">Message</label>
<textarea id="message" name="message" v-model="form.message" placeholder="Dear Digilol Team, Is smoking bad for my computer?" required></textarea>
</div>
<div class="form-element submit">
<VPButton text="Submit" />
</div>
</form>
</div>
...
Modal Pop-up Component β
On error or success this is component that will be displayed over the form.
DGModal.vue:
jsx
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { VPButton } from 'vitepress/theme'
const props = defineProps<{
show: Boolean
altEmail?: string
}>()
</script>
<template>
<Transition name="modal">
<div v-if="show" class="modal-mask">
<div class="modal-container">
<div class="modal-content">
<div class="modal-header">
<slot name="header" />
</div>
<div class="modal-body">
<slot name="body" />
</div>
<div class="modal-footer">
<slot name="footer">
<VPButton text="Close" @click="$emit('close')" />
<VPButton v-if="altEmail" text="Email directly" @click="$emit('close')" :href="altEmail" />
</slot>
</div>
</div>
</div>
</div>
</Transition>
</template>
<style>
.modal-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--vp-c-bg-soft);
display: flex;
transition: opacity 0.3s ease;
}
.modal-container {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
padding: 20px 30px;
border-radius: 5px;
border: 1px solid var(--vp-button-brand-active-bg);
background-color: var(--vp-c-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.modal-content {
display: block;
}
.modal-header h3 {
margin-top: 0;
}
.modal-body {
margin: 20px 0;
}
...
This component uses Vue.js animations (fade-in/out) like so:
css
.modal-enter-from {
opacity: 0;
}
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>
Register a Global Component in Vitepress β
If you use Vitepress like us, you will need to register the contact form component like so:
.vitepress/theme/index.ts:
ts
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './style.css'
import DGContact from './components/DGContact.vue'
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
})
},
enhanceApp({ app, router, siteData }) {
app.component('DGContact', DGContact)
}
} satisfies Theme
Now you should be able to use the DGContact
component like so:
html
<DGContact formEndpoint="https://contact-form.digilol.net/form" />
The Backend β
For the backend we will write a small Go program that launches an HTTP server to handle form requests and forward them to an email address.
main.go:
go
package main
import (
"bytes"
"encoding/json"
"flag"
"html/template"
"log"
"net/http"
"os"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/httprate"
mail "github.com/xhit/go-simple-mail/v2"
"gopkg.in/yaml.v3"
)
type Config struct {
Server struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
Timeout struct {
Server time.Duration `yaml:"server"`
Write time.Duration `yaml:"write"`
Read time.Duration `yaml:"read"`
Idle time.Duration `yaml:"idle"`
} `yaml:"timeout"`
} `yaml:"server"`
Cors struct {
AllowedOrigins []string `yaml:"allowed-origins"`
} `yaml:"cors"`
RateLimit struct {
Requests int `yaml:"requests"`
Duration time.Duration `yaml:"duration"`
} `yaml:"rate-limit"`
Email struct {
SMTP struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Encryption string `yaml:"encryption"`
Timeout struct {
Connect time.Duration `yaml:"connect"`
Send time.Duration `yaml:"send"`
} `yaml:"timeout"`
} `yaml:"smtp"`
Sender string `yaml:"sender"`
Recipient string `yaml:"recipient"`
TemplatePath string `yaml:"template-path"`
} `yaml:"email"`
}
type formRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Service string `json:"service"`
Message string `json:"message"`
}
var (
cfg *Config
emailTemplate *template.Template
smtpServer *mail.SMTPServer
)
func sendEmail(form formRequest) error {
var b bytes.Buffer
err := emailTemplate.Execute(&b, form)
if err != nil {
return err
}
email := mail.NewMSG()
email.SetFrom(cfg.Email.Sender).
AddTo(cfg.Email.Recipient).
SetReplyTo(form.Name + " <" + form.Email + ">").
SetPriority(mail.PriorityHigh).
SetSubject("Contact form inquiry from " + form.Name)
email.SetBody(mail.TextHTML, b.String())
if email.Error != nil {
return email.Error
}
smtpClient, err := smtpServer.Connect()
if err != nil {
return err
}
return email.Send(smtpClient)
}
// On form submit
func formHandler(w http.ResponseWriter, r *http.Request) {
var f formRequest
err := json.NewDecoder(r.Body).Decode(&f)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if f.Name == "" || f.Email == "" || f.Service == "" || f.Message == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
err = sendEmail(f)
if err != nil {
log.Println("Failed to email:", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
// Parses YAML config
func newConfig(configPath string) (*Config, error) {
c := &Config{}
f, err := os.Open(configPath)
if err != nil {
return nil, err
}
defer f.Close()
d := yaml.NewDecoder(f)
if err := d.Decode(&c); err != nil {
return nil, err
}
return c, nil
}
func main() {
var cfgPath string
flag.StringVar(&cfgPath, "config", "config.yaml", "path to configuration file")
flag.Parse()
var err error
cfg, err = newConfig(cfgPath)
if err != nil {
log.Fatal(err)
}
emailTemplate, err = template.ParseFiles(cfg.Email.TemplatePath)
if err != nil {
log.Fatal("Failed to parse email template:", err)
}
// You need an SMTP server to send emails.
smtpServer = mail.NewSMTPClient()
smtpServer.Host = cfg.Email.SMTP.Host
smtpServer.Port = cfg.Email.SMTP.Port
smtpServer.Username = cfg.Email.SMTP.Username
smtpServer.Password = cfg.Email.SMTP.Password
switch cfg.Email.SMTP.Encryption {
case "None":
smtpServer.Encryption = mail.EncryptionNone
case "SSL/TLS":
smtpServer.Encryption = mail.EncryptionSSLTLS
case "STARTTLS":
smtpServer.Encryption = mail.EncryptionSTARTTLS
}
smtpServer.ConnectTimeout = cfg.Email.SMTP.Timeout.Connect
smtpServer.SendTimeout = cfg.Email.SMTP.Timeout.Send
r := chi.NewRouter()
r.Use(middleware.Heartbeat("/"))
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Route("/form", func(r chi.Router) {
r.Use(httprate.LimitByIP(cfg.RateLimit.Requests, cfg.RateLimit.Duration))
r.Use(cors.Handler(cors.Options{
AllowedOrigins: cfg.Cors.AllowedOrigins,
AllowedMethods: []string{"POST"},
}))
r.Use(middleware.AllowContentType("application/json"))
r.Post("/", formHandler)
})
hs := &http.Server{
Addr: cfg.Server.Host + ":" + cfg.Server.Port,
Handler: r,
ReadTimeout: cfg.Server.Timeout.Read * time.Second,
WriteTimeout: cfg.Server.Timeout.Write * time.Second,
IdleTimeout: cfg.Server.Timeout.Idle * time.Second,
}
log.Fatal(hs.ListenAndServe())
}
We can specify a template file for the emails that will be sent:
email.html:
html
<html>
<body>
<p>Contact form inquiry.</p>
<p>
<b>Name:</b> {{ .Name }}<br>
<b>Email:</b> {{ .Email }}<br>
<b>Service:</b> {{ .Service }}
</p>
<p>{{ .Message }}</p>
</body>
</html>
And also a configuration file in YAML format that allows to modify many parameters:
config.yaml:
yaml
server:
host:
port: 8080
timeout:
server:
write:
read:
idle:
cors:
allowed-origins:
- https://www.digilol.net
rate-limit:
requests: 4
duration: 12h
email:
smtp:
host: mail.domain.tld
port: 465
username: hello@domain.tld
password: s3cret
encryption: SSL/TLS
timeout:
connect: 30s
send: 30s
sender: Digilol Contact Form <hello@domain.tld>
recipient: info@domain.tld
template-path: email.html
License β
All of the code included in this post is licensed under The GNU General Public License v3.0. The license text is available here.