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:
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.
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.
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.
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:
It isn't passive, so you won't just zone out while reading, you'll actually have to comprehend the changes.
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.
Create a new Django project.
django-admin.py startproject ajax_tut
Create the
notes
app.python manage.py startapp notes
Create a handful of directories.
mkdir media mkdir notes/templates mkdir notes/templates/notes
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.
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 theajax_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, and
r'^'` simply means to pass any urls that reach it to theurls.py
file in thenotes
app.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.
Next we want to create the
Note
model that will store the data for our webapp. Open upajax_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)
<span class="k">def</span> <span class="nf">__unicode__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="s">u"Note(</span><span class="si">%s</span><span class="s">,</span><span class="si">%s</span><span class="s">)"</span> <span class="o">%</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">title</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">slug</span><span class="p">)</span> <span class="k">def</span> <span class="nf">get_absolute_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="s">u"/note/</span><span class="si">%s</span><span class="s">/"</span> <span class="o">%</span> <span class="bp">self</span><span class="o">.</span><span class="n">slug</span>
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
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
andlist_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
andupdate_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.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 loadappName/modelName_detail.html
.In this case they will be
notes/note_list.html
andnotes/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 namedbase.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.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 thecreate_note
method innotes.views
.Let's go implement that.
Open up
notes/views.py
, and we'll implementcreate_note
. It'll need to make sure that it's recieving a POST request, and and also that the request contains values fortitle
andslug
.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).
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).
Next we need to write
update_note
, which is the view that will handle the submitted form data fromnote_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.
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 namedstyle.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.
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.
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'.
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 jQueryajax
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 valuefalse
. 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.
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.
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:
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.
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.
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.
Before we implement updating, we're going to take a brief detour to create a
display_success
function to complement thedisplay_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
anddisplay_error
functions. Open upmedia/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.
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 innotes.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 thedone
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
anddisplay_success
functions to handle most of the details.Finally, we have to modify the
title_to_span
andslug_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?
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 aboveajax_update_note
request, and modify theperform_update
function innotes/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.
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.
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:
- using additional views to improve the Ajax experience, and
- adding a verification layer to this application.
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.↩