Nov 29, 2013

AJAX form in Django with jQuery.form plugin

Here I will describe an example form using jQuery Form plugin. It will provide a good frontend, while Django serving a simple backend.
I made this AJAX form using unobtrusive javascript. With human language it means the form will function with JavaScript disabled. And so jQuery.form plugin will work as an AJAX speedup addition and not as a major requirement.

Features:
* Works both with JavaScript  and without.
* Uses standard Django ideology/hooks.
* Uses the most known jQuery plugin for forms AJAX handling.

Theory:
Main idea that Django supports both normal and AJAX request in a standard view. And has a handy request.is_ajax() request method. It returns True/False depending on if request search has HTTP_X_REQUESTED_WITH header for the string 'XMLHttpRequest'. jQuery.form plugin sure does have that.

Backend Django.
We will create an app called 'contact' for our task and place it in the example django project. Urls from the project will redirect to this app. OK. Here is what in our:
urls.py
from django.conf.urls import patterns, url

urlpatterns = patterns('contact.views',
    url(r'^$', 'contact_form', name="contact_form"),
)
Here ve have only one view serving all the needs. and looking at the root of the app. Enough to prove the idea.

forms.py
from django import forms


class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField(max_length=100)
    message = forms.CharField(widget=forms.Textarea(), max_length=1000)
The form is far too simple. But quite sufficient for the concept proof.

views.py
import json

from django.shortcuts import render
from django.http import HttpResponseBadRequest
from django.http import HttpResponse
from django.core.mail import mail_admins
from forms import ContactForm


def contact_form(request):
    if request.POST:
        form = ContactForm(request.POST)
        if form.is_valid():
            # Imaginable form purpose. Post to admins.
            message = """From: %s <%s>\r\nMessage:\r\n%s\r\n""" % (
                form.cleaned_data['name'],
                form.cleaned_data['email'],
                form.cleaned_data['message']
            )
            mail_admins('Contact form', message)

            # Only executed with jQuery form request
            if request.is_ajax():
                return HttpResponse('OK')
            else:
                # render() a form with data (No AJAX)
                # redirect to results ok, or similar may go here 
                pass
        else:
            if request.is_ajax():
                # Prepare JSON for parsing
                errors_dict = {}
                if form.errors:
                    for error in form.errors:
                        e = form.errors[error]
                        errors_dict[error] = unicode(e)

                return HttpResponseBadRequest(json.dumps(errors_dict))
            else:
                # render() form with errors (No AJAX)
                pass
    else:
        form = ContactForm()

    return render(request, 'contact/form.html', {'form':form})
Here we will stop in more detail. We will work out all form possible conditions. e.g. valid/invalid and ajax/plain.
The main form purpose is to actually send a email to admins about contact request for our imaginable site (mail_admins() command and message being created from submitted data).
We have 2 form view conditions here. GET (When we load start of this page) and POST (When we upload the data and parse the form).
In case this view would not have the AJAX part it will look like so:
def contact_form(request):
    if request.POST:
        form = ContactForm(request.POST)
        if form.is_valid():
            # Imaginable form purpose. Post to admins.
            message = """From: %s <%s>\r\nMessage:\r\n%s\r\n""" % (
                form.cleaned_data['name'],
                form.cleaned_data['email'],
                form.cleaned_data['message']
            )
            mail_admins('Contact form', message)
    else:
        form = ContactForm()

    return render(request, 'contact/form.html', {'form':form})
