Intro to Unintrusive JavaScript with Django


There are quite a number of tutorials looking at using Ajax with Django. Despite that, it is still a topic that leaves many confused. I think the fundamental issue is that there are three very different groups looking for tutorials on using Ajax with Django:

  1. Experienced web developers who have used a multitude of other frameworks (and used to administer networks composed of passenger pigeons in their younger days) who just need a couple of low-level details.

  2. Web developers who have mastered one other framework (often Ruby on Rails), but need a slightly higher level of detail to get used to the differences between Django's way of doing things and their previous experience.

  3. Inexperienced web developers who have never mastered a web framework, and may have a limited grasp on Python, JavaScript, and the high-level concepts of client-server interaction that occur in Ajax applications.

Usually tutorials for group one get written first, because they usually only need to be a couple of paragraphs and a code snippet (and they make up the majority of early adopters). Then, over time tutorials that haphazardly target groups two and three get published, often to the detriment (and frustration) of both groups.

This tutorial's aim is to guide someone with a bit of Django knowledge--but no understanding of Ajax or JavaScript--through implementing a simple note taking application while touching on the high, low, and medium level concepts of creating Ajax web applications.

If you've walked through the Django tutorial then you're ready to get started with unintrusive JavaScript and Ajax.

Overview

Many people will tell you that the first step to creating an Ajax webapp is to pick a JavaScript library. Should you use jQuery, or Dojo, or the Pythonic MochiKit? But, woah, woah. Picking a JavaScript library isn't actually the first step.

The first step in implementing an Ajax webapp is to implement the webapp without Ajax. That's a bit counter-intuitive, so lets say that again: the first step to implementing an Ajax webapp is to implement the webapp without Ajax.

What does that even mean?

It means that we'll start out by implementing the entire notetaking app without using any JavaScript, instead using the old-school tool of web interactivity: the form.

Afterward, we'll use JavaScript to improve upon the website and make the user experience more pleasant, but by starting without JavaScript we make sure that our website functions properly for individuals with JavaScript disabled or with antiquated browsers.

This concept of JavaScript as added-value (as opposed to the-value) is called unintrusive JavaScript. It's a bit of a trendy topic these days, but that doesn't mean it isn't worthwhile; it is1.

So, if step one is to build the site without JavaScript, then the second step will be using jQuery to spruce things up.

We'll be using jQuery because it has emerged as something of the industry leader of powerful lightweight JavaScript libraries. Even if you don't particularly care for jQuery, the concepts this tutorial examines will be applicable to other JS libraries, so you'll still be able to take some value home with you.

The Application

The Django application we're building is called notes, and is fairly simple: its index page displays a list of notes, and allows the creation of new notes.

Each note will have a detail page as well, which allows viewing and editing the note's contents.

That's it. Nonetheless, there is a surprising amount of room for sprucing the app up with Ajax. Cheer up, it's gonna be fun.

Screenshot of Notes.DjApp list view.

Supplementary Materials: the Git Repo

Usually I write tutorials while writing the application. I write a line in the app, then I write a line in the tutorial. It helps avoid mistakes by omission, but it promotes mistakes by... mistake. I've been trying to rework my tutorial writing process, and this time I approached it a bit differently.

I wrote the tutorial using Git for version control and religiously commited each change as I made it (well, until the last 20%, when I voluntarily forfeited my mind), such that you could follow the changes in the repository's revision history to view the program grow in small steps.

In fact, if you are Git saavy, then I highly encourage taking that approach. Read through this tutorial to get the high-level details and skim the code, but then sit down with the Git repository and examine the changes.

Even if you're not terriably familiar with Git, it's actually pretty easy to do this. As long as you have Git installed, download this zipped up Git repository, and enter the folder.

Then view the change history by using log:

will-larsons-macbook:ajax_tut will$ git log

If you go all the way down to the beginning, then you'll see this:

commit b87ad98503285f531aadc30d2bcfa306402c2c93
Author: Will Larson <lethain@gmail.com>
Date:   Thu Sep 18 08:42:53 2008 -0400

    Created ajax_tut project.

That is the first commit I made. To see the changes between revisions:

