Patrick Altman

I am the VP of Engineering at Eldarion, Co-Founder of Amino Software, and a Mentor at jumpstart foundry.

I am equally passionate about writing code as I am about building businesses.

You really should

Posted On

Aug. 23, 2012, 10:10 a.m.

Tags

Do you have an iOS or web application that you'd like help with?

Eldarion offers lean coaching, software development, code reviews, and in some cases staff augmentation.

Give me a call at 615-300-2930 or send us an email and let's see how we can help you.

This site is hosted on

You should consider it for your Django/Python web hosting as well!

Twitter Bootstrap and AJAX

As a long time web developer who has struggled with being comfortable with doing front-end development, having Twitter Bootstrap available is nothing short of transformative. The aspect that I am most enamored with is how much the bar has been lowered for guys like me to make relatively large UI changes without the need to segment work out to UI experts.

However, I was writing the same bits of Javascript everywhere to do simply little things with $.ajax in jQuery. Code was fragile and often I would defer making the experience of doing some less polished/snappy because it would mean having to write some more of the same old Javascript that I was bored writing. "Ah, a POST followed by the 302 redirect isn't too bad, let's just roll with that for now...", I would say to myself.

So, I created bootstrap-ajax.js.

For the record, I don't think that all traditional form submissions followed by the standard 302 redirect are bad. In many cases it's exactly all that is needed and works well. You have to make the call for yourself and in the context of your own web apps what works best for each situation.

What if we could break down the problem and recognize the abstractions that we were solving repetitively, package it up, and make it dead simple to leverage on demand the way Twitter has made doing things like modals, so easy.

First, the abstractions:

  1. Simple posts like the ones used to toggle a vote, or submit a rating. The kinds of things we find ourselves either writing little tiny forms and a morass of Javascript because when that post happens, there are three other blocks of content on the page that should be updated.
  2. Handling a form submission via AJAX. We have entire plugins dedicated to doing this, but even then who wants to write the handler code for it. I just want to add a class and/or an attribute or two in my <form> element and be done with it.
  3. Content blocks should know how to update themselves instead of having bodies of fragile Javascript manipulate the DOM to add/update data.

I recognize some of this can be solved with backbone.js, but I didn't want to go that route. I wanted templates to continue to be rendered server side. I wanted to be able to (for the most part) already take advantage of a large body of reusable Django apps. When and if apps that I wanted to use didn't have an AJAX interface in it's views, or maybe one that didn't support the convention I was cooking up, adding to it was easily done in a backwards compatible way. Let me be clear, this is not a knock against backbone.js apps, there are some really great reasons to use it, but it's not the appropriate tool for every job.

To give you an idea, let's go with an example.

Imagine you have a web app that has a page where you can share status messages (original, I know). Also on this page, you have a side bar with stats about your account like the number of messages you have left.

You could just do a traditional form POST to log the status message and return a 302 Redirect to the same page, rendering the entire template again, this time with you new message in the feed and the message count updated in the side bar. This actually isn't a bad place to start, but you can do better.

views.py

from django.shortcuts import redirect, render

from django.contrib.auth.decorators import login_required

from .forms import MessageForm


@login_required
def message_list(request):
    if request.method == "POST":
        form = MessageForm(request.POST)
        if form.is_valid():
            message = form.save(commit=False)
            message.user = request.user
            message.save()
            return redirect("message_list")
    else:
        form = MessageForm()
    return render(request, "messages/message_list.html", {
        "messages": request.user.messages.all(),
        "form": form
    })

message_list.html

{% extends "messages/base.html" %}

{% load bootstrap_tags %}

{% block body %}
    <h1>Your Messages</h1>
    <p class="lead">Here is a stream of your messages.</p>
    <form action="{% url message_list %}" method="post" class="form">
        {% csrf_token %}
        {{ form|as_bootstrap }}
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    </form>
    {% for message in messages %}
        {% include "messages/_message.html" with message=message %}
    {% endfor %}
{% endblock %}

Now that you have the simple form POSTing working let's add in bootstrap-ajax.. The first thing we need to do is modify our views.py to add a POST only view and pull out the POST handling from our main list view. 

views.py (rev 2)

from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils import simplejson as json
from django.views.decorators.http import require_POST

from django.contrib.auth.decorators import login_required

from .forms import MessageForm


@require_POST
@login_required
def message(request):
    form = MessageForm(request.POST)
    if form.is_valid():
        message = form.save(commit=False)
        message.user = request.user
        message.save()
        form = MessageForm()
    data = {
        "html": render_to_string("messages/_message_form.html", {
            "form": form
        }, context_instance=RequestContext(request))
    }
    return HttpResponse(json.dumps(data), mimetype="application/json")


@login_required
def message_list(request):
    return render(request, "messages/message_list.html", {
        "messages": request.user.messages.all(),
        "form": MessageForm()
    })

_message_form.html

{% load bootstrap_tags %}
<form action="{% url message_list %}" method="post" id="message-form" class="form ajax" data-replace="#message-form">
    {% csrf_token %}
    {{ form|as_bootstrap }}
    <div class="form-actions">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>

messages.html (rev 2)

{% extends "messages/base.html" %}

{% block body %}
    <h1>Your Messages</h1>
    <p class="lead">Here is a stream of your messages.</p>
    {% include "messages/_message_form.html" with form=form %}>
    {% for message in messages %}
        {% include "messages/_message.html" with message=message %}
    {% endfor %}
{% endblock %}

{% block extra_body %}
    <script src="{% static "js/bootstrap-ajax.js" %}"></script>
{% endblock %}

Now we have the original page that renders just the same as before except now the message will post via AJAX so it won't render the entire page, we are passing back a rendering of the same include with a fresh form (if it is success) or a form with validation errors (if it has an error) and that will just replace itself.

But what about the message list? Should we update that? I think so. Seems silly not to.

views.py (rev 3)

# only adding this request.is_ajax() block to the existing views.py
@login_required
def message_list(request):
    if request.is_ajax():
        data = {
            "html": render_to_string("messages/_message_list.html", {
                "messages": request.user.messages.all()
            }, context_instance=RequestContext(request))
        }
        return HttpResponse(json.dumps(data), mimetype="application/json")
    return render(request, "messages/message_list.html", {
        "messages": request.user.messages.all(),
        "form": MessageForm()
    })

_message_list.html

<div class="messages" data-refresh-url="{% url messages %}">
    {% for message in messages %}
        {% include "messages/_message.html" with message=message %}
    {% endfor %}
</div>

_message_form.html (rev 2)

 
{% load bootstrap_tags %}
<form action="{% url message_list %}" method="post" id="message-form" class="form ajax" 
         data-replace="#message-form"
         data-refresh=".messages,.message-count">
    {% csrf_token %}
    {{ form|as_bootstrap }}
    <div class="form-actions">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>

messages.html (rev 3)

{% extends "messages/base.html" %}

{% block body %}
    <h1>Your Messages</h1>
    <p class="lead">Here is a stream of your messages.</p>
    <{% include "messages/_message_form.html with form=form %}>
    {% include "messages/_message_list.html" with messages=messages %}
{% endblock %}

{% block extra_body %}
    <script src="{% static "js/bootstrap-ajax.js" %}"></script>
{% endblock %}

I left out the view and template fragment for .message-count refreshing for the sake of brevity. Post a comment if you'd like me to explain how that works or if it's in anyway unclear how you'd hook that up as well.

We could optimize this further by structuring things in such a way where we only needed refresh new messages, or add the one that we just posted, but I just wanted to give you a sense for how this thing works. Maybe another post later.

Feedback and Commentary

blog comments powered by Disqus