Enhancing the User Interface with Ajax

Exclusive offer: get 50% off this eBook here
Learning Website Development with Django

Learning Website Development with Django — Save 50%

A beginner's tutorial to building web applications, quickly and cleanly, with the Django application framework

$23.99    $12.00
by Ayman Hourieh | June 2008 | Architecture & Analysis Content Management Open Source

The coming of Ajax was an important landmark in the history of Web 2.0. Ajax is a group of technologies that enable developers to build interactive, feature-rich web applications. Most of these technologies were available many years before Ajax itself. However, the advent of Ajax represents the transition of the web from static pages that need to be refreshed whenever data was exchanged to dynamic, responsive and interactive user interfaces.

In this article by Ayman Hourieh, you will learn about the following:

  • Ajax and the benefits of using it in web applications.
  • How to install an Ajax framework in Django.
  • How to use the Open Source jQuery framework.
  • Live searching of bookmarks.
  • Editing a bookmark in place without loading a separate page.
  • Auto-completion of tags when submitting a bookmark.

Since our project is a Web 2.0 application, it should be heavily focused on the user experience. The success of our application depends on getting users to post and share content on it. Therefore, the user interface of our application is one of our major concerns. This article will improve the interface of our application by introducing Ajax features, making it more user-friendly and interactive.

Ajax and Its Advantages

Ajax, which stands for Asynchronous JavaScript and XML, consists of the following technologies:

  • HTML and CSS for structuring and styling information.
  • JavaScript for accessing and manipulating information dynamically.
  • XMLHttpRequest, which is an object provided by modern browsers for exchanging data with the server without reloading the current web page.
  • A format for transferring data between the client and server. XML is sometimes used, but it could be HTML, plain text, or a JavaScript-based format called JSON.

Ajax technologies let code on the client-side exchange data with the server behind the scenes, without having to reload the entire page each time the user makes a request. By using Ajax, web developers are able to increase the interactivity and usability of web pages.

Ajax offers the following advantages when implemented in the right places:

  • Better user experience. With Ajax, the user can do a lot without refreshing the page, which brings web applications closer to regular desktop applications.
  • Better performance. By exchanging only the required data with the server, Ajax saves bandwidth and increases the application's speed.

There are numerous examples of web applications that use Ajax. Google Maps and Gmail are perhaps two of the most prominent examples. In fact, these two applications played an important role in spreading the adoption of Ajax, because of the success that they enjoyed. What sets Gmail from other web mail services is its user interface, which enables users to manage their emails interactively without waiting for a page reload after every action. This creates a better user experience and makes Gmail feel like a responsive and feature-rich application rather than a simple web site.

This article explains how to use Ajax with Django so as to make our application more responsive and user friendly. We are going to implement three of the most common Ajax features found in web applications today. But before that, we will learn about the benefits of using an Ajax framework as opposed to working with raw JavaScript functions.

Using an Ajax Framework in Django

In this section we will choose and install an Ajax framework in our application. This step isn't entirely necessary when using Ajax in Django, but it can greatly simplify working with Ajax. There are many advantages to using an Ajax framework:

  • JavaScript implementations vary from browser to browser. Some browsers provide more complete and feature-rich implementations, whereas others contain implementations that are incomplete or don't adhere to standards. Without an Ajax framework, the developer must keep track of browser support for the JavaScript features that they are using, and work around the limitations that are present in some browser implementations of JavaScript. On the other hand, when using an Ajax framework, the framework takes care of this for us; it abstracts access to the JavaScript implementation and deals with the differences and quirks of JavaScript across browsers. This way, we concentrate on developing features instead of worrying about browser differences and limitations.
  • The standard set of JavaScript functions and classes is a bit lacking for fully fledged web application development. Various common tasks require many lines of code even though they could have been wrapped in simple functions. Therefore, even if you decide not to use an Ajax framework, you will find yourself having to write a library of functions that encapsulates JavaScript facilities and makes them more usable. But why reinvent the wheel when there are many excellent Open Source libraries already available?

Ajax frameworks available on the market today range from comprehensive solutions that provide server-side and client-side components to light-weight client-side libraries that simplify working with JavaScript. Given that we are already using Django on the server-side, we only want a client-side framework. In addition, the framework should be easy to integrate with Django without requiring additional dependencies. And finally, it is preferable to pick a light and fast framework. There are many excellent frameworks that fulfil our requirements, such as Prototype, the Yahoo! UI Library and jQuery. I have worked with them all and they are all great. But for our application, I'm going to pick jQuery, because it's the lightest of the three. It also enjoys a very active development community and a wide range of plugins. If you already have experience with another framework, you can continue using it during this article. It is true that you will have to adapt the JavaScript code in this article to your framework, but Django code on the server-side will remain the same no matter which framework you choose.

Now that you know the benefits of using an Ajax framework, we will move to installing jQuery into our project.

Downloading and Installing jQuery

One of the advantages of jQuery is that it consists of a single light-weight file. To download it, head to http://jquery.com/ and choose the latest version (1.2.3 at the time of writing). You will find two choices:

  • Uncompressed version: This is the standard version that I recommend you to use during development. You will get a .js file with the library's code in it.
  • Compressed version: You will also get a .js file if you download this version. However, the code will look obfuscated. jQuery developers produce this version by applying many operations on the uncompressed .js file to reduce its size, such as removing white spaces and renaming variables, as well as many other techniques. This version is useful when you deploy your application, because it offers exactly the same features as the uncompressed one, but with a smaller file size.

I recommend the uncompressed version during development because you may want to look into jQuery's code and see how a particular method works. However, the two versions offer exactly the same set of features, and switching from one to another is just a matter of replacing one file.

Once you have the jquery-xxx.js file (where xxx is the version number), rename it to jquery.js and copy it to the site_media directory of our project (Remember that this directory holds static files which are not Python code). Next, you will have to include this file in the base template of our site. This will make jQuery available to all of our project pages. To do so, open templates/base.html and add the highlighted code to the head section in it:

<head>
<title>Django Bookmarks |
{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style.css"
type="text/css" />
<script type="text/javascript"
src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/jquery.js"></script>
</head>

To add your own JavaScript code to an HTML page, you can either put the code in a separate .js file and link it to the HTML page by using the script tag as above, or write the code directly in the body of a script tag:

<script type="text/javascript">
// JavaScript code goes here.
</script>

The first method, however, is recommended over the second one, because it helps keep the source tree organized by putting HTML and JavaScript code in different files. Since we are going to write our own .js files during this article, we need a way to link .js files to templates without having to edit base.html every time. We will do this by creating a template block in the head section of the base.html template. When a particular page wants to include its own JavaScript code, this block may be overridden to add the relevant script tag to the page. We will call this block external, because it is used to link external files to pages. Open templates/base.html and modify its head section as follows:

<head>
<title>Django Bookmarks | {% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style.css"
type="text/css"/>
<script type="text/javascript" src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/jquery.js">
</script>
{% block external %}{% endblock %}
</head>

And we have finished. From now on, when a view wants to use some JavaScript code, it can link a JavaScript file to its template by overriding the external template block.

Before we start to implement Ajax enhancements in our project, let's go through a quick introduction to the jQuery framework.

The jQuery JavaScript Framework

jQuery is a library of JavaScript functions that facilitates interacting with HTML documents and manipulating them. The library is designed to reduce the time and effort spent on writing code and achieving cross-browser compatibility, while at the same time taking full advantage of what JavaScript offers to build interactive and responsive web applications.

The general workflow of using jQuery consists of two steps:

  • Select an HTML element or a group of elements to work on.
  • Apply a jQuery method to the selected group

Element Selectors

jQuery provides a simple approach to select elements; it works by passing a CSS selector string to a function called $. Here are some examples to illustrate the usage of this function:

  • If you want to select all anchor (<a>) elements on a page, you can use the following function call: $("a")
  • If you want to select anchor elements which have the .title CSS class, use $("a.title")
  • To select an element whose ID is #nav, you can use $("#nav")
  • To select all list item (<li>) elements inside #nav, use $("#nav li")

And so on. The $() function constructs and returns a jQuery object. After that, you can call methods on this object to interact with the selected HTML elements.

jQuery Methods

jQuery offers a variety of methods to manipulate HTML documents. You can hide or show elements, attach event handlers to events, modify CSS properties, manipulate the page structure and, most importantly, perform Ajax requests.

Before we go through some of the most important methods, I highly recommend using the Firefox web browser and an extension called Firebug to experiment with jQuery. This extension provides a JavaScript console that is very similar to the interactive Python console. With it, you can enter JavaScript statements and see their output directly without having to create and edit files. To obtain Firebug, go to http://www.getfirebug.com/, and click on the install link. Depending on the security settings of Firefox, you may need to approve the website as a safe source of extensions.

If you do not want to use Firefox for any reason, Firebug's website offers a "lite" version of the extension for other browsers in the form of a JavaScript file. Download the file to the site_media directory, and then include it in the templates/base.html template as we did with jquery.js:

<head>
<title>Django Bookmarks | {% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/site_media/style.css"
type="text/css"/>
<script type="text/javascript" src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/firebug.js">
</script>
<script type="text/javascript" src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/jquery.js">
</script>
{% block external %}{% endblock %}
</head>

To experiment with the methods outlined in this section, launch the development server and navigate to the application's main page. Open the Firebug console by pressing F12, and try selecting elements and manipulating them.

Hiding and Showing Elements

Let's start with something simple. To hide an element on the page, call the hide() method on it. To show it again, call the show() method. For example, try this on the navigation menu of your application:

>>> $("#nav").hide()
>>> $("#nav").show()

Enhancing the User Interface with Ajax

You can also animate the element while hiding and showing it. Try the fadeOut(), fadeIn(), slideUp() or slideDown() methods to see two of these animated effects.

Of course, these methods (like all other jQuery methods) also work if you select more than one element at once. For example, if you open your user page and enter the following method call into the Firebug console, all of the tags will disappear:

>>> $('.tags').slideUp()

Accessing CSS Properties and HTML Attributes

Next, we will learn how to change CSS properties of elements. jQuery offers a method called css() for performing CSS operations. If you call this method with a CSS property name passed as a string, it returns the value of this property:

>>> $("#nav").css("display")
Result: "block"

If you pass a second argument to this method, it sets the specified CSS property of the selected element to the additional argument:

>>> $("#nav").css("font-size", "0.8em")
Result: <div id="nav" style="font-size: 0.8em;">

In fact, you can manipulate any HTML attribute and not just CSS properties. To do so, use the attr() method which works in a similar way to css(). Calling it with an attribute name returns the attribute value, whereas calling it with an attribute name/value pair sets the attribute to the passed value. To test this, go to the bookmark submission form and enter the following into the console:

>>> $("input").attr("size", "48")
Results:
<input id="id_url" type="text" size="48" name="url">
<input id="id_title" type="text" size="48" name="title">
<input id="id_tags" type="text" size="48" name="tags">

(Output may slightly differ depending on the versions of Firefox and Firebug). This will change the sizes of all input elements on the page at once to 48.

In addition, there are shortcut methods to get and set commonly used attributes, such as val() which returns the value of an input field when called without arguments, and sets this value to an argument if you pass one. There is also html() which controls the HTML code inside an element. Finally, there are two methods that can be used to attach or detach a CSS class to an element; they are called addClass() and removeClass(). A third method is provided to toggle a CSS class, and it is called toggleClass(). All of these class methods take the name of the class to be changed as a parameter.

Manipulating HTML Documents

Now that you are comfortable with manipulating HTML elements, let's see how to add new elements or remove existing elements. To insert HTML code before an element, use the before() method, and to insert code after an element, use the after() method. Notice how jQuery methods are well-named and very easy to remember!

Let's test these methods by inserting parentheses around tag lists on the user page. Open your user page and enter the following in the Firebug console:

>>> $(".tags").before("<strong>(</strong>")
>>> $(".tags").after("<strong>)</strong>")

You can pass any string you want to - before() or after() - the string may contain plain text, one HTML element or more. These methods offer a very flexible way to dynamically add HTML elements to an HTML document.

If you want to remove an element, use the remove() method. For example:

$("#nav").remove()

Not only does this method hide the element, it also removes it completely from the document tree. If you try to select the element again after using the remove() method, you will get an empty set:

>>> $("#nav")
Result: []

Of course, this only removes the elements from the current instance of the page. If you reload the page, the elements will appear again.

Traversing the Document Tree

Although CSS selectors offer a very powerful way to select elements, there are times when you want to traverse the document tree starting from a particular element. For this, jQuery provides several methods. The parent() method returns the parent of the currently selected element. The children() method returns all the immediate children of the selected element. Finally, the find() method returns all the descendants of the currently selected element. All of these methods take an optional CSS selector string to limit the result to elements that match the selector. For example, $("#nav").find ("li") returns all the <li> descendants of #nav.

If you want to access an individual element of a group, use the get() method which takes the index of the element as a parameter. $("li").get(0) for example returns the first <li> element out of the selected group.

Handling Events

Next, we will learn about event handlers. An event handler is a JavaScript function that is invoked when a particular event happens, for example, when a button is clicked or a form is submitted. jQuery provides a large set of methods to attach handlers to events; events of particular interest in our application are mouse clicks and form submissions. To handle the event of clicking on an element, we select this element and call the click() method on it. This method takes an event handler function as a parameter. Let's try this using the Firebug console. Open the main page of the application, and insert a button after the welcome message:

>>> $("p").after("<button id="test-button">Click me!</button>")

(Notice that we had to escape the quotations in the strings passed to the after() method.)

If you try to click this button, nothing will happen, so let's attach an event handler to it:

>>> $("#test-button").click(function () { alert("You clicked me!"); })

Now, when you click the button, a message box will appear. How did this work? The argument that we passed to click() may look a bit complicated, so let's examine it again:

function () { alert("You clicked me!"); }

This appears to be a function declaration but without a function name. Indeed, this construct creates what is called an anonymous function in JavaScript terminology, and it is used when you need to create a function on the fly and pass it as an argument to another function. We could have avoided using anonymous functions and declared the event handler as a regular function:

>>> function handler() { alert("You clicked me!"); }
>>> $("#test-button").click(handler)

The above code achieves the same effect, but the first one is more concise and compact. I highly recommend you to get used to anonymous functions in JavaScript (if you are not already), as I'm sure you will appreciate this construct and find it more readable after using it for a while.

Handling form submissions is very similar to handling mouse clicks. First, you select the form, and then you call the submit() method on it and pass the handler as an argument. We will use this method many times while adding Ajax features to our project in later sections.

Sending Ajax Requests

Before we finish this section, let's talk about Ajax requests. jQuery provides many ways to send Ajax requests to the server. There is, for example, the load() method which takes a URL and loads the page at this URL into the selected element. There are also methods for sending GET or POST requests, and receiving the results. We will examine these methods in more depth while implementing Ajax features in our project.

What Next?

This wraps up our quick introduction to jQuery. The information provided in this section will be enough to continue with this article, and once you finish the article, you will be able to implement many interesting Ajax features on your own. But please keep in mind that this jQuery introduction is only the tip of the iceberg. If you want a comprehensive treatment of the jQuery framework, I highly recommend the book "Learning jQuery" from Packt Publishing, as it covers jQuery in much more detail. You can find out more about the book at:

http://www.packtpub.com/jQuery

Implementing Live Searching of Bookmarks

We will start introducing Ajax into our application by implementing live searching. The idea behind this feature is simple: when the user types a few keywords into a text field and clicks search, a script works behind the scenes to fetch search results and present them on the same page. The search page does not reload, thus saving bandwidth, and providing a better and more responsive user experience.

Before we start implementing this, we need to keep in mind an important rule while working with Ajax: write your application so that it works without Ajax, and then introduce Ajax to it. If you do so, you ensure that everyone will be able to use your application, including users who don't have JavaScript enabled and those who use browsers without Ajax support.

Implementing Searching

So before we work with Ajax, let's write a simple view that searches bookmarks by title. First of all, we need to create a search form, so open bookmarks/forms.py and add the following class to it:

class SearchForm(forms.Form):
query = forms.CharField(
label='Enter a keyword to search for',
widget=forms.TextInput(attrs={'size': 32})
)

As you can see, it's a pretty straightforward form class with only one text field. This field will be used by the user to enter search keywords.

Next, let's create a view for searching. Open bookmarks/views.py and enter the following code into it:

def search_page(request):
form = SearchForm()
bookmarks = []
show_results = False
if request.GET.has_key('query'):
show_results = True
query = request.GET['query'].strip()
if query:
form = SearchForm({'query' : query})
bookmarks =
Bookmark.objects.filter (title__icontains=query)[:10]
variables = RequestContext(request, { 'form': form,
'bookmarks': bookmarks,
'show_results': show_results,
'show_tags': True,
'show_user': True
})
return render_to_response('search.html', variables)

Apart from a couple of method calls, the view should be very easy to understand. We first initialize three variables, form which holds the search form, bookmarks which holds the bookmarks that we will display in the search results, and show_results which is a Boolean flag. We use this flag to distinguish between two cases:

  • The search page was requested without a search query. In this case, we shouldn't display any search results, not even a "No bookmarks found" message.
  • The search page was requested with a search query. In this case, we display the search results, or a "No bookmarks found" message if the query does not match any bookmarks.

We need the show_results flag because the bookmarks variable alone is not enough to distinguish between the above two cases. bookmarks will empty when the search page is requested without a query, and it will also be empty when the query does not match any bookmarks.

Next, we check whether a query was sent by calling the has_key method on the request.GET dictionary:

if request.GET.has_key('query'):
show_results = True
query = request.GET['query'].strip()
if query:
form = SearchForm({'query' : query})
bookmarks = Bookmark.objects.filter(title__icontains=query)[:10]

We use GET instead of POST here because the search form does not create or change data; it merely queries the database, and the general rule is to use GET with forms that query the database, and POST with forms that create, change or delete records from the database.

If a query was submitted by the user, we set show_results to True and call strip() on the query string to ensure that it contains non-whitespace characters before we proceed with searching. If this is indeed the case, we bind the form to the query and retrieve a list of bookmarks that contain the query in their title. Searching is done by using a method called filter in Bookmark.objects. This is the first time that we have used this method; you can think of it as the equivalent of a SELECT statements in Django models. It receives the search criteria in its arguments and returns search results. The name of each argument must adhere to the following naming convention:

field__operator

Note that field and operator are separated by two underscores: field is the name of the field that we want to search by and operator is the lookup method that we want to use. Here is a list of the commonly-used operators:

  • exact: The value of the argument is an exact match of the field.
  • contains: The field contains the value of the argument.
  • startswith: The field starts with the value of the argument.
  • lt: The field is less than the value of the argument.
  • gt: The field is greater than the value of the argument.

Also, there are case-insensitive versions of the first three operators: iexact, icontains and istartswith.

After this explanation of the filter method, let's get back to our search view. We use the icontains operator to get a list of bookmarks that match the query and retrieve the first ten items using Python's list slicing syntax. Finally we pass all the variables to a template called search.html to render the search page.

Now create the search.html template in the templates directory with the following content:

{% extends "base.html" %}
{% block title %}Search Bookmarks{% endblock %}
{% block head %}Search Bookmarks{% endblock %}
{% block content %}
<form id="search-form" method="get" action=".">
{{ form.as_p }}
<input type="submit" value="search" />
</form>
<div id="search-results">
{% if show_results %}
{% include 'bookmark_list.html' %}
{% endif %}
</div>
{% endblock %}

The template consists of familiar aspects that we have used before. We build the results list by including the bookmark_list.html like we did when building the user and tag pages. We gave the search form an ID, and rendered the search results in a div identified by another ID so that we can interact with them using JavaScript later. Notice how many times the include template tag saved us from writing additional code? It also lets us modify the look of the bookmarks list by editing a single file. This Django template feature is indeed very helpful in organizing and managing templates.

Before you test the new view, add an entry for it in urls.py:

urlpatterns = patterns('',
# Browsing
(r'^$', main_page),
(r'^user/(w+)/$', user_page),
(r'^tag/([^s]+)/$', tag_page),
(r'^tag/$', tag_cloud_page),
(r'^search/$', search_page),
)

Now test the search view by navigating to http://127.0.0.1:8000/search/ and experiment with it. You can also add a link to it in the navigation menu if you want; edit templates/base.html and add the highlighted code:

<div id="nav">
<a href="/">home</a> |
{% if user.is_authenticated %}
<a href="/save/">submit</a> |
<a href="/search/">search</a> |
<a href="/user/{{ user.username }}/">
{{ user.username }}</a> |
<a href="/logout/">logout</a>
{% else %}
<a href="/login/">login</a> |
<a href="/register/">register</a>
{% endif %}
</div>

We now have a functional (albeit very basic) search page. Thanks to our modular code, the task will turn out to be much simpler than it may seem.

Implementing Live Searching

To implement live searching, we need to do two things:

  • Intercept and handle the event of submitting the search form. This can be done using the submit() method of jQuery.
  • Use Ajax to load the search results in the back scenes, and insert them into the page. This can be done using the load() method of jQuery as we will see next.

jQuery offers a method called load() that retrieves a page from the server and inserts its contents into the selected element. In its simplest form, the function takes the URL of the remote page to be loaded as a parameter.

First of all, let's modify our search view a little so that it only returns search results without the rest of the search page when it receives an additional GET variable called ajax. We do so to enable JavaScript code on the client-side to easily retrieve search results without the rest of the search page HTML. This can be done by simply using the bookmark_list.html template instead of search.html when request.GET contains the key ajax. Open bookmarks/views.py and modify search_page (towards the end) so that it becomes as follows:

def search_page(request):
[...]
variables = RequestContext(request, {
'form': form,
'bookmarks': bookmarks,
'show_results': show_results,
'show_tags': True,
'show_user': True
})
if request.GET.has_key('ajax'):
return render_to_response('bookmark_list.html', variables)
else:
return render_to_response('search.html', variables)

Next, create a file called search.js in the site_media directory and link it to templates/search.html like this:

{% extends "base.html" %}
{% block external %}
<script type="text/javascript" src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/search.js">
</script>
{% endblock %}
{% block title %}Search Bookmarks{% endblock %}
{% block head %}Search Bookmarks{% endblock %}
[...]

Now for the fun part! Let's create a function that loads search results and inserts them into the corresponding div. Write the following code into site_media/search.js:

function search_submit() {
var query = $("#id_query").val();
$("#search-results").load(
"/search/?ajax&query=" + encodeURIComponent(query)
);
return false;
}

Let's go through this function line by line:

  • The function first gets the query string from the text field using the val() method.
  • We use the load() method to get search results from the search_page view, and insert the search results into the #search-results div. The request URL is constructed by first calling encodeURIComponent on query, which works exactly like the urlencode filter we used in Django templates. Calling this function is important to ensure that the constructed URL remains valid even if the user enters special characters into the text field such as &. After escaping query, we concatenate it with /search/?ajax&query=. This URL invokes the search_page view and passes the GET variables ajax and query to it. The view returns search results, and the load() method in turn loads the results into the #search-results div.
  • We return false from the function to tell the browser not to submit the form after calling our handler. If we don't return false in the function, the browser will continue to submit the form as usual, and we don't want that.

One little detail remains; where and when to attach search_submit to the submit event of the search form? A rule of a thumb when writing JavaScript is that we cannot manipulate elements in the document tree before the document finishes loading. Therefore, our function must be invoked as soon as the search page is loaded. Fortunately for us, jQuery provides a method to execute a function when the HTML document is loaded. Let's utilize it by appending the following code to site_media/search.js:

$(document).ready(function () {
$("#search-form").submit(search_submit);
});

$(document) selects the document element of the current page. Notice that there are no quotations around document; it's a variable provided by the browser, not a string. ready() is a method that takes a function and executes it as soon as the selected element finishes loading. So in effect, we are telling jQuery to execute the passed function as soon as the HTML document is loaded. We pass an anonymous function to the ready() method; this function simply binds search_submit to the submit event of the form #search-form.

That's it. We've implemented live searching with less than fifteen lines of code. To test the new functionality, navigate to http://127.0.0.1:8000/search/, submit queries, and notice how the results are displayed without reloading the page:

Enhancing the User Interface with Ajax

The information covered in this section can be applied to any form that needs to be processed in the back scenes without reloading the page. You can, for example, create a comment form with a preview button that loads the preview in the same page without reloading. In the next section, we will enhance the user page to let users edit their bookmarks in place, without navigating away from the user page.

Editing Bookmarks in Place

Editing of posted content is a very common task in web sites. It's usually implemented by offering an edit link next to content. When clicked, this link takes the user to a form located on another page where content can be edited. When the user submits the form, they are redirected back to the content page.

Imagine, on the other hand, that you could edit content without navigating away from the content page. When you click edit, the content is replaced with a form. When you submit the form, it disappears and the updated content appears in its place. Everything happens on the same page; edit form rendering and submission are done using JavaScript and Ajax. Wouldn't such a workflow be more intuitive and responsive?

The technique described above is called in-place editing. It is now finding its way into web applications and becoming more common. We will implement this feature in our application by letting the user edit their bookmarks in place on the user page.

Since our application doesn't support the editing of bookmarks yet, we will implement this first, and then modify the editing procedure to work in place.

Implementing Bookmark Editing

We already have most of the parts that are needed to implement bookmark editing. This was easy to do thanks to the get_or_create method provided by data models. This little detail greatly simplifies the implementation of bookmark editing. Here is what we need to do:

  • We pass the URL of the bookmark that we want to edit as a GET variable named url to the bookmark_save_page view.
  • We modify bookmark_save_page so that it populates the fields of the bookmark form if it receives the GET variable. The form is populated with the data of the bookmark that corresponds to the passed URL.

When the populated form is submitted, the bookmark will be updated as we explained earlier, because it will look like the user submitted the same URL another time.

Before we implement the technique described above, let's reduce the size of bookmark_save_page by moving the part that saves a bookmark to a separate function. We will call this function _bookmark_save. The underscore at the beginning of the name tells Python not to import this function when the views module is imported. The function expects a request and a valid form object as parameters; it saves a bookmark out of the form data, and returns this bookmark. Open bookmarks/views.py and create the following function; you can cut and paste the code from bookmark_save_page if you like, as we are not making any changes to it except for the return statement at the end.

def _bookmark_save(request, form):
# Create or get link.
link, dummy =
Link.objects.get_or_create(url=form.clean_data['url'])
# Create or get bookmark.
bookmark, created = Bookmark.objects.get_or_create(
user=request.user,
link=link
)
# Update bookmark title.
bookmark.title = form.clean_data['title']
# If the bookmark is being updated, clear old tag list.
if not created:
bookmark.tag_set.clear()
# Create new tag list.
tag_names = form.clean_data['tags'].split()
for tag_name in tag_names:
tag, dummy = Tag.objects.get_or_create(name=tag_name)
bookmark.tag_set.add(tag)
# Save bookmark to database and return it.
bookmark.save()
return bookmark

Now in the same file, replace the code that you removed from bookmark_save_page with a call to _bookmark_save:

@login_required
def bookmark_save_page(request):
if request.method == 'POST':
form = BookmarkSaveForm(request.POST)
if form.is_valid():
bookmark = _bookmark_save(request, form)
return HttpResponseRedirect(
'/user/%s/' % request.user.username
)
else:
form = BookmarkSaveForm()
variables = RequestContext(request, {
'form': form
})
return render_to_response('bookmark_save.html', variables)

The current logic in bookmark_save_page works like this:

if there is POST data:
Validate and save bookmark.
Redirect to user page.
else:
Create an empty form.
Render page.

To implement bookmark editing, we need to slightly modify the logic as follows:

if there is POST data:
Validate and save bookmark.
Redirect to user page.
else if there is a URL in GET data:
Create a form an populate it with the URL's bookmark.
else:
Create an empty form.
Render page.

Let's translate the above pseudo code into Python. Modify bookmark_save_page in bookmarks/views.py so that it looks like the following (new code is highlighted):

from django.core.exceptions import ObjectDoesNotExist
@login_required
def bookmark_save_page(request):
if request.method == 'POST':
form = BookmarkSaveForm(request.POST)
if form.is_valid():
bookmark = _bookmark_save(request, form)
return HttpResponseRedirect(
'/user/%s/' % request.user.username
)
elif request.GET.has_key('url'):
url = request.GET['url']
title = ''
tags = ''
try:
link = Link.objects.get(url=url)
bookmark = Bookmark.objects.get(
link=link,
user=request.user
)
title = bookmark.title
tags = ' '.join(
tag.name for tag in bookmark.tag_set.all()
)
except ObjectDoesNotExist:
pass
form = BookmarkSaveForm({
'url': url,
'title': title,
'tags': tags
})
else:
form = BookmarkSaveForm()
variables = RequestContext(request, {
'form': form
})
return render_to_response('bookmark_save.html', variables)

This new section of the code first checks whether a GET variable called url exists. If this is the case, it loads the corresponding Link and Bookmark objects of this URL, and binds all the data to a bookmark saving form. You may wonder why we load the Link and Bookmark objects in a try-except construct that silently ignores exceptions. Indeed, it's perfectly valid to raise an Http404 exception if no bookmark was found for the requested URL. But our code chooses to only populate the URL field in this situation, leaving the title and tags fields empty.

Learning Website Development with Django A beginner's tutorial to building web applications, quickly and cleanly, with the Django application framework
Published: April 2008
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Now, let's add edit links next to each bookmark in the user page. Open templates/bookmark_list.html and insert the highlighted code:

{% if bookmarks %}
<ul class="bookmarks">
{% for bookmark in bookmarks %}
<li>
<a href="{{ bookmark.link.url }}" class="title">
{{ bookmark.title|escape }}</a>
{% if show_edit %}
<a href="/save/?url={{ bookmark.link.url|urlencode }}"
class="edit">[edit]</a>
{% endif %}
<br />
{% if show_tags %}
Tags:
{% if bookmark.tag_set.all %}
<ul class="tags">
{% for tag in bookmark.tag_set.all %}
<li><a href="/tag/{{ tag.name|urlencode }}/">
{{ tag.name|escape }}</a></li>
{% endfor %}
</ul>
{% else %}
None.
{% endif %}
<br />
[...]

Notice how we constructed edit links by appending the bookmark's URL to /save/?url=. Also, since we only want to show edit links on the user's page, the template renders these links only when the show_edit flag is set to True. Otherwise, it wouldn't make sense to let the user edit other people's links. Now open bookmarks/views.py and add the show_edit flag to template variables in user_page:

def user_page(request, username):
user = get_object_or_404(User, username=username)
bookmarks = user.bookmark_set.order_by('-id')
variables = RequestContext(request, {
'bookmarks': bookmarks,
'username': username,
'show_tags': True,
'show_edit': username == request.user.username,
})
return render_to_response('user_page.html', variables)

The expression username == request.user.username evaluates to True only when the user is viewing their own page, and this is precisely what we want.

Finally, I suggest reducing the font size of edit links a little. Open site_media/style.css and append the following to its end:

ul.bookmarks .edit {
font-size: 70%;
}

And we are done! Feel free to navigate to your user page and experiment with editing your bookmarks before we continue.

Implementing In-Place Editing of Bookmarks

Now that we have bookmark editing implemented, let's move to the exciting part: adding in-place editing with Ajax!

Our approach to this task will be as follows:

  • We intercept the event of clicking on an edit link, and use Ajax to load a bookmark editing form from the server. Then we replace the bookmark on the page with the editing form.
  • When the user submits the edit form, we intercept the submission event, and use Ajax to send the updated bookmark to the server. The server saves the bookmark and returns the HTML representation of the new bookmark. We replace the edit form on the page with the markup returned by the server.

We will implement the above using an approach very similar to live searching. First we modify bookmark_save_page so that it responds to Ajax requests when a GET variable called ajax exists. Next, we write JavaScript code to retrieve an edit form from the view, which posts bookmark data back to the server when the user submits this form.

Since we want to return the markup of an edit form to the Ajax script from the bookmark_save_page view, let's restructure our templates a little. Create a file called bookmark_save_form.html in templates, and move the bookmark saving form from bookmark_save.html to this new file:

<form id="save-form" method="post" action="/save/">
{{ form.as_p }}
<input type="submit" value="save" />
</form>

Notice that we also changed the action attribute of the form to /save/ and gave it an ID. This is necessary for the form to work on the user page as well as on the bookmark submission page.

Next, include this new template in bookmark_save.html:

{% extends "base.html" %}
{% block title %}Save Bookmark{% endblock %}
{% block head %}Save Bookmark{% endblock %}
{% block content %}
{% include 'bookmark_save_form.html' %}
{% endblock %}

OK, now we have the form in a separate template. Let's update the bookmark_save_page view to handle both normal and Ajax requests. Open bookmarks/views.py and update the view to look like the following (modified with the new lines are highlighted):

def bookmark_save_page(request):
ajax = request.GET.has_key('ajax')
if request.method == 'POST':
form = BookmarkSaveForm(request.POST)
if form.is_valid():
bookmark = _bookmark_save(form)
if ajax:
variables = RequestContext(request, {
'bookmarks': [bookmark],
'show_edit': True,
'show_tags': True
})
return render_to_response('bookmark_list.html', variables)
else:
return HttpResponseRedirect(
'/user/%s/' % request.user.username
)
else:
if ajax:
return HttpResponse('failure')
elif request.GET.has_key('url'):
url = request.GET['url']
title = ''
tags = ''
try:
link = Link.objects.get(url=url)
bookmark = Bookmark.objects.get(link=link, user=request.user)
title = bookmark.title
tags = ' '.join(tag.name for tag in bookmark.tag_set.all())
except:
pass
form = BookmarkSaveForm({
'url': url,
'title': title,
'tags': tags
})
else:
form = BookmarkSaveForm()
variables = RequestContext(request, {
'form': form
})
if ajax:
return render_to_response(
'bookmark_save_form.html',
variables
)
else:
return render_to_response(
'bookmark_save.html',
variables
)

Let's examine each highlighted section separately:

ajax = request.GET.has_key('ajax')

At the beginning of the method, we check whether a GET variable named ajax exists. We store the result of the check in a variable called ajax. Later in the method, we can check whether we are handling an Ajax request or not by using this variable in an if condition:

if form.is_valid():
bookmark = _bookmark_save(form)
if ajax:
variables = RequestContext(request, {
'bookmarks': [bookmark],
'show_edit': True,
'show_tags': True
})
return render_to_response('bookmark_list.html', variables)
else:
return HttpResponseRedirect('/user/%s/' % request.user.username)
else:
if ajax:
return HttpResponse('failure')

If we receive a POST request, we check whether the submitted form is valid or not. If it is valid, we save the bookmark. Next we check if this is an Ajax request. If it is, we render the saved bookmark using the bookmark_list.html template and return it to the requesting script. Otherwise, it is a normal form submission, so we redirect the user to their user page. On the other hand, if the form is not valid, we only act if it's an Ajax request by returning the string "failure", which we will respond to by displaying an error dialog in JavaScript. We don't need to do anything if it's a normal request because the page will be reloaded and the form will display any errors in the input.

if ajax:
return render_to_response('bookmark_save_form.html', variables)
else:
return render_to_response('bookmark_save.html', variables)

This check is done at the end of the method. Execution reaches this point if there is no POST data, which means that we should render a form and return it. We use the bookmark_save_form.html template if it's an Ajax request, and bookmark_save.html otherwise.

Our view is now ready to serve Ajax requests as well as normal page requests. Let's write the JavaScript code that will take advantage of the updated view. Create a new file called bookmark_edit.js in site_media. But before we add any code to it, let's link bookmark_edit.js to the user_page.html template. Open user_page.html and modify it as follows:

{% extends "base.html" %}
{% block external %}
<script type="text/javascript" src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/bookmark_edit.js">
</script>
{% endblock %}
{% block title %}{{ username }}{% endblock %}
{% block head %}Bookmarks for {{ username }}{% endblock %}
{% block content %}
{% include 'bookmark_list.html' %}
{% endblock %}

We have to write two functions in bookmark_edit.js:

  • bookmark_edit: The function handles clicks on edit links; it loads an edit form from the server, and replaces the bookmark with this form.
  • bookmark_save: The function handles the submissions of edit forms; sends form data to the server, and replaces the form with the bookmark HTML returned by the server.

Let's start with the first function. Open site_media/bookmark_edit.js and write the following code in it:

function bookmark_edit() {
var item = $(this).parent();
var url = item.find(".title").attr("href");
item.load("/save/?ajax&url=" + escape(url), null, function () {
$("#save-form").submit(bookmark_save);
});
return false;
}

Because this function handles click events on an edit link, the variable this refers to the edit link itself. Wrapping it in the jQuery $() function and calling parent() returns the parent of the edit link, which is the element of the bookmark (try it in the Firebug console to see for yourself).

After retrieving a reference to the bookmark's element, we obtain a reference to the bookmark's <li> title, and extract the bookmark's URL from it using the attr() method.

Next, we use the load() method to put an editing form in place of the bookmark's HTML. This time we are calling load() with two extra argum

Learning Website Development with Django A beginner's tutorial to building web applications, quickly and cleanly, with the Django application framework
Published: April 2008
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

If you try to edit a bookmark in the user page after writing this function, an edit form should appear, but you should also get a JavaScript error message in the Firebug console because the function bookmark_save is not defined, so let's write it:

function bookmark_save() {
var item = $(this).parent();
var data = {
url: item.find("#id_url").val(),
title: item.find("#id_title").val(),
tags: item.find("#id_tags").val()
};
$.post("/save/?ajax", data, function (result) {
if (result != "failure") {
item.before($("li", result).get(0));
item.remove();
$("ul.bookmarks .edit").click(bookmark_edit);
}
else {
alert("Failed to validate bookmark before saving.");
}
});
return false;
}

Here, the variable this refers to the edit form because we are handling the event of submitting a form. The function starts by retrieving a reference to the form's parent, which is again the bookmark's <li> element. Next, the function retrieves the updated data from the form, using the ID of each form field and the val() method. Then it uses a method called $.post() to send data back to the server. Finally, it returns false to prevent the browser from submitting the form.

As you may have guessed, $.post() is a jQuery method that sends POST requests to the server; it takes three parameters:

  • The URL of the target of the POST request.
  • An object of key/value pairs that represent POST data
  • A function that is invoked when the request is done. Server response is passed to this function as a string parameter.

It's worth mentioning that jQuery provides a method called $.get() for sending a GET request to the server. It takes the same types of parameters as $.post().

We use $.post() to send the updated bookmark data to the bookmark_save_page view. As discussed a few paragraphs ago, the view returns the update bookmark HTML if it succeeds in saving it. Otherwise, it returns the string "failure". Therefore, we check whether the result returned from the server is "failure" or not. If the request succeeded, we insert the new bookmark before the old one using the before() method, and remove the old bookmark from the HTML document using the remove(). If, on the other hand, the request fails, we display an alert box saying so.

Several little things remain before we finish this section: Why do we insert $("li", result).get(0) instead of result itself? If you check the bookmark_save_page view, you will see that it uses the bookmark_list.html template to construct the bookmark's HTML. However, bookmark_list.html returns the bookmark element wrapped in a tag. Basically, $("li", result).get(0) tells jQuery to extract the first element in result, and this is the element that we want. As you see from this snippet, you can use the jQuery $() function to select elements from an HTML string by passing this string as a second argument to the function.

bookmark_submit is attached to its event from within bookmark_edit, so we don't need to do anything about it in $(document).ready().

Lastly, after loading the updated bookmark into the page, we call $("ul.bookmarks .edit").click(bookmark_edit) again to attach bookmark_edit to the newly-loaded edit link. If you don't do so, and try to edit a bookmark twice, the second click on the edit link will take you to a separate form page.

When you have finished writing the JavaScript code, open your browser and go to your user page to experiment with the new feature. Edit the bookmarks, save them and notice how the changes are immediately reflected on the page without any reloading:

Enhancing the User Interface with Ajax

Now that you have completed this section, you should have a good understanding of how in-place editing is implemented. There are many other scenarios where this feature can be useful; for example, it can be used to edit an article or a comment on the same page without navigating away to a form located on a different URL.

In the next section, we will implement a third common Ajax feature that helps the user enter tags when submitting a bookmark.

Auto-Completion of Tags

The last Ajax enhancement that we are going to implement in this article is auto-completion of tags. The concept of auto-completion found its way into web applications when Google released their Suggest searching interface. Suggest works by displaying the most popular search queries below the search input field based on what the user has typed so far. It's also similar to how code editors in integrated development environments offer code completion suggestions based on what you type. This feature saves time by letting the user type a few characters of the word they want and then select it from a list, without having to type it in completely.

We will implement this feature by offering suggestions when the user enters tags while submitting a bookmark, but instead of writing this feature from scratch, we are going to use a jQuery plugin to implement it. jQuery enjoys a large and continually growing list of plugins that provides a variety of features. Installing a plugin is no different from installing jQuery itself. You download one (or more) files and link them to your template, and then you write a few lines of JavaScript code to activate the plugin.

You can browse the list of available jQuery plugins by pointing your browser to http://docs.jquery.com/Plugins. Search for the auto-complete plugin in the list, and download it. Or you can directly grab it from the following URL:

http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/

You will get a zip archive with many files in it. Extract the following files (which can be found in the directory jquery/autocomplete/scroll) to the site_media directory:

  • jquery.autocomplete.css
  • dimensions.js
  • jquery.bgiframe.min.js
  • jquery.autocomplete.js

Since we want to offer the autocomplete feature on the bookmark submission page, create an empty file called tag_autocomplete.js in site_media. Then open templates/bookmark_save.html and link all of the above files to it:

{% extends "base.html" %}
{% block external %}
<link rel="stylesheet"
href="/site_media/jquery.autocomplete.css" type="text/css" />
<script type="text/javascript"
src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/dimensions.js"> </script>
<script type="text/javascript"
src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/jquery.bgiframe.min.js"> </script>
<script type="text/javascript"
src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/jquery.autocomplete.js"> </script>
<script type="text/javascript"
src='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="/site_media/tag_autocomplete.js"> </script>
{% endblock %}
{% block title %}Save Bookmark{% endblock %}
{% block head %}Save Bookmark{% endblock %}
[...]

We have now finished installing the plugin. If you read its documentation, you will find that this plugin is activated by calling a method named autocomplete() on a selected input element. autocomplete() takes the following parameters.

  • A server-side URL. The plugin sends a GET request to this URL with what has been typed so far, and expects the server to return a set of suggestions.
  • An object that can be used to specify various options. Ones that are of interest to us are multiple, which is a Boolean variable that tells the plugin that the input field is used to enter multiple values (remembering that we use the same text field to enter all tags), and multipleSeparator, which is used to tell the plugin which string separates multiple entries. In our case, it's a single space character.

So before activating the plugin, we need to write a view that receives user input and returns a set of suggestions. Open bookmarks/views.py and append the following to its end:

def ajax_tag_autocomplete(request):
if request.GET.has_key('q'):
tags =
Tag.objects.filter(name__istartswith=request.GET['q'])[:10]
return HttpResponse('n'.join(tag.name for tag in tags))
return HttpResponse()

The autocomplete plugin sends user input in a GET variable named q. Therefore, we check that this variable exists, and build a list of tags whose names begin with the value of this variable. This is done using the filter method and the istartswith operator we learned about earlier this article. We only take the first ten results to avoid overwhelming the user with suggestions, and to reduce bandwidth and performance costs. Finally, we join the suggestions into a single string separated by newlines, wrap the string into an HttpResponse object, and return it.

With the suggestion view ready, create a URL entry to it in urls.py:

urlpatterns = patterns('',
# Ajax
(r'^ajax/tag/autocomplete/$', ajax_tag_autocomplete),
)

Now, activate the plugin on the tags input field by entering the following code into site_media/tag_autocomplete.js:

$(document).ready(function () {
$("#id_tags").autocomplete(
'/ajax/tag/autocomplete/',
{multiple: true, multipleSeparator: ' '}
);
});

The code passed an anonymous function to $(document).ready(). This function invokes autocomplete() on the tags input field, passing the arguments that we talked about earlier.

These few lines of code are all that we need in order to implement auto-completion of tags. To test the new feature, navigate to the bookmark submission form at http://127.0.0.1:8000/save/ and try to enter a character or two into the tags field. Suggestions should appear based on the tags available in your database:

Enhancing the User Interface with Ajax

With this feature, we finish the article. We have covered a lot of material and have learned about many exciting technologies and techniques. After reading the article, you should be able to think of and implement many other enhancements to the user interface, such as the ability to delete bookmarks from the user page or to do live browsing of bookmarks by tags among many other things.

Summary

Phew, this was a long article, but hopefully, you have learned a lot from it! We started the article with learning about the jQuery framework and how to integrate it into our Django project. After that, we implemented three exciting features into our bookmarking application: live searching, in place editing and auto-completion.

About the Author :


Ayman Hourieh

Ayman Hourieh holds a bachelor degree in Computer Science. He joined the engineering team at Google in January 2008. Prior to that, he worked with web application development for more than two years. In addition, he has been contributing to several Open Source projects such as Mozilla Firefox. Ayman also worked as a teaching assistant in Computer Science courses for one year. Even after working with a variety of technologies, Python remains Ayman's favorite programming language. He found Django to be a powerful and flexible Python framework that helps developers to produce high-quality web applications in a short time.

Books From Packt

BPEL Cookbook: Best Practices for SOA-based integration and composite applications development
BPEL Cookbook: Best Practices for SOA-based integration and composite applications development

Building SOA-Based Composite Applications Using NetBeans IDE 6
Building SOA-Based Composite Applications Using NetBeans IDE 6

Catalyst
Catalyst

Documentum Content Management Foundations: EMC Proven Professional Certification Exam E20-120 Study Guide
Documentum Content Management Foundations: EMC Proven Professional Certification Exam E20-120 Study Guide

Xen Virtualization
Xen Virtualization

Visual SourceSafe 2005 Software Configuration Management in Practice
Visual SourceSafe 2005 Software Configuration Management in Practice

Windows Server 2003 Active Directory Design and Implementation: Creating, Migrating, and Merging Networks
Windows Server 2003 Active Directory Design and Implementation: Creating, Migrating, and Merging Networks

Lotus Notes Domino 8: Upgrader's Guide
Lotus Notes Domino 8: Upgrader's Guide

 


 

 

No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
r
S
q
y
c
5
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software