git diff b87ad98503285f531aadc30d2bcfa306402c2c93 d178ab382f85fcb5aa5069cc764afc1b3a4a012d

Those two hashes are the hash for the first revision followed by the hash for the second revision, and it outputs some text like this:

diff --git a/notes/__init__.py b/notes/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/notes/models.py b/notes/models.py
new file mode 100755
index 0000000..71a8362
--- /dev/null
+++ b/notes/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/notes/views.py b/notes/views.py
new file mode 100755
index 0000000..60f00ef
--- /dev/null
+++ b/notes/views.py
@@ -0,0 +1 @@
+# Create your views here.

And you can continue up the changes using git log to determine the hashes for each revision and slowly walk through the program as it grows.

Using this approach has two advantages:

  1. It isn't passive, so you won't just zone out while reading, you'll actually have to comprehend the changes.

  2. Even if I forget to write a set of instructions in the tutorial, the repository has devotedly recorded each and every keystroke that has changed, so it is the muse with all the answers.

Give the Git repository approach a try, and let me know if it is helpful.

Starting notes

Now it's time to actually get started.

  1. Create a new Django project.

    django-admin.py startproject ajax_tut
    
  2. Create the notes app.

    python manage.py startapp notes
    
  3. Create a handful of directories.

    mkdir media
    mkdir notes/templates
    mkdir notes/templates/notes
    
  4. Open up settings.py and add these lines at the top:

    import os
    ROOT_PATH = os.path.dirname(__file__)
    

    Then change these values:

    DATABASE_ENGINE = 'sqlite3'
    DATABASE_NAME = os.path.join(ROOT_PATH, 'notes.sqlite')
    MEDIA_ROOT = os.path.join(ROOT_PATH, 'media')
    MEDIA_URL = 'http://127.0.0.1:8000/media/'
    TEMPLATE_DIRS = (
        os.path.join(ROOT_PATH, 'templates'),
    )
    INSTALLED_APPS = (
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.sites',
        'notes',
    )
    

    These changes may seem a bit confusing, but are incrediably helpful in creating development projects. These settings allow the project to detect its current position and then create the correct the paths for its sqlite database, media directory, and template directory.

    Without these settings you'd have to type in the absolute path yourself, but with them you can change the project's directory--or even zip it up and send it to a colleague-- and it will just work.

    I use them in pretty much every development project.

  5. Next we need to edit ajax_tut/urls.py to use the Django media server (which is for development purposes only, don't use it for deployment!), and to pass all incoming urls to the ajax_tut/notes/urls.py file (which we haven't written quite yet).

    ajax_tut/urls.py should look like this:

    from django.conf.urls.defaults import *
    from django.conf import settings
    urlpatterns = patterns('',
        (r'^media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT }),
        (r'^', include('ajax_tut.notes.urls')),
    )
    

    Note that urls are matched in order, so even though the second regex will match any incoming url, the incoming url will be matched against the first url before reaching it.

    The r'^media/(?P.*)$'regex is what allows us to serve static media using the development server, andr'^'` simply means to pass any urls that reach it to the urls.py file in the notes app.

  6. Now is a good time to run the development server and check to see if any typos have entered the system.

    python manage.py runserver
    

    Then navigate to http://127.0.0.1:8000 and you should get a shiny error page complaining that the ajax_tut.notes.urls module doesn't exist.

    We're on the right track.

  7. Next we want to create the Note model that will store the data for our webapp. Open up ajax_tut/notes/models.py and make it look like this:

    from django.db import models
    
    class Note(models.Model):
        title = models.CharField(max_length=100)
        slug = models.SlugField()
        text = models.TextField(blank=True,null=True)
    
        def __unicode__(self):
            return u"Note(%s,%s)" % (self.title, self.slug)
    
        def get_absolute_url(self):
            return u"/note/%s/" % self.slug
    

    We're kind of jumping ahead of ourselves a bit by defining the get_absolute_url method already, since we haven't written the corresponding url yet, but we're just saving ourselves a wee bit of time by doing it now.

    After that, go to the command line and sync the database to create the appropriate tables for the Note model.

    python manage.py syncdb
    
  8. Now we can write the ajax_tut/notes/urls.py file.

    from django.conf.urls.defaults import *
    from models import Note
    
    notes = Note.objects.all()
    
    urlpatterns = patterns(
        '',
        (r'^$',
         'django.views.generic.list_detail.object_list',
         dict(queryset=notes)),
        (r'^note/(?P<slug>[-\w]+)/$',
         'django.views.generic.list_detail.object_detail',
         dict(queryset=notes, slug_field='slug')),
        (r'^create/$','notes.views.create_note'),
        (r'^note/(?P<slug>[-\w]+)/update/$','notes.views.update_note'),
    )
    

    For all our displaying content needs we will using the two generic views list_detail.object_list and list_detail.object_detail.

    The former will display all the notes, and the later will display one specific note based on the slug parameter detected in the url's regex.

    The generic views operate on a queryset of objects, which is why we have to create the notes queryset and pass it to the views.

    We are also specifying two custom views, create_note and update_note which we will need to write by hand, which will allow us to create and make changes to notes by POSTing data to them.

  9. Generic views have a simple mechanism for determining the template to use to display an object. For the list view, it will attempt to load appName/modelName_list.html, and for the detail view it will attempt to load appName/modelName_detail.html.

    In this case they will be notes/note_list.html and notes/note_detail.html.

    So our next step is to create those two templates, but first we'll want to create a base.html template that the other two can extend to reduce repetition.

    In the notes/templates/ directory make a new file named base.html, and fill it with this code:

    <html> <head>
    <title>{% block title %}Notes.DjApp{% endblock %}</title>
    <link rel="stylesheet" href="/media/style.css">
    <script type="text/javascript" src="/media/jquery-1.2.6.min.js"></script>
    </head>
    <body>
    <div class="container">
    {% block content %}{% endblock %}
    </div>
    </body>
    </html>
    

    This is a very simple base template, which just handles a few static imports and basic html structure for us.

    Notice at the top we are adding a stylesheet and importing the jquery library, which we haven't actually placed in the media/ directory yet. We'll do that after a few more steps.

  10. The next step is to create the notes/note_list.html template to display the list of notes on our index page.

    Create the file ajax_tut/notes/templates/notes/note_list.html, and add these contents to it:

    {% extends "base.html" %}
    {% block content %}
    <div class="new">
      <h2> Create a new note. </h2>
      <form method="post" action="/create/">
      <label for="title">Title</label>
      <input type="text" name="title" id="title">
      <label for="slug">Slug</label>
      <input type="text" name="slug" id="slug">
      <input type="submit" value="create note">
      </form>
    </div>
    <div class="list">
      <h2> Notes </h2>
      <ol id="notes">
        {% for object in object_list %}
        <li><a href="{{ object.get_absolute_url }}">{{ object.title }}</a></li>
        {% endfor %}
      </ol>
    </div>
    {% endblock %}
    

    We're creating a simple form for creating new notes, and a simple ordered list for displaying them. We're sending the contents of the form to the /create/ url, which just so happens to be directed to the create_note method in notes.views.

    Let's go implement that.

  11. Open up notes/views.py, and we'll implement create_note. It'll need to make sure that it's recieving a POST request, and and also that the request contains values for title and slug.

    Otherwise it should return a somewhat helpful error message.

    from models import Note
    from django.http import HttpResponseRedirect, HttpResponseServerError
    
    def create_note(request):
        error_msg = u"No POST data sent."
        if request.method == "POST":
            post = request.POST.copy()
            if post.has_key('slug') and post.has_key('title'):
                slug = post['slug']
                if Note.objects.filter(slug=slug).count() > 0:
                    error_msg = u"Slug already in use."
                else:
                    title = post['title']
                    new_note = Note.objects.create(title=title,slug=slug)
                    return HttpResponseRedirect(new_note.get_absolute_url())
            else:
                error_msg = u"Insufficient POST data (need 'slug' and 'title'!)"
        return HttpResponseServerError(error_msg)
    

    Notice that on line 10 we are verifying that the slug isn't already in use to prevent slug duplication (which would prevent either of the notes with the duplicated slug from being accessed with the generic detail view).

  12. Half of our application is now implemented and working coherently. We can view the list of notes, and create new ones. We just need to view and edit individual notes.

    Onward we go.

    Open up notes/templates/notes/note_detail.html and imbue it with life:

    {% extends "base.html" %}
    {% block content %}
    <div class="header">
      <a href="/"> Back to Index </a>
    </div>
    <div class="detail">
      <form method="post" action="update/">
      <div class="text">
        <label for="title">Title</label>
        <input type="text" name="title" id="title" value="{{ object.title }}">
        <label for="slug">Slug</label>
        <input type="text" name="slug" id="slug" value="{{ object.slug }}">
      </div>
      <textarea name="text" id="text">{{ object.text }}</textarea>
      <input class="submit" type="submit" value="update note">
      </form>
    </div>
    {% endblock %}
    

    This time the form is POSTing its contents to update/, which is a relative url, unlike /create/ which was posting to an absolute url.

    That makes life a bit easier, but the note's slug is already included in its url, so we don't have to post the slug each time we update (the current form will indeed post all the content each time it is sent, but for our Ajax remix that won't necessarily be the case).

  13. Next we need to write update_note, which is the view that will handle the submitted form data from note_detail.html.

    Open up notes/views.py and add this function below the code that is already there:

    def update_note(request, slug):
        if request.method == "POST":
            post = request.POST.copy()
            note = Note.objects.get(slug=slug)
            if post.has_key('slug'):
                slug_str = post['slug']
                if note.slug != slug_str:
                    if Note.objects.filter(slug=slug_str).count() > 0:
                        error_msg = u"Slug already taken."
                        return HttpResponseServerError(error_msg)
                    note.slug = slug_str
            if post.has_key('title'):
                note.title = post['title']
            if post.has_key('text'):
                note.text = post['text']
            note.save()
            return HttpResponseRedirect(note.get_absolute_url())
        error_msg = u"No POST data sent."
        return HttpResponseServerError(error_msg)
    

    Notice that this is a fairly flexible approach to updating, if all three fields are present it will update all of them, but it can also handle updating only one or two of the fields as well.

    Once again we are verifying that the slug is not already in use before we process the update (slugs must be unique).

    Lastly, we're still trying to provide helpful error messages which will help users figure out what they're doing wrong, and will be essential in making our Ajax version have a good user experience.

  14. Phew. Now we're pretty much done with the non-Ajax version of the website, but before we move forward with some JavaScript, we're going to apply just a little bit of CSS to make the whole thing a bit more bearable to look at.

    In the ajax_tut/media/ folder create a file named style.css, and enter this CSS into it:

    body {
        font-family: georgia;
    }
    .container {
        width: 80%;
        margin-top: 2em;
        margin-left: auto;
        margin-right: auto;
    }
    /* note_list.html */
    .new {
        text-align: center;
        border: 1px solid grey;
        margin-bottom: 1em;
        padding-bottom: 1em;
    }
    .list {
        border: 1px solid grey;
    }
    .list h2 {
        text-align: center;
    }
    /* note_detail.html */
    .header {
        margin-bottom: 2em;
    }
    .detail .text {
        text-align: center;
    }
    .detail textarea {
        margin-top: 1em;
        width: 100%;
        height: 300px;
    }
    .detail .submit {
        width: 50%;
        margin-left: 25%;
        margin-right: 25%;
    }
    

    Hey, it actually looks slightly handsome now.

  15. Go ahead and save everything and then run the dev server to make sure that no typos have snuck into the code:

    python manage.py runserver
    

    And go over to http://127.0.0.1:8000/ to make sure it's all working.

Play around with it, take a walk in the park, and when you're eyes stop hurting from staring at the screen, then come back and we'll get started with some Ajax.

Inviting Ajax to the Party

Now that we have a working application, it's time to start considering how to incorporate some dynamic Ajax functionality into the application.

Before we move onto the details, I want to briefly approach the high level concept of Ajax. Simply put, Ajax is about using out-of-band asynchronous communication to provide functionality without actually leaving the url at which you arrived.

Good use of Ajax can make your webapps more responsive. Poor usage can make your webapps horrifying deathtraps. It's important to make sure you're using Ajax because it actually improves your site, and not because... well, actually, there isn't any other reason to use it.

From the Django perspective, what implementing Ajax entails is writing JavaScript to send requests from the client side, and making sure you've written appropriate views to service those requests.

Typically this means creating custom views to JSON encode relevant data and return it to an awaiting JavaScript function.

However, in this example we're going to do things slightly differently. We've already written views to handle the functionality we need (creating and editing notes), and we're simply going to reuse those with our JavaScript.

However... those views are returning HTML instead of JSON, so we're going to have to extract the necessary data ourselves. We're going to use regular expressions towards that end. If you find this a bit overly complex, take heart because you can avoid this regex-sorcery by simply writing additional views that return the necessary data in JSON encoding.

Lets get started.

  1. The first step is to download the jQuery JavaScript library, which you can download here.

    Then move it into the media/ directory.

    mv ~/Downloads/jquery-1.2.6.min.js media/
    

    We already added the import into the base.html template, which looked like this:

    <head>
    <title>{% block title %}Notes.DjApp{% endblock %}</title>
    <link rel="stylesheet" href="/media/style.css">
    <script type="text/javascript" src="/media/jquery-1.2.6.min.js"></script>
    </head>
    

    And with that jQuery is 'installed'.

  2. The first Ajaxosity we're going to attempt is to improve upon the note creation process.

    As it stands, when you attempt to create a new note, you type in the note's title and slug and then hit create. It'll then take you to the note's page.

    We're going to change it a bit. Instead, when you create a new note the link to the new note will appear at the top of the note's list, without leaving the page.

    This makes it easier to create multiple notes at once, and still makes it easy to access the note once it is created.

    To accomplish this we're going to add some JavaScript to the notes/note_list.html template.

    Open up the notes/notes_list.html file, and insert this code right above the {% endblock %} template tag. I'm going to break it down into two functions, but they should all be within one <script></script> block.

    var create_note = function() {
      var title = $("#title").val()
      var slug = $("#slug").val()
      if (title != "" && slug != "") {
        var data = { title:title, slug:slug };
        var args = { type:"POST", url:"/create/", data:data, complete:done };
        $.ajax(args);
      }
      else {
        // display an explanation of failure
      }
      return false;
    };
    

    The create_note function here is use the jQuery ajax function to send an asynchronous request to the /create/ url. It extracts the values of the title and slug inputs and passes them as POST parameters.

    It also specifies the done function to be called once it receives a response. (We'll look at that function in just a second.)

    Finally, it is very important that create_note return the value false. In order to override an html button's (or in this case a submit input's) default click behavior, you must return false. Otherwise the button's normal behavior will still occur (which in this case would mean trying to create the same note twice, and redirecting the user to an error page instead of the freshly minted note).

    The done function that is handing the reponse looks like this:

    var done = function(res, status) {
      if (status == "success") {
        var txt = res.responseText;
        var titleStr = txt.match(/id="title" value="\w+"/ig)[0];
        var slugStr = txt.match(/id="slug" value="\w+"/ig)[0];
        var title = titleStr.match(/"\w+"/ig)[1].replace(/"/g,'');
        var slug = slugStr.match(/"\w+"/ig)[1].replace(/"/g,'');
        var newLi = $('<li><a href="/note/'+slug+'">'+title+'</a></li>');
        $("#notes").prepend(newLi);
        $("#title").val("");
        $("#slug").val("");
      }
      else {
        // display an explanation of failure
      }
    }
    

    The done function is a bit more complex because we're parsing the slug and text out of the response, which happens to be html.

    After we extact the data, then we use jQuery to create a new <li> element, and prepend it to <ol id="notes">.

    Finally, the last thing we need to do is associate the create_note function with the submit button in the form:

    $("#create").click(create_note);
    

    Now give it a test. It's a bit of an improvement.

  3. Now we kind of skirted around an important issue: error handling. As it stands, if something goes wrong then the user has no idea what has transpired. That's a pretty mean thing to do to a user, so we're going to improve upon that.

    This error notification is a pattern that we'll use in the note_detail.html template as well, so we're going to make sure we write reusable code and throw it into a .js file in the media directory.

    Go ahead and create a new file media/notes.js, and fill it with this function:

    var display_error = function(msg, elem) {
      var msg_div = $('<div class="error_msg"><p>'+msg+'</p></div>');
      msg_div.insertAfter(elem).fadeIn('slow').animate({opacity: 1.0}, 5000).fadeOut('slow',function() { msg_div.remove(); });
    };
    

    This display_error function creates a new div element with a specified error message, then fades it in, display it for five seconds, and then fades it out (and removes the div).

    Then we have to load it in our base.html template, adding this line beneath loading the jQuery library:

    <script type="text/javascript" src="/media/notes.js"></script>
    

    And finally we have to integrate it into our notes/note_list.html template in two places.

    Replace:

    var done = function(res, status) {
      if (status == "success") {
        // do stuff
      }
      else {
        // display an explanation of failure
      }
    }
    

    With this code that actually handles errors:

    var done = function(res, status) {
      if (status == "success") {
        // do stuff
      }
      else {
        // Only this changed...
        display_error(res.responseText, $(".new"));
      }
    }
    

    And in create_note replace this line

    // display an explanation of failure
    

    With this:

    display_error("Requires values for both title and slug.", $(".new"));
    

    Now our Ajax note creation is actually usable, instead of simply being a minefield for users to unhappily navigate.

    And somehow we muster the strength to continue further into this fearsome new land.

  4. Now we're going to shift focus from notes/note_list.html to the detail template, notes/note_detail.html.

    We're going to implement a couple two cute ideas:

    1. The slug and title input will be converted to plain text until the mouse hovers over them, at which point they will turn into inputs.

    2. We're going to make the submit button disappear, and update data as soon as we leave the corresponding input or textfield.

    We're going to do a very simple job of it at first, and fix it up a bit afterwards.

    Go ahead and open up notes/note_detail.html.

    Just above the {% endblock %} tag we're going to add another <script></script> block.

    Now we're going to do something we didn't do in notes_list.html, but probably should have. We're going to wrap all of our JavaScript within this function:

    $(document).ready(function() {
      // do stuff
    });
    

    That is a helpful jQuery trick which makes sure your JavaScript loads after the DOM data is available to manipulate. Essentially it makes sure the page isn't still construction itself when your script gets called (which could prevent the script from executing properly).

    Now, within that function we're going to start out by making the inputs change into spans when the mouse isn't over them. Let's start with the title input.

    $(document).ready(function() {
      var title_to_input = function() {
        var title = $("#title");
        var input = $('<input type="text" name="title" id="title" value="'+title.text()+'">');
        input.hover(function() {}, title_to_span);
        title.replaceWith(input);
      }
      var title_to_span = function() {
        // called on mouse away
        var title = $("#title");
        var span = $('<span id="title"><em>'+title.val()+'</em></span>');
        span.hover(title_to_input,function() {});
        title.replaceWith(span);
      }
    });
    

    You can see that we're starting to use a bit more of jQuery here. The hover function allows you to specify a function to call when the mouse is above an element, as well as a second function to call when it leaves the element.

    We're using it to call our functions to convert between inputs and spans. I'm using a verbose style here, but you could undoubtedly compact that JavaScript down to fewer lines... but I find it more legible as it is.

    We also need to write equivalent functions for the slug input. Throws these inside of the $(document.ready() function as well:

    var slug_to_input = function() {
      var slug = $("#slug");
      var input = $('<input type="text" name="slug" id="slug" value="'+slug.text()+'">');
      input.hover(function() {}, slug_to_span);
      slug.replaceWith(input);
    }
    var slug_to_span = function() {
      // called on mouse away
      var slug = $("#slug");
      var span = $('<span id="slug"><em>'+slug.val()+'</em></span>');
      span.hover(slug_to_input,function() {});
      slug.replaceWith(span);
    }
    title_to_span();
    slug_to_span();
    

    As you can see, we also need to call the two _to_span functions once when the page loads, so that they begin as spans.

    Go ahead and test it out on the page.

    You'll probably think to yourself that its pretty annoying, seeing as the resizing really shifts things around. Lets throw a few lines of CSS into style.css to help relieve that.

    First, remove these lines:

    .detail .text {
       text-align: center;
    }
    

    And then... give up. It's possible to make this technique look okay, but we'd need to add a few divs to the markup, and this tutorial isn't aiming for visual perfection.

  5. Now we have the input's flitting between span and input, but we still have to submit data using the 'ole fashioned button.

    So, first let's get rid of that button (for people who have JavaScript enabled), and then lets replace its functionality by saving when the mouse leaves the editing field.

    To get rid of the submit button we simply add this line of JavaScript to what we already have (inside the ready() function):

    $("input[type=submit]").remove()
    

    And updating is just a wee bit more complex.

  6. Before we implement updating, we're going to take a brief detour to create a display_success function to complement the display_error function we previously created.

    Go ahead and open media/notes.js and add this function:

    var display_success = function(msg, elem) {
      var msg_div = $('<div class="success_msg"><p>'+msg+'</p></div>');
      msg_div.insertAfter(elem).fadeIn('slow').animate({opacity: 1.0}, 5000).fadeOut('slow',function() { msg_div.remove(); });
    };
    

    Then we're going to style the divs created by both the display_success and display_error functions. Open up media/style.css and add these lines:

    .error_msg {
        background-color: red;
        border: 1px solid black;
        margin-top: 1em;
        margin-bottom: 1em;
        text-align: center;
    }
    .success_msg {
        background-color: blue;
        border: 1px solid black;
        margin-top: 1em;
        margin-bottom: 1em;
        text-align: center;
    }
    

    This CSS will make it easy to distinguish between success and error messages, as well as make them look a little more presentable.

  7. Now we're ready to add updating to notes/note_detail.html. There is a fair amount of code that we're going to modify in this step, so first I'll look at the individual pieces, and then show the entirety of it (to facilitate copy-pasting).

    First, we need to create a function for sending the updates to the update/ url.

    var perform_update = function(field, val) {
      var data = {};
      data[field] = val;
      var args = { type:"POST", url:"update/", data:data, complete:done };
      $.ajax(args);
    };
    

    Using this function to update values is as simple as:

    perform_update("title", "my-title");
    perform_update("slug", "my-slug");
    perform_update("text", "it was a great time");
    

    Remember that we made the update view in notes.views flexible enough to handle recieving all values at once, or to also handle recieving one value at a time, which allows us to write this simple update function.

    Also notice that perform_update is caling the done function on completion, which we haven't written yet. done looks like this:

    var done = function(res, status) {
      if (status == "success") display_success("Updated successfully.", $(".text"));
      else display_error(res.responseText, $(".text"));
    }
    

    It uses our helpful display_error and display_success functions to handle most of the details.

    Finally, we have to modify the title_to_span and slug_to_span functions to call send updates.

    That is as simple as changing them to look like:

    var title_to_span = function() {
      var title = $("#title");
      perform_update("title", title.val());
      // etc etc
    }
    

    But that leads to a problem: we call the _to_span functions when we load the page, so we'd be sending updates right as we loaded the page.

    We can prevent that scenario by using a pair of variables to track whether or not this is the initial usage, and to prevent sending the update in that situation.

    var initialTitleChange = true;
    var initialSlugChange = true;
    
    var slug_to_span = function() {
      var slug = $("#slug");
      if (initialSlugChange) initialSlugChange = false;
      else perform_update("slug", slug.val());
      // etc etc
    }
    

    With that change we have thing working decently well, and that entire chunk of code (everything inside the <script></script> block) looks like this:

    $(document).ready(function() {
      var initialTitleChange = true;
      var initialSlugChange = true;
    
      var perform_update = function(field, val) {
        var data = {};
        data[field] = val;
        var args = { type:"POST", url:"update/", data:data, complete:done };
        $.ajax(args);
      };
    
      var done = function(res, status) {
        if (status == "success") display_success("Updated successfully.", $(".text"));
        else display_error(res.responseText, $(".text"));
      }
    
      var title_to_input = function() {
        var title = $("#title");
        var input = $('<input type="text" name="title" id="title" value="'+title.text()+'">');
        input.hover(function() {}, title_to_span);
        title.replaceWith(input);
      }
      var title_to_span = function() {
        // called on mouse away
        var title = $("#title");
        if (initialTitleChange) initialTitleChange = false;
        else perform_update("title", title.val());
        var span = $('<span id="title"><em>'+title.val()+'</em></span>');
        span.hover(title_to_input,function() {});
        title.replaceWith(span);
      }
      var slug_to_input = function() {
        var slug = $("#slug");
        var input = $('<input type="text" name="slug" id="slug" value="'+slug.text()+'">');
        input.hover(function() {}, slug_to_span);
        slug.replaceWith(input);
      }
      var slug_to_span = function() {
        // called on mouse away
        var slug = $("#slug");
        if (initialSlugChange) initialSlugChange = false;
        else perform_update("slug", slug.val());
        var span = $('<span id="slug"><em>'+slug.val()+'</em></span>');
        span.hover(slug_to_input,function() {});
        slug.replaceWith(span);
      }
      title_to_span();
      slug_to_span();
      $("input[type=submit]").remove()
    });
    

    Just a couple-few lines... right?

  8. An astute reader, or aggressive critic, will be quick to note that there are a number of ways in which this implementation is imperfect.

    For example, wouldn't it be great if it specifically mentioned which field was updated? Even better, wouldn't it be great if it reverted all changes when an update failed?

    Yep. Those would be pretty sweet. However, implementing them would require us to write additional custom views to feed us the knowledge we need for those situations.

    For example, the ajax friendly update method might look something like this:

    from django.utils import simplejson
    
    def ajax_update_note(request, slug):
        post = request.POST.copy()
        note = Note.objects.get(slug=slug)
        if post.has_key('slug'):
            slug_str = post['slug']
            if Note.objects.filter(slug=slug_str).count() > 0:
                d = { 'msg':'Slug already in use.','slug':note.slug }
                return HttpResponse(simplejson.dumps(d))
            note.slug = slug_str
        if post.has_key('title'):
            note.title = post['title']
        if post.has_key('text'):
            note.text = post['text']
        d = { 'msg':'Update successful!' }
        return HttpResponse(simplejson.dumps(d))
    

    If we were using that, then we'd have sufficient data on hand to revert invalid changes to their previous value, and we could be much more fanciful with out modifications.

    This tutorial won't explicitly walk down that path, but if you simple create a new url regex in notes.urls, paste in the above ajax_update_note request, and modify the perform_update function in notes/note_detail.html, then you'd be pretty close to finished.

    You would have to rewrite done as well, to take advantage of the incoming JSON. It might look like:

    var done = function(res, status) {
      if (status == "success") {
        var data = eval(res.responseText);
        display_success(data['msg'], $(".text"));
      else {
        var data = eval(res.responseText);
        display_error(data['msg'], $(".text"));
        if (data.slug) {
          $("#slug").text(data.slug);
        }
      }
    }
    

    When you start writing more complex Ajax functionality, then you will inevitably have to write custom Django views to support that functionality.

    It's just the cost of doing (Ajax) business.

  9. Finally, we need to setup the textfield to update as well. Since we already have the utility functions written, this will be really quick:

    $("#text").hover(function() {}, function() { perform_update("text", $("#text").text()); });
    

    Add that line so the end of the JavaScript in notes/note_detail.html looks like this:

    title_to_span();
    slug_to_span();
    $("#text").hover(function() {}, function() { perform_update("text", $("#text").text()); });
    $("input[type=submit]").remove()
    

    And now it updates just like the text fields do.

Screenshot of Notes.DjApp detail view.

Although it isn't very polished, we have implemented some interesting Ajax functionality here, hopefully enough to get you started on implementing ideas of your own.

Moving Onward

In this tutorial we've converted a pretty plan application into something that is a wee bit exciting. For the time being we are going to leave it at that, and you can download a zip of the Git repository here.

There are two additional topics which I'll address in two supplementary articles:

  1. using additional views to improve the Ajax experience, and
  2. adding a verification layer to this application.

Part two continues here.


  1. Certainly there are fewer and fewer individuals whose browser cannot run JavaScript. For a time many mobile browsers struggled with JS, but at this point even that seems a thing of the past.

    There are indeed individuals who disable JavaScript for security reasons, but even beyond that small segment, there are benefits to designing websites that don't require JavaScript.

    First, it promotes standard and predictable APIs between the client and the server (POST or GET requests for the win). Second, it makes your site more crawlable for both accessibility purposes, and for search engines as well.

All Rights Reserved, Will Larson 2007 - 2014.