Quick, simple and arguably proper Ajax (without touching server-side)
My beliefs
First of all, I’d like to clarify things a little bit. I have some axioms, beliefs and biases about how Ajax should work. They aren’t the theme of the post, it’s just a declaration: in this particular blog post, when I say “right” I mean “according to these idiosyncratic points”, however really right they are:
- If I disable Javascript on the page, the page should still behave well: forms can be submitted, comments are being posted, et cetera. (It is probably called graceful degradation.)
- GET is not ever used for not-idempotent requests. If I want to create something1, I send POST2 3. If I want to load something without changing anything on the server, I use GET. (This is probably a RESTful way, but I don’t swear it is.)
- To make a POST request, you should have a form and just send it. If you don’t have a form, you’re probably breaking the first rule. If you have a form, but for some reason still collecting parameters by hand in Javascript, you’re acting strange.
- To make a GET request, you can have a link (
<a href=...>). If you’re using links for something like POST requests, I loathe you. But you can also send a form with GET, because it can be useful, for example, for filtering things.
Don’t touch server-side
I don’t like changing my beautiful server-side code for some ajaxy stuff. Yeah, sometimes it’s necessary, so I just get over it and do that’s necessary. But it’s not always necessary.
For example, let’s assume you have some fragment on a page, and you’d like to allow user to refresh it using ajax (for whatever reason). You can add a check for ajax to your server-side code, add a couple of ifs and duplicate code a lot.4 But why? Your app can just ask server for the full next page, extract actual content from it and insert it into the current page. (There can be performance concerns if loading the full page eats too much resources on your server and your website is really popular. Poor you.)
This is simple
And however frightening it may sound, sending GET requests for a portion of a page is really simple. For example, if you’re using jQuery, you can do:
<a href="/my_page/" class="ajaxed_refresh">Refresh!</a>
<div id="page">
<div class="content">
<ul>
<li>Thing 1</li>
<li>Another thing</li>
<li>And the third one</li>
</ul>
</div>
</div>
<script>
function set_refresh_callback() {
$('.ajaxed_refresh').click(function(e) {
e.preventDefault();
var page_url = $(this).attr('href');
$('#page').load(page_url + ' div.content');
return false;
});
}
$(document).ready(set_refresh_callback);
</script>
What’s happening here?
- We show a page and a link. The link is a normal HTML link. If user clicks on your link, the standard “click-the-link” behavior is expected: the page will be loaded again. Without JS, everything is as expected.
- In our javascript, we override this behavior. We prevent the default one, so user won’t leave the page.
- We get the url which should be loaded. It’s in the “href” tag of the link, so we’re not repeating ourselves.
- We call
jQuery().load()method with parameterpage_url + ' div.content'. For example, if url is/my_page/, the parameter will be"/my_page/ div.content". jQuery will understand it as “load/my_page/, extract elementdiv.contentfrom it and place it into the#pageelement”.
Yes, that simple. jQuery().load() seems awesome at first. As an exercise, add a link to this page which loads another page in ajaxy way. This can be done without any new javascript code.
.load() with POST
As I said, POST is the way to create things. If you want to show form, send it with AJAX and replace some part of your page with the result, it’s still simple.
My usual pattern on the server-side is to receive a POST request and return the page with a form again with errors, if there were any, or, if not, return a 3035 redirect. It’s called “Post/Redirect/Get” and it is a really good way to create things on web.6
jQuery().load() receive a second parameter - form data. If it’s present, the request is sent as POST request. So the only thing we should do is to convert all the form data into the JS dict.
You won’t believe it, but there is no method in jQuery to convert form data into the dict. You can convert it to the QueryString ("a=1&b=2"), or to some really weird data structure ([{ name: 'a', value: 1 }, { name: 'b', value: 2 }]), but not to the dict ({ a: 1, b: 2 }. On the other hand, to send .load() as POST, one have to pass data parameter in the last format. Crazy, huh?
I have a simple function for you to serialize forms in a useful way:
function serialize_form(form) {
var params = {};
$.each(form.serializeArray(),
function(k, v) {
params[v.name] = v.value;
});
return params;
}
Now the task is simple:
<div id="page">
<form action="/create_thingy/" method="post" class="ajaxed_form">
<input name="title">
<input type="submit">
</form>
</div>
<script>
function set_form_callback() {
$('.ajaxed_form').submit(function(e) {
e.preventDefault();
var action = $(this).attr('action');
$('#page').load(action + ' div.content', serialize_form( $(this) ));
return false;
});
}
$(document).ready(set_form_callback);
</script>
The actual <form> is untouched, and it works with and without javascript. Wonderful!
But .load() is too stupid
Let’s try to write an awesome paginator with our new-learned trick.
<div id="page">
<div class="content">
<ul>
<li>Blog post one</li>
<li>Another blog post</li>
<li>Last blog post on the page</li>
</ul>
</div>
<div class="paginator">
<a href="/page/2/">Next page</a>
</div>
</div>
We again want to just .load() a new page into something. But into what exactly should we load it?
- If we load
.contentinto#page, it will replace content, not append it! - If we load
.contentinto.content, it will replace content and create a strange hierarchy:<div id="age"><div class="content"><div class="content">... - If we load something into
.paginator, nothing will be gone, but hierarchy will still be strange.
For some reason which I can’t understand at all, jQuery function can’t do an appending load(). So we’ll have to do it by hand.
What we’re going to do is to get the page contents into the varialbe, extract everything we need and append it into the right places:
<script>
function set_paginator_callback() {
$('a.paginator').click(function() {
var href = $(this).attr('href');
var content = $.get(href, function (data) {
var resp = $(data)
resp.find('.content ul li').appendTo( $('.content ul') );
resp.find('.paginator').replaceAll( $('.paginator') );
set_paginator_callback();
});
});
};
$(document).ready(set_paginator_callback);
</script>
We’re loading the page, extracting needed element, placing them into the current page and resetting callbacks (don’t forget to do it!). Yay!
Cool story bro
The approach discussed there works with popular frameworks (for example, you don’t have to change Django’s generated forms at all), can update lots of elements, simple, generic, idiomatic, error-prone with malformed pages and inline javascripts and a little bit rude to your server.
There is no problem to catch errors, set callbacks, et cetera, but please figure it out on your own.
You don’t have to use it and it’s definitely not the only one way. But it gets things done, and I like it.
-
Actually, as Luke Plant have pointed out, idempotency is not the same thing as ‘safety’ (meaning freedom from side effects, such as creation of resources). Still, distinction between GET and POST is that the former should be idempotent and side-effect free, while the latter has no such restrictions. It’s not the case with PUT and DELETE: these two are expected to be idempotent (if you send such request twice, nothing new happens), but, of course, they are changing the server state.
↩ -
In my humble opinion, PUT is used for creating things urls for which are known beforehand. For my webapps, it’s rarely the case.
↩ -
There is a problem with PUTs and DELETEs: if Javascript is not working, you can only send GET and POST. But there are actually workarounds, and PUTs and DELETEs are RESTful, so I’m not actually against them. I just rarely use them.
↩ -
I believe it can be avoided with beautiful object-oriented programming or something else. But it’s not really simple, so if you take this path, you can make thing even worse. So it’s not the first thing I’d do - probably the second.
↩ -
As Anand Kumria have pointed out, in such cases one shall use 303 or 307 redirect codes instead of 301 or 302.
↩ -
Actually, since AJAX in browsers follows redirects, it does not really matter whether you use this pattern or not.
↩
Please send your comments, ideas, rants and job offers at v.golev@gmail.com.
Made with Nginx, Jekyll, Git, EC2 and Emacs.