How to add a contact form using Django and htmx
In this short guide, you will learn how to add a contact form to your app. This guide is tailored for Vanty Starter kit users, but the concepts here may also be applied to other Django apps.
The contact will be form to capture basic information about potential clients and send it to the configured inbox. We will use the htmx library to make the UI modern/responsive.
Prerequisites
-
Vanty Starter Kit - This guide makes reference to libraries and modules that are already bundled with the starter kit. This includes:
- django_htmx
- emailing utilities and templates
- base templates for the contact page
Other than that, you should be able to follow along and apply the same principles to your project. Let's get started.
Set up the views
views.py
from apps.common.emails import send_generic_email
@require_http_methods(["GET"])
def contact_form_view(request):
template_name = "web/contact_page.html"
if request.htmx:
template_name = "web/htmx_contact_form.html"
if request.method == "GET":
form = ContactForm()
context = {"contact_form": form}
return render(request, template_name, context)
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
send_generic_email(
settings.DEFAULT_EMAIL,
'contacts/contact',
ctx_data=form.cleaned_data
)
response = render(request, "web/htmx_contact_confirm.html")
trigger_client_event(
"notice",
{"type": "success", "message": "Message sent successfully"},
after="swap",
)
return response
else:
return render(
request,
template_name,
context={"contact_form": form}
)
Notes
This is a simple view that either returns a blank form for GET requests or a validated form for POST requests.
-
if
request.htmx
: - set a different template name when the request is htmx. The htmx attribute is added by thedjango_htmx
library middleware which is bundled with the starter kit. -
send_generic_email
- we call this method to send an email to the DEFAULT_EMAIL address included in config/settings/base.py. Please see the docs on more info on how to set up emails. -
trigger_client_event
- successfully validated forms will trigger a client event of type 'notice' to give the user some feedback.
Add the forms and urls
forms.py
class ContactForm(forms.Form):
full_name = forms.CharField(max_length=400)
email = forms.EmailField(required=True)
message = forms.CharField(
widget=forms.Textarea(
{"rows": 4, "placeholder": "Tell us more about your product"}
)
)
def clean_email(self):
email = self.cleaned_data["email"]
# you can black list certain emails here, e.g. span accounts etc
for domain in settings.ACCOUNT_EMAIL_DOMAIN_BLACKLIST:
if domain in email:
raise forms.ValidationError(
f"{email} has been blacklisted. "
f"Please use a valid email or get in touch with us"
)
return email
config/urls.py
urlpatterns = [
...
path("contact-us/", contact_form_view, name="contact-form"),
....
]
Notes
The form is a standard django form with a small modification
def clean_email(self)
- In addition to validating the email field, the clean email method also checks that the email is not in the list blacklisted emails. This is important if you expect a lot of traffic from this page. If you are still getting a lot of span consider using the
django_ratelimit
library on the view.
Add the form templates
web/contact_page.html
{% extends "layouts/landing.html" %}{% load static i18n vanty_tags %}
{% block title %} Vanty Demo | Contact {% endblock %}{% endblock %}
{% block theme %}data-theme="light"{% endblock %}
{% block promo_banner %}{% endblock %}
{% block page_content %}
<div class="bg-white container md:mx-auto max-w-[65ch] my-2 p-2 md:p-0">
<div id="contact-form">
<div class="overflow-hidden">
<section class="relative bg-white" aria-labelledby="contact-heading">
<div class="absolute w-full h-1/2 bg-warm-gray-50" aria-hidden="true"></div>
<div>
<div class="relative bg-white">
<h2 id="contact-heading" class="sr-only">Contact us</h2>
{% include "web/htmx_contact_form.html" %}
</div>
</div>
</section>
</div>
</div>
</div>
{% include "shared/footer/main_footer.html" with is_footer_white=True %}
{% endblock %}
web/htmx_contact_form.html
{% load crispy_forms_tags heroicons %}
<div class="flex justify-between items-center">
<h3 class="text-lg font-medium text-warm-gray-900">Get in touch</h3>
</div>
<div class="mt-6 px-3">
<form novalidate
hx-post="{% url "contact-form" %}"
hx-target="#contact-form">
{% csrf_token %}
{{ contact_form|crispy }}
<div class="sm:col-span-2 sm:flex sm:justify-end mt-5">
<button type="submit" class="btn btn-sm">
Send
</button>
</div>
</form>
</div>
Notes
-
web/
- both the main contact page and form are in a folder called web. You can place them where ever you prefer. Just remember to update the view accordingly. -
{% extends "layouts/landing.html" %}
- the contact page extends the landing.html template. This template already includes the dependencies you need such as the htmx library. -
<form novalidate .
.. - pay attention to the form attributes. Notice how the action attribute is replaced byhx-post
="". This tells htmx where to post the contact form to. Thehx-target="#contact-form"
tag instructs htmx to swap out the contents of the element with id #contact-form with the response from the server.
Email templates
templated_email/contacts/contact.html
{% extends "templated_email/base_email.html" %}
{% block content %}
<p>Message from {{full_name}}, {{email}}</p>
<p>{{ message }}</p>
{% endblock %}
{% block button %}
<a href="{{ domain | safe }}" target="_blank">Dashboard </a>
{% endblock %}
templated_email/contacts/contact
{% load i18n %}
{% block subject %}
{% blocktrans trimmed context "New Contact" %}
Contact Email
{% endblocktrans %}
{% endblock %}
{% block plain %}
Message from {{full_name}} {{email}}
{{ message }}
{{ domain }}
{% blocktrans trimmed context "Base email text" %}
This is an automatically generated e-mail, please do not reply.
{% endblocktrans %}
{% blocktrans trimmed context "Base email footer" %}
Sincerely, {{ site_name }}
{% endblocktrans %}
{% endblock %}
{% block html %}
{% include 'templated_email/contacts/contact.html' %}
{% endblock %}
Notes
- Finally we add the email templates. Be sure to include both the plain text and html template for emails. The base templates have already been set up for you.
Conclusion
That's it. The emails should be sent to the default email you included in your settings. To test this is working, check the development mailbox (mailhog) if you are using the docker.