AJAX Form Submission in Django



Here is a simple example of using the jQuery javascript framework to hijack and submit a Django form using AJAX. Using unobtrusive javascript to hijack the form's submit event allows for a progressive enhancement web design strategy. In other words, this form degrades gracefully for users who do not have javascript. Moreover, a minimalistic approach is used for the AJAX code so as not to add too much additional maintenance to the project.
A simple contact form makes for a great example. The video below shows how the final contact form works once the AJAX has been added. (I used a 2 second delay in the responses to illustrate the effect).

Strategy

  • Create a normal form with a traditional submission.
  • Design the templates with AJAX in mind from the very start.
  • Implement AJAX at the very end.
  • Use unobtrusive Javascript.
  • Avoid repeating logic in the Javascript.
So here is how this will work: The submit event on a traditional form is "hijacked" using some unobtrusive javascript. The form is then submitted using an XMLHttpRequest instead of the traditional request. The response to either type of request is virutally the same. The javascript replaces the HTML form on the page with the HTML form in the response.

The Contact Form

Like I said, the form is simple.
forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField(max_length=100)
    subject = forms.CharField(max_length=200)
    message = forms.CharField(widget=forms.Textarea(), max_length=500)

URL Configuration

There are simply two URL patterns: a form page and a success page. I am using named URL patterns which will make it easy to link or redirect to them later.
urls.py
from django.conf.urls.defaults import patterns, url
from django.views.generic.simple import direct_to_template

urlpatterns = patterns('contact.views',
    url(r'^contact/$', 'contact_form', name="contact_form"),
    url(r'^contact/success/$', direct_to_template, {'template': 'contact/success.html'},
        name="contact_success"),
)

The View

The view displays the contact form and processes the submission. If the form submission was valid the user is redirected to a "Thank you" page. Pretty much your standard Django form view.
I will use this same view for traditional POST requests as well as the AJAX requests. So, I have used the HttpRequest.is_ajax() method to check for an AJAX request before redirecting a user. This is because the problem with duplicate POST submissions is only a problem with traditional requests. With the AJAX request I can simply render the success page template instead of redirecting to it.
Note: I am using the render() shortcut which was introduced in Django 1.3. If you have an older version of Django you can use the render_to_response() shortcut instead.
views.py
from django.shortcuts import redirect, render
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():
            message = "From: %s <%s>\r\nSubject:%s\r\nMessage:\r\n%s\r\n" % (
                form.cleaned_data['name'],
                form.cleaned_data['email'],
                form.cleaned_data['subject'],
                form.cleaned_data['message']
            )
            mail_admins('Contact form', message, fail_silently=False)
            if request.is_ajax():
                return render(request, 'contact/success.html')
            else:
                return redirect('contact_success')
    else:
        form = ContactForm()

    return render(request, 'contact/form.html', {'form':form})

The Templates

The template code below shows both the unobtrusive javascript and the form markup. I'm going to discuss the javascript in detail next.
Note: I put everything in this one template to keep the example simple. An actual implementation would use external stylesheets and javascript files, a base template, template blocks and includes, etc.
templates/contact/form.html
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
  <title>Contact</title>
  <script type="text/javascript" 
    src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
  <script type="text/javascript">
  /*<![CDATA[*/
  jQuery(function() {
      var form = jQuery("#contactform");
      form.submit(function(e) {
          jQuery("#sendbutton").attr('disabled', true)
          jQuery("#sendwrapper").prepend('<span>Sending message, please wait... </span>')
          jQuery("#ajaxwrapper").load(
              form.attr('action') + ' #ajaxwrapper',
              form.serializeArray(),
              function(responseText, responseStatus) {
                  jQuery("#sendbutton").attr('disabled', false)
              }
          );
          e.preventDefault(); 
      });
  });
  /*]]>*/
  </script>
</head>
<body>
  <h1>Contact</h1>
  <form action="{% url contact_form %}" method="post" id="contactform">
    <div id="ajaxwrapper">
    {% csrf_token %}
    {{ form.non_field_errors }}
    {{ form.as_p }}
    <p id="sendwrapper"><input type="submit" value="Send" id="sendbutton"/></p>
    </div>
  </form>