This view works normally with provided template and disabled JavaScript scripts in the template (you may compare it to the first view with AJAX additions). I have not used common possibilities, because I do not need them for demo. It's up to you to decide where to make redirect or so.
Note we are using here request.is_ajax() method and adding another dynamic web page functionality, thus making view behave right for our AJAX needs. And the logic goes into another direction when commented (No AJAX).  Here is where our logic changes go when we do not use jQuery form for request.
Errors here are treated specially. We need the field names of them and the error html to modify for4m in the frontend JavaScript.
Now I hope you understood what is made here. Now let's move on to the templates.
<!DOCTYPE html>
<head>
    <title>Contact</title>
    <script type="text/javascript"
        src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js">
    </script>
    <script type="text/javascript"
        src="http://malsup.github.com/min/jquery.form.min.js">
    </script>
    <script type="text/javascript">
        $(document).ready(function() {
            function block_form() {
                $("#loading").show();
                $('textarea').attr('disabled', 'disabled');
                $('input').attr('disabled', 'disabled');
            }

            function unblock_form() {
                $('#loading').hide();
                $('textarea').removeAttr('disabled');
                $('input').removeAttr('disabled');
                $('.errorlist').remove();
            }

            // prepare Options Object for plugin
            var options = {
                beforeSubmit: function(form, options) {
                    // return false to cancel submit
                    block_form();
                },
                success: function() {
                    unblock_form();
                    $("#form_ajax").show();
                    setTimeout(function() {
                        $("#form_ajax").hide();
                    }, 5000);
                },
                error:  function(resp) {
                    unblock_form();
                    $("#form_ajax_error").show();
                    // render errors in form fields
                    var errors = JSON.parse(resp.responseText);
                    for (error in errors) {
                        var id = '#id_' + error;
                        $(id).parent('p').prepend(errors[error]);
                    }
                    setTimeout(function() {
                        $("#form_ajax_error").hide();
                    }, 5000);
                }
            };

            $('#ajaxform').ajaxForm(options);
        });
    </script>
    <style>
        #form_ajax_error, .errorlist {
            color: red;
        }
    </style>
</head>
<body>
    <h1>Contact</h1>
    <form id="ajaxform" action="{% url contact_form %}" method="post">
        {% csrf_token %}
        {{ form.non_field_errors }}
        {{ form.as_p }}
        <div id="loading" style="display:none;">
            <span id="load_text">loading...</span>
        </div>
        <div id="form_ajax" style="display:none;">
            <span>Form submit successfully.</span>
        </div>
        <div id="form_ajax_error" style="display:none;">
            <span>Can not submit data due to errors.</span>
        </div>
        <p id="sendwrapper"><input type="submit" value="Send" id="sendbutton"/></p>
    </form>
</body>
</html>
Here I have all the script encoded for show purposes. However it is a bad practice. You should not do it in a real life project.
Here we render the form with default django tags and add 3 conditional form help text messages.
They will display and lightly fade after 5 seconds. Saying either script have submitted/loading/errors returned state.
They are controlled with the jQuery script. We also disabling the entire form with all the elements while AJAX is working. SO user can not change the data accidently.
The plugin itself is binded to the form with jQuery command $('#ajaxform').ajaxForm(options). It uses options object created earlier. Here we have 3 condition handlers, as functions. We are locking the form and displaying the loading text (beforeSubmit), submit succeeded (success) and the most interesting one is error handling function (error). It parses JSON provided by the backend in case of errors. Then modifying the DOM with provided errors, relying on the django form rendering logic to find the "id" of the HTML input that needs an error prepended.
Our form will look like so:

I have collected everything in a git repository. You can clone/copy and run it for yourself. The app has some hooks for debug to slow down request/response. So you will be able to see loading state, etc...
Repository with working copy from this app is here:
https://github.com/garmoncheg/ajax_form_example

Comments? Suggestions?

5 comments:

  1. Hi ! Thanks for this useful post !
    Could you give example on how to use this plugin to upload file or images ? I've tried on your example by adding simply a fileField to ContactForm, but when i submit the form, i got error msg.
    Thanks in advance for your help

    ReplyDelete
    Replies
    1. I know the answer to your question.it is required to fix that error with msg...

      Please write down your error msg to ask... I do not have superpowers to guess...

      Delete
    2. Hi. Thanks for your reply !
      The error msg happens after i press the submit button and the text is in french and is the following : 'ce champs est obligatoire' (juste on top of the file upload button), which i can translate in english as : 'this field is required'.
      I notice that the error is at the javascript level as my django view is not reached...

      Delete
    3. Hi,
      just to let you know i succeeded to solve my prob. I just tried with adding directly a file input element into the html (input type="file" size="60" name="upload") instead of adding it into the django form. After that, i saved the uploaded picture in my view with the following code :
      if request.is_ajax():
      user_profile = request.user.get_profile()
      user_profile.picture = request.FILES['upload']
      user_profile.save()

      Again, thanks for your tutorial. It really helped me...

      Delete
    4. Glad it helped. Asking right question is almost 50% of all the work ;)

      Delete