HTML forms belong among basic components of almost any website or web application. Earlier or later you as a Django developer are going to be challenged to code some Django forms mostly advance ones and because of that, you need to understand how using Django forms. In a real-life scenario, you will have to deal with validation of submitted data, displaying user feedback and error outputs, ensure basic form security and design a proper user workflow through the form with good UX. Fortunately, Django offers many libraries and tools to make form coding easier and faster.
After reading this post you are going to have a basic overview howto build a form with Django with Ajax and Boostrap technology from scratch step by step. Let’s create a good one contact form that is a mandatory part of almost every a company or landing website that you can use it as inspiration for any real-world cases.
Our goal – build a nice contact form with Django 3, Ajax and Boostrap 4
In a few steps I am going to show you how to build a nice contact form in Django from scratch without using class ModelForm
.
The form will use Ajax technology for non-blocking submit processing and smooth user experience. Each form fied will be rendered by template tag because of reusability and better code clarity. We will also use CSRF protection as template tag {% csrf_token %}
for security reason. Processing of form events will be handled with the Javascript jQuery library and using CSS framework Bootstrap 4 will give us an acceptable good-looking form even though we are not graphic designers.
To successful form submiting, you have to fill out all four fields name, e-mail, phone number, message
. For more fun and study purposes I decided to meet unusual conditions. An e-mail has to arrive from the domain example.com
, a phone number has to meet the phone format +420 609 123 456
and the message have to include a word hello
, because we are polite people, aren’t ? 🙂
For form fields we will need two types of HTML input tags – text, textarea
. So decided to generate them a little different way. I will use Django custom tags to simplify future changes. Check out more about Django custom template tags.
Form validation will be handled on a server-side. Each field error messages will be displayed under an appropriate field input and a non-field error message will be shown at the top of the form. After successful submission and validation the form will be sent the info-email containing all those form fields including the message.
This will be the result of our effort. On the left picture is the look of the final form and on the right picture is the final form too but with errors displayed.
At the moment we have gathered all the needed information for our work. Let’s make Django forms better again. 🙂
Prerequisites
You have to manage basics of HTML, CSS, Python 3 and Django. You need be able to create the Python 3 virtual environment and start the brand new Django project. You also need a functional Linux machine with the Internet connection.
What we are going to do
- establish a new Python 3.8 virtual environment
- create a structure of the brand new Django 3 project with SQLite database and configured mail server on localhost e.g Postfix
- the file
template/__default.html
define the main project template - the file
template/form_contact.html
define the form template - the file
templatetags/mytemplatetags.py
define template tags for field rendering - the file
forms.py
define the form object for our contact form - the files
views.py, ajax.py
define view functions for a form processing and validation including sending email functionality - define the jQuery code for handling form events
- the file
urls.py
defining an proper url routing
Step 1 – setup the Python virtual environment and the Django project structure for the application ContactForm
Be sure that you have installed Python 3.8 ( 3.7 is enough as well) in your Linux box in the directory /opt/python38
. How to do that check out my tutorial for installing Python 3.8 to Centos Linux . Create new Django project with Python virtual environment and start Django developer server. In my case I am going to make all the work in the directory /home/hanz/projects/django-contact-form
.
[hanz@myserver]# mkdir -p /home/hanz/projects/django-contact-form/ [hanz@myserver]# cd /home/hanz/projects/django-contact-form/ [hanz@myserver]# sudo /opt/python38/bin/python -m venv /home/hanz/projects/django-contact-form/env [hanz@myserver]# source /home/hanz/projects/django-contact-form/env/bin/active (env) [hanz@myserver django-contact-form]# pip install --upgrade pip (env) [hanz@myserver django-contact-form]# pip install django (env) [hanz@myserver django-contact-form]# python -m django --version >>> 3.0.4 (env) [hanz@myserver django-contact-form]# django-admin startproject contactform (env) [hanz@myserver django-contact-form]# ls -l >>> total 8 >>> drwxr-xr-x 3 hanz hanz 4096 Feb 5 17:41 contactform >>> drwxr-xr-x 5 hanz hanz 4096 Feb 5 17:31 env (env) [hanz@myserver django-contact-form]# cd contactform/ (env) [hanz@myserver contactform]# python manage.py runserver >>> Performing system checks... >>> >>> System check identified no issues (0 silenced). >>> >>> You have unapplied migrations; your app may not work properly until they are applied. >>> Run 'python manage.py migrate' to apply them. >>> >>> Django version 3.0.4, using settings 'contactform.settings' >>> Starting development server at http://127.0.0.1:8000/ >>> Quit the server with CONTROL-C.
All the form HTML input tags we will generate with Django template tags because we want to have all the stuff on one place. Bootstrap 4 styling is much more easier and we and we also stick with the design principle DRY. We also have to take care of common HTML attributes such as label, type, required, placeholder, disabled, autocomplete, rows, cols
and some helper HTML classes. Don’t forget to mark the output string as the safe with Django helper utility mark_safe(html)
.
from django.template import Library from django.utils.safestring import mark_safe register = Library() @register.simple_tag def b4_form_field(form, field_name, **kwargs): field_obj = form[field_name] field_attrs = form.fields[field_name] type = kwargs.get('type', 'text').lower() # default input type is <input type="text"> autocomplete = kwargs.get('autocomplete', 'on').lower() # default input autocomplete is "on" label_for = field_obj.id_for_label label_text = field_obj.label id_ = field_obj.id_for_label name = field_obj.name value = field_obj.value if isinstance(field_obj.value, str) else '' autocomplete = "on" if autocomplete == 'on' else 'off' required = "required" if field_attrs.required else '' disabled = "disabled" if field_attrs.disabled else '' placeholder = field_attrs.widget.attrs['placeholder'] if field_attrs.widget.attrs['placeholder'] else '' help_text = f"<small class='form-text text-muted'>{field_obj.help_text}</small>" if field_obj.help_text else '' if type == 'textarea': text_area_rows = field_attrs.widget.attrs.get('rows', 40) text_area_cols = field_attrs.widget.attrs.get('cols', 5) html = f""" <div class="form-group"> <label for="{label_for}">{label_text}</label> <textarea class="form-control" autocomplete="{autocomplete}" type="{type}" id="{id_}" name="{name}" placeholder="{value}" rows="{text_area_rows}" cols="{text_area_cols}" {disabled} {required}>{value}</textarea> {help_text} </div> """ else: html = f""" <div class="form-group"> <label for="{label_for}">{label_text}</label> <input class="form-control" autocomplete="{autocomplete}" type="{type}" id="{id_}" name="{name}" value="{value}" placeholder="{placeholder}" {disabled} {required}> {help_text} </div> """ return mark_safe(html)
Step 3 – the form class definition
Django form definition the file forms.py
is pretty straightforward. We create a subclass ContactForm
of the forms.Form
. Purposely we don’t use the approach including class form.ModelForm
because we don’t need to save form data into a database. After all the validation checks and cleaning, we will send all the customer data to e.g. our company service e-mail.
Take a look closely at all the clean form methods, where we handle our “weird” conditions and provide raising Validation exception or adding error message for non-field errors. For clarity I just recall that we have to accomplish that an e-mail has to arrive from the domain example.com
, a phone number has to meet the phone format +420 609 123 456
and the message have to include a word hello
.
import re from django import forms class ContactForm(forms.Form): NEEDED_WORD = "hello" ALLOWED_DOMAIN = "example.com" RE_PHONE_PATTERN = '^((\+\d{3}|\d{5}) ?)?\d{3}( |-)?\d{3}( |-)?\d{3}$' name = forms.CharField(label="Your name", widget=forms.TextInput(attrs={"placeholder": "John Smith"}), max_length=80, required=True, localize=False, disabled=False, help_text="Your name.") email = forms.EmailField(label="E-mail", widget=forms.TextInput(attrs={"placeholder": "John.Smith@your-email.com"}), max_length=80, required=True, localize=False, disabled=False, help_text="Your e-mail.") phone_number = forms.CharField(label="Phone number", widget=forms.TextInput(attrs={"placeholder": "+420 609 123 456"}), max_length=80, required=False, localize=False, disabled=False, help_text="Your phone number in the format +420 609 123 456.") message = forms.CharField(label="Message", widget=forms.Textarea(attrs={'rows': 7, 'cols': 45, "placeholder": "Write your message here."}), max_length=315, required=True, localize=False, disabled=False, help_text="Your message.") def clean_email(self): cleaned_data = super().clean() email = cleaned_data.get("email") if email: email = email.lower() else: raise forms.ValidationError("This field is required.", code="required") if not email.endswith(ContactForm.ALLOWED_DOMAIN): raise forms.ValidationError(f"The e-mail has to come from the domain \"{ContactForm.ALLOWED_DOMAIN}\".", code="invalid") return email def clean_phone_number(self): cleaned_data = super().clean() phone_number = cleaned_data.get("phone_number") if not phone_number: raise forms.ValidationError("This field is required.", code="required") if not re.search(ContactForm.RE_PHONE_PATTERN, phone_number): raise forms.ValidationError("This field has not the correct phone format \"+420 609 123 456\" or contain an invalid characters.", code="invalid") return phone_number def clean_message(self): cleaned_data = super().clean() message = cleaned_data.get("message") if message: message = message.lower() else: raise forms.ValidationError("This field is required.", code="required") if not ContactForm.NEEDED_WORD in message: raise forms.ValidationError(f"The message has to contain the word \"{ContactForm.NEEDED_WORD}\".", code="invalid") return message def clean(self): cleaned_data = super().clean() email = cleaned_data.get('email') message = cleaned_data.get('message') if email and message: message = message.lower() email = email.lower() # do something if both fields are valid so far. if not email.endswith(ContactForm.ALLOWED_DOMAIN) and not ContactForm.NEEDED_WORD in message: if not email.endswith(ContactForm.ALLOWED_DOMAIN): self.add_error("email", f"This field e-mail has to come from the domain \"{ContactForm.ALLOWED_DOMAIN}\".") if not ContactForm.NEEDED_WORD in message: self.add_error(f"message", f"This field message has to contain the word \"{ContactForm.NEEDED_WORD}\".") raise forms.ValidationError(f"The e-mail has to come from the domain \"{ContactForm.ALLOWED_DOMAIN}\" and the message has to contain the word \"{ContactForm.NEEDED_WORD}\".", code="invalid")
Step 4 – the main project template
There is not something special about the main project template __default.html
. It contains some CSS styles for Bootstrap 4, link to the Javascript jQuery library, an including tag for the contact form template and “thank you” message div showed after successfully form submitting.
<!DOCTYPE html> <html lang="en" class="h-100"> <head> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.4.1.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" crossorigin="anonymous"></script> </head> <style> html, body { background: -webkit-linear-gradient(top, #95c8ff, #dbebfa); letter-spacing: 0.3px; } form ul { font-size: 14px; } form ul li:not(:last-child) { margin-bottom: 1em; } form ::placeholder { color: #a9b4c2 !important; } </style> <body class="h-100"> <div class="container h-100"> <div class="row h-100 py-5 px-1 justify-content-center align-items-center"> <div id="form-wrapper" class="col-12 col-sm-10 col-md-8 col-lg-6 pt-5 bg-light rounded-lg shadow"> <h2 class="text-center mb-4">Contact form</h2> <form role="form" class="pb-3 px-4" method="post" action="" autocomplete="on" novalidate> {% include 'forms/form_contact.html' with form=form %} </form> <div id="message-done" class="text-center mt-5" style="display: none;"> Thank you for your message.<br> I am going to answer you as soon as possible. </div> </div> </div> </div> </body> </html>
Step 5 -the form template with the jQuery code
The form template form_contact.html
is pretty simple too. The HTML5 form attribute novalidate
ensures that the default browser validation will be disabled. We don’t need this due to our custom check on the server-side. We must not forget adding of CSRF tag {% csrf_token %}
for the security reason. All the form fields are generated by template tags as we discussed above. The jQuery code is more complex because we have to handle data form extraction, the form AJAX submitting with JSON payload and process returned data from the server like is showing error messages and displaying “Thank you message” after success form submitting. Surely the jQuery code can be better.
{% load mytemplatetags %} <form role="form" method="post" action="" novalidate> <div class="alert alert-danger" style="display: none;"> <ul class="list-unstyled mb-0 py-1"> </ul> </div> <fieldset> {% csrf_token %} {% b4_form_field form=form field_name="name" type="text" %} {% b4_form_field form=form field_name="email" type="text" %} {% b4_form_field form=form field_name="phone_number" type="text" %} {% b4_form_field form=form field_name="message" type="textarea" %} <div class="form-group"> <button type="reset" class="btn btn-secondary mr-2">Reset</button> <button type="submit" class="btn btn-success">Send</button> </div> </fieldset> </form> <script> $(document).ready(function () { let field_error_template = $("<div class='invalid-feedback'></div>'"); const form_wrappper = $("#form-wrapper"); const form = form_wrappper.find('form'); const message_done = $("#message-done"); let non_field_alert = form.find("div.alert"); function formRemoveError() { form.find(":input.is-invalid").removeClass('is-invalid'); form.find("div.invalid-feedback").remove(); non_field_alert.find("ul").empty(); non_field_alert.hide(); {#form.removeClass('was-validated');#} } function formAddFieldError(form_errors) { if ("__all__" in form_errors) { $.each(form_errors["__all__"], (i, value) => { non_field_alert.find("ul").append($("<li>" + value["message"] + "</li>")); }); non_field_alert.show(); } $.each(form_errors, (field, errors) => { if (field !== "__all__") { let input = $(':input[name=' + field + ']', form); if (input) { input.addClass('is-invalid'); $.each(errors, (i, value) => { input.after(field_error_template.clone().text(value.message)); }); } } }); } function formClear() { form.find(':input').val(''); } form.on('reset', () => formRemoveError()); $("[type='submit']", form).on('click', (event) => { event.preventDefault(); const formData = new FormData(form.get(0)); let jqxhr = $.ajax( { url: "{% url 'ajax_contact_form' %}", data: formData, method: 'post', dataType: 'json', cache: false, contentType: false, // Important! processData: false, // Important! }); jqxhr.done((server_data) => { formRemoveError(); if (server_data.form_is_valid) { form_wrappper.height(form_wrappper.height()).width(form_wrappper.width()); form_wrappper.fadeOut(1000, () => { form.hide(); message_done.show(); formClear(); }).fadeIn(1000).delay(2500).fadeOut(1000, () => { message_done.hide(); form.show(); form.trigger("reset"); }).fadeIn(1000); } else { formAddFieldError(JSON.parse(server_data.form_errors)); } }); jqxhr.fail(() => Console.log("Error during processing form with ID: " + form.val('id')) ) }); }); </script>
Step 6 – the Ajax form handling in files views.py, ajax.py
The view default
has only a purpose to generate the main template __default.html
with a new instance of form class ContactForm
. Nothing else to do.
from django.shortcuts import render from web.forms import ContactForm def default(request): return render(request, '__default.html', {'form': ContactForm()})
The view contact_form
handles incoming Ajax POST requests from the form, makes the form validation and any form errors send back to the browser in JSON format then the jQuery shows an error CSS class on the appropriate form field. The valid form data are filled into the template string and send to selected into e-mail via the Django mail sending interface send_email()
. Make sure to correctly setup your email server.
from django.core.mail import send_mail from django.http import Http404, JsonResponse from web.forms import ContactForm def contact_form(request): EMAIL_TO = "info@your-company-email.com" EMAIL_FROM = "contact-form_no-reply@your-email.com" SUBJECT = "The message from a contact form" if not request.is_ajax(): raise Http404('No ajax!') else: form = ContactForm(request.POST or None) if form.is_valid(): message = form.cleaned_data['message'] phone_number = form.cleaned_data['phone_number'] email = form.cleaned_data['email'] name = form.cleaned_data['name'] email_content = f"""Name: {name} Phone: {phone_number} Email: {email} ------------------------------------------- {message} """ send_mail(SUBJECT, email_content, EMAIL_FROM, [EMAIL_TO]) # print(email_content) r = {'form_is_valid': True} else: r = {'form_is_valid': False, 'form_errors': form.errors.as_json()} return JsonResponse(r)
Step 7 – the URL routing settings
In the URL Django dispatcher, we only take care of two URL paths /
and /ajax/contact-form/
with correct mapping to views. Take a note at the URL naming that we use throughout the project.
from django.urls import path from web import views as web_views from web import ajax as web_ajax urlpatterns = [ path('', web_views.default, name='default'), path('ajax/contact-form/', web_ajax.contact_form, name='ajax_contact_form'), ]
Step 8 – the form is alive
Here is the result of our great efforts. The Django contact form works. Good job all.
Final thoughts
Congratulations!
You’ve reached the end!
At this point, you have some basic insight about creating a form in Django with Ajax from scratch.
I hope you enjoyed this guideline and you learned something new. If you have some tips for improvements or found a mistake, give me a shout in comments.
Enjoy!
Hanz
Great tutorial for creating objects through ajax. Thans for sharing. I have a question, How would you handle edit an object with this method?
I assume you mean the inline form editing, but it is not solved in my example.