</body>
</html>

The Javascript

First of all, I am using 'jQuery()' instead of the more common shortcut of using '$()'. This is my preference due to possible conflicts with other javascript libraries (such as Prototype and YUI 2). Just remember that you can use a dollar sign '$' everywhere you see 'jQuery' in the code below.
A form's submit event cannot be hijacked until the form exists, therefore, a callback function is setup to fire once the DOM has been loaded. This is done by passing a javascript function to jQuery().
jQuery(function() {
    //...
});
The form object is obtained by passing a CSS-style selector for the form ID to jQuery() and then stored in a local variable for convenience.
var form = jQuery("#contactform");
A callback function is attached the form object's submit event.
form.submit(function(e) {
    // ...
});
The submit button is disabled to prevent duplicate requests and a message is prepended to the container for the submit button that gives the user some visual cue that their submission is in progress. A real-world implementation might use some sort of modal dialog or spinning AJAX graphic here.
jQuery("#sendbutton").attr('disabled', true)
jQuery("#sendwrapper").prepend('Sending message, please wait... ')
jQuery's load() method is called on the <div> element that wraps the inner contents of the form. The load method will send an AJAX request to the server and replace the contents of that <div> with the response.
The first argument passed to load() is the URL of the request which is obtained from the 'action' attribute of the form. However, an additional CSS selector can be added to the URL to let jQuery know to parse out that element from the response. Thus, the URL in the code below, depending on your Django URLconf, might evalutate to "/contact/ #ajaxWrapper". In other words, jQuery will replace <div id="ajaxWrapper"> in the current document with <div id="ajaxWrapper"> in the response from the AJAX request.
The second argument passed to load() is the data. The serializeArray() method is used to serialize the form data into an array. An array is necessary because an array is an object. The load() method will send a GET request if the data is a string, such as that returned by serialize(), and a POST request if the data is an object.
The third argument passed to load() is a callback function to call when the response comes back. Thi is used to re-enable the submit button.
jQuery("#ajaxwrapper").load(
    form.attr('action') + ' #ajaxwrapper',      
    form.serializeArray(),                     
    function(responseText, responseStatus) {   
        jQuery("#sendbutton").attr('disabled', false)
    }
);
The default submit event handler is disabled at the end of the new submit handler. This way, if there is a syntax error in the javascript before the then which breaks the AJAX request, the form's default submit event handler will still work.
e.preventDefault(); 

Suggested Improvements

Move javascript out of template
You should move that javascript into an external file and serve it up wtih the rest of your static media.
Slowing down AJAX responses
While testing using your local Django development server, AJAX reqeust and responses can happen very, very fast. When you are creating visual cues for the end user during an AJAX operation, such as a modal dialog or a spinning "Loading..." graphic, it can be nice to temporarily delay the responses so you can see it happen. This can be done by temporarily adding a call to time.sleep() for AJAX requests in your view.
Note: This is a tip for development/testing only and should be removed when it is no longer needed.
from django.conf import settings
import time

def contact_form(request):
  # your view code ...
  if request.is_ajax(): # only if AJAX
      if getattr(settings, 'DEBUG', False): # only if DEBUG=True
          import time
          time.sleep(5) # delay AJAX response for 5 seconds
Partial page responses
Using the selector as part of the URL passed to jQuery's load() method was a nice, convenient way to parse out just the content we wanted from the response. This is quite acceptable for something like a contact form that doesn't have high traffic. However, a blog comment or shopping cart function might want to optimize the response size by returning just the part of the response needed. I prefer to break up my templates into includes and have my views send partial responses for AJAX requests.
For example, move the contents of <div id="ajaxWrapper"> into an include template named templates/contact/fields.html, remove the selector from URL passed to the jQuery's load() method, and change how the view sends the response:
if request.is_ajax:
    # response is just the form 
    return render(request, 'contact/fields.html', {'form':form})
else:
    # response is the entire page
    return render(request, 'contact/form.html', {'form':form})
(you would have to do something similar for success.html)

No comments:

Post a Comment