Django And JQuery AJAX, Part 1: Loading Paginated Subpages

This tutorial teaches a simple way to use JQuery's AJAX functionality to integrate paginated webpages, represented by Django's ListView, into the DOM of another webpage. The implementation makes very few assumptions about the included page, using its html code rather than json objects, which would require further processing. This is meant to make usage of the script very straightforward even with little knowledge of what the included webpage or its data look like. The backend is written in Django and uses some of its useful features to keep the page integration DRY and consistent. However the JQuery part by itself can be used in combination with other frameworks as well, or even with plain PHP pages. The pagination of the original, included page will be converted into an expanding list on the AJAX page, where additional pages are subsequently added to the end of the previously loaded pages. So let's get started. I will call the page, that will be included, the source page and the page that uses JQuery to integrate it, the target page.

Step 1. The Django View

First we need to create the source page. Pages are represented in Django by Views. I will use a ListView as an example for this article, because they usually do have pagination. Non-paginated views could be viewed as a subcase of paginated views, that only have one page. So the ListView is the more general and more interesting case for the purposes of this article. The following code snippet shows a very standard ListView in Django.

from django.views.generic.list import ListView
from .models import MyModel

class MyModelListView(ListView):
    model = MyModel
    template_name = "myapp/mymodel_list.html"
    context_object_name = "mymodel_list"
    paginate_by = 10

(Snippet)

The model variable gives the class name of a Model defined in models.py. Since the view is derived from ListView, it will internally create a QuerySet for objects of the model class. This queryset can be accessed in the templates under the context_object_name, which is defined as "mymodel_list". The pagination is defined by the paginate_by parameter. The paginate_by value of 10 means that each page contains a maximum of 10 MyModel objects. If the database contains more objects than that, they will be paginated, and each page can be accessed by adding a page parameter to the GET query string. So a query string of ?page=2 added to the URL would show the second page and so on. The URL for the view is defined in urls.py.

from django.conf.urls import include, url
from .views import MyModelListView

urlpatterns = [
    url(r'^mymodels/?$', MyModelListView.as_view(), name='mymodels_list'),
]

(Snippet)

It's important to define a name parameter in the url call. The URL for the page should only be referred to by its name in templates or in python code, as we will see later.

Finally, the last step in setting up our paginated "source page" for our AJAX project, is to fill in the HTML code through the template defined in MyModelListView.template_name. The templates reside in the templates subfolder of the app, so the full path is "templates/myapp/mymodel_list.html". It will contain the blueprint for the full HTML code, with a section to show the objects from the QuerySet.

{% for item in mymodel_list %}
    {# print all relevant information per object here #}
{% empty %}
    {# show an empty page message here #}
{% endfor %}

(Snippet)

Note that "mymodel_list" was defined in MyModelListView.context_object_name.

That's it. We now have a paginated webpage, ready to be integrated into the target page via JQuery Ajax calls and DOM manipulation. Let's see how to do just that in the next step.

Step 2. The Ajax Script

We need three elements on the target page. A div to hold the source page, a hidden element to count the pagination page number, and a button to trigger the Ajax calls and load additional pages.

<div id='pagediv-id'></div>
<div><input type='hidden' id='pagination-id' value='1'></div>
<button id='load-id' type='submit'>Show</button>

(Snippet)

The source pages will be attached to #pagediv-id, the pagination counter #pagination-id is initialized to 1, and #load-id is the button that will trigger the loading of additional pages.

Next we need to bind a loading function to the the load button.

$("#load-id").click(function(){
    load_page(
        "/myapp/mymodels/",
        "#pagination-id",
        "#load-id",
        "#pagediv-id",
      );
});

(Snippet)

This binds the load_page function to the button's click event, and passes the source page URL and the three element IDs that we defined and placed in the target page earlier. In order to make it work, we have to define the load_page function in a ajax.js file first. This is the code.

function load_page(
    page_url, paginationfield_id, loadbutton_id, pagediv_id,
){
    page = parseInt($(paginationfield_id).val());
    
    $(loadbutton_id).prop("disabled", true);
    $(loadbutton_id).text("Loading ...");
    
    $.ajax({
        async: true,
        type: "GET",
        url: page_url,
        data: { page: page },
        error: function() {
                $(loadbutton_id).replaceWith("<p></p>");
            },
        success: function(data){ // check if there is an additional page
                                // , disable load button if not
                $.ajax({
                    async: true,
                    type: "HEAD",
                    url: page_url,
                    data: { page: page + 1 },
                    error: function(data){
                            $(loadbutton_id).replaceWith("<p>No more data</p>");
                        },
                    success: function(response){
                            $(loadbutton_id).text("Load more");
                            $(paginationfield_id).val(page + 1);
                            $(loadbutton_id).prop("disabled", false);
                        }
                });
                $(pagediv_id).append($(data).find("div"));
            }
    });
}

(Snippet)

So what is going on here? First the page number is converted from the string value in the pagination element to an integer, so we can increment it later. Then the load button is disabled and the button text is changed to "Loading .." to inform the user about the progress of the action. This is done to prevent the user from clicking the button several times before the script can finish its job. The script works in asynchronous mode. If the script were synchronous the user would be blocked from interacting with the entire page until the script is finished. However by using asynchronous mode and then just disabling the button, the user can still interact with the rest of the page, while the content from the source page is fetched in the background. The content is fetched by the JQuery $.ajax(..) call. The $.ajax() function is passed a Javascript object, that holds the actual parameters we need to pass as its object fields or attributes. The url of the source page is passed in the url field, and the page number is passed in the data field. The data field contains the key-value pairs of the query string used by the Ajax call.

The callbacks

As I said earlier, the $.ajax() call works asynchronously, so the follow up actions to fetching the content from the source page cannot be placed sequentially after the $.ajax call in the script. Instead they're handled by callbacks. These callbacks are defined by the "success" and the "error" parameters to the $.ajax() call. The error function is very simple and just removes the load button from the page, since it's not working. Instead of removing the button it could also be replaced with an error message for example, by passing an appropriate element string to the replaceWith() method.

The success callback does a little more work than the error handler. First it spawns off another $.ajax() call, which I'll get to right away. Then while this new $.ajax() call is doing its work, the original callback continues by adding (.append()) the data it received, in other words the new source page, to the #pagediv-id element, which is the placeholder for the source page content. Remember that at this point the loadbutton is still disabled and we haven't yet talked about how to reenable it. That's where the nested $.ajax() call inside the success function of the first $.ajax() call comes into play. The point of this second Ajax request is to ask the server if there are more pages or not. If there are more pages, then the load button should be re-enabled. On the other hand if there are no more pages, the load button should be replaced with something else that indicates to the user that there are no more pages to load. So long story short, the second $.ajax call does a HEAD request for the follow up page (page+1). If the request fails, that means there are no more pages to load, and that fact is displayed to the user with a "No more data" message, that replaces the load button. If the HEAD request however indicates that the follow up page exists, a few updates are necessary. The "#pagination-id" value must be updated to the next page number and the load button must be reenabled. This way the next time the user clicks the load button it will load the next page.

One thing to note, that I didn't explicitly explain, is the way the data returned by the GET request are added to the #pagediv-id element. This was the code

$(pagediv_id).append($(data).find("div"));

The response data are not added directly to the target, but only the descndant div elements are added. This isn't the only possible way to filter the result before appending them to the target div, and obviously it depends on what part of the the page that you're loading you actually want to display. In this example I added the descendant divs, but if your needs are different, you would obviously apply a different filter. The reason the data usually cannot be added directly as they are, is that the Ajax request could return a full HTML page, and you cannot have a <html> element inside of the <html> element of the target page. Another way around this problem would be to filter the data on the server side. In that case the server would have to know that the request is an Ajax request, so it can serve the filtered data, ready for insertion in the target page, as opposed to returning the full page. In Django this distinction between direct (full page) requests and Ajax requests can be made by the request.is_ajax method. So if you would prefer to handle the filtering on the server side, you can use this method.

Step 3. Script Integration Via Template Includes And Template Tags

At this point you should be able to integrate paginated webpages into any other page you wish. The main function lives inside a ajax.js file and is called from within the target webpage with the appropriate parameters. This last step of calling the script with different parameters on the target webpages however could become problematic in the future. Let's say the load_page function is used on many pages, each of which has its own script section passing parameters to that function. What happens if you want to change the signature of the load_page? If you just change it in the ajax.js file, it might break all the pages that were calling the function in its old format. For example if you replace the parameters with just one object, that contains the parameters like a dictionary, all pages that still call the function by passing the parameters directly, won't work anymore.

A first solution to this problem is to use a template include. Make sure to define all parameters in a Django "with" tag and include the template.

{% url "mymodels_list" as page_url %}
{% with page_url=page_url pagination_id="#pagination-id" loadbutton_id="#load-id" pagediv_id="#pagediv-id" %}
    {% include loadpage.html %}
{% endwith %}

(Snippet)

And in loadpage.html just call load_page using these variables

$("{{ loadbutton_id }}").click(function(){
    load_page(
        {{ page_url }},
        {{ pagination_id|default:"#pagination-id" }},
        {{ loadbutton_id|default:"#load-id" }},
        {{ pagediv_id|default:"#pagediv-id" }},
      );
});

(Snippet)

Make sure the script is inside a <script> block.

Since we have provided default values for all parameters except the page_url, these other variables can be left undefined when including loadpage.html. "page_url" however must be defined or it won't work. This script must be added to the target page after JQuery and the file containing load_page have been added. Usually these scripts are loaded at the very bottom of the page, so that they don't slow down the loading of the rest of the page. Therefore we need a separate Django  template, to place the div elements and the loadbutton higher up on the page. Let's call this separate template "loadpage_elems.html".

<div id='{{ pagediv_id|default:"#pagediv-id" }}'></div>
<input type='hidden' id='{{ pagination_id|default:"#pagination-id" }}' value='1'>
<button id='{{ loadbutton_id|default:"#load-id" }}' type='submit'>Show</button>

(Snippet)

And it would be included on the template of the target page very similar to the way loadpage.html was included. However we're beginning to see a problem with this approach. Right now three variable names - pagediv_id, pagination_id, and loadbutton_id - have to be kept consistent in two independent files, loadpage.html and loadpage_elems.html. The default values are defined twice, which is redundant and difficult to maintain. When including the templates and changing the values, they have to be defined twice, when calling loadpage.html and when calling loadpage_elems.html. Again this is redundant and a possible source of errors and confusion. That problem is only exacerbated, if we want to add more flexibility, like for example the flexibility to place the load button separately from the div that holds the source contents. Now we have at least 3 different files to maintain and keep consistent. And the problem gets worse if we want to add more parameters over time, that affect several files. What we want is a DRY'er approach to the problem.

Template Tags To The Rescue

Django provides an elegant way that we can use to overcome these aforementioned problems with template includes. Template tags. More specifically assignment tags. An assignment tag is called from within a template, taking any number of arguments that are required, and returns an object to the template. In our case, we want to pass the url_page, pagediv_id, pagination_id, and loadbutton_id to the template tag and receive an object, that will let us write the elements and the script to the page markup, as and where we wish. The returned object itself takes care of consistency by encapsulating the data it needs. This object could be a class object or a simple dictionary. For simplicity I will use a dictionary as an example here.

Templatetags are defined in a templatetags subdirectory of the app and made available to the templates by loading them by the name of the python file. So if there is a file called ajax.py in the templatetags folder, the templatetag functions inside can be made available by using {% load ajax %} inside the template. That's all there is to do. So let's have a look at how we can define our assignment tag inside ajax.py.

from django import template
register = template.Library()
@register.assignment_tag
def get_pageloader(page_url, *args, **kwargs):
    
    pagediv_id = kwargs.get("pagediv_id", "#pagediv-id")
    loadbutton_id = kwargs.get("loadbutton_id", "#load-id")
    pagination_id = kwargs.get("pagination_id", "#pagination-id")
    
    button_script = """
                        $("#{loadbutton_id}").click(function(){
                            load_page(
                                '{page_url},
                                '{pagination_id},
                                '{loadbutton_id},
                                '{pagediv_id},
                              );
                        });
                     """.format(
                                page_url = page_url,
                                pagediv_id = page_div_id,
                                pagination_id = pagination_id,
                                loadbutton_id = loadbutton_id,
                        )
    
    return {
                "pagediv": "<div id='" + pagediv_id + "'></div>",
                "loadbutton": "<button id='" + loadbutton_id
                                + "' type='submit'>Show</button>",
                "pagination": "<input type='hidden' id='"
                                + pagination_id + "' value='1'>",
                "button_script": button_script,
            }

(Snippet)

The first three lines show how to turn a function into an assignment tag. You can check the Django documentation on template tags if you want to know more. The function itself takes a required page_url parameter and an open ended list of keyword arguments. The kwargs.get(name, default) calls read the values that were passed to the function and set a default value if a value doesn't exist in the kwargs dictionary. The dictionary returned by the function contains the elements and scripts required by the template to make the Ajax calls work.

On the template side the template tag would be used as in the following code snippet.

{% load ajax %}
{% url "mymodels_list" as page_url %}
{% get_pageloader page_url as pageloader %}
                    
{{ pageloader.pagediv|safe }}
{{ pageloader.loadbutton|safe }}
{{ pageloader.pagination|safe }}

(Snippet)

The pageloader dictionary is loaded into a variable called pageloader. And then the page elements are read from it and placed on the page. Then further down at the bottom of the html page the script would be included like this.

{{ pageloader.button_script|safe }}

And that's it. Using the template tags approach the only files involved in any changes or extensions should be the ajax.js file containing the load_page script and the ajax.py file containing the get_pageloader template tag. As long as the existing parameter names and their default values in the template tag and any previous output behavior aren't changed, all templates using the script should continue to work as before.

I uploaded the code for this article to a git repository on bitbucket, which you can check here.



Comments



Post A Comment