Now that you've got JQuery in your Camping application, one of the things you might want to do is take advantage of JQuery's AJAX capabilities. I'm going to assume you're familiar with AJAX, and JQuery's implementation of it, and just talk about how to get it going in Camping. If it's foreign to you, you might want to look at a handy AJAX and JQuery tutorial.

To save me from having to make up an actual interesting example, I'm going to write a hideously incomplete todo list application, much of it ripped off and JQuery-fied from Jan Lehnardt's CouchDB tutorial. Though not the CouchDB stuff, because that's not what I'm writing about. Though it's pretty cool, and you should check out the tutorial. Okay, back to the tuscan ham.

First we set up a Camping application and deliver JQuery. I'll use the more general method of serving static files to do this.

Camping.goes :Ajax
module Ajax::Controllers
class Index < R '/'
def get
render :index
end
end
class Resource < R '/resources/(.+)'
def get resource
types = {
'.css' => 'text/css', '.js' => 'text/javascript',
'.jpg' => 'image/jpeg', '.jpeg' => 'image/jpeg',
'.gif' => 'image/gif', '.png' => 'image/png'
}
full_path = File.expand_path(File.dirname(__FILE__)) +
"/resources/#{resource}"
if resource.include? ".." # prevent directory traversal attacks
@status = "403"
"403 - Forbidden"
elsif not File.exists?(full_path)
@status = "404"
"404 - File not found"
else
@headers['Content-Type'] = types[resource[/\.\w+$/]]||"text/plain"
@headers['X-Sendfile'] = full_path
end
end
end
end
The root of the server simply renders an index page. We've also set up the route for serving static files, which we'll use in our views to send JQuery.module Ajax::Views
Camping::Mab.set(:indent, 2)
def index
html do
head do
title 'Taking AJAX Camping'
script :src => R(Resource, 'jquery.js'), :type => 'text/javascript'
script :type => 'text/javascript' do "
$(document).ready(function() {
$('#todo').focus();
$('#add').submit(function() {
var value = $('#todo').val();
$('#todo').val(''); // reset input box
if (value) { // don't add empty todos
$.ajax({
type: 'POST',
url: '/add',
data: 'todo=' + value,
success: function(msg){
// Add the todo to the top of the list
$('#todos').prepend('<li>' + msg + '</li>');
}
});
}
// focus the input field when the document loads
$('#todo').focus();
return false;
});
});"
end
end
body do
h1 'Taking AJAX Camping'
form.add! do
legend "Another todo list that doesn't work"
input.todo! :type => 'text', :name => 'todo'
end
ul.todos! ""
end
end
end
end

A few things are going on in the view. First we make a call to Camping::Mab.set(:indent, 2) which makes the output pretty. While not required, it does make things easier if you're debugging. We then use a script element to include JQuery, which is served statically through the Resource route, and set up an AJAX call to the URL /add. Entering some text in the textbox and hitting enter triggers the AJAX call, and adds whatever was returned to the list.

Of course, we don't have a route that will respond to a request to /add, so we'd better add that now. Whack this in your controller. class Add < R '/add'
def get
"#{@input.todo}: #{Time.now.to_s}"
end
end

The current time will now be returned in response to the AJAX call. It's not a very interesting use of AJAX. We should be saving the todo to a database, but that's not the point of this piece and would just clutter up the code. So there.

But hang on, we're not rendering a view? That's right, you don't need to render a view at all, simply return text straight from the controller and everything is hunky dory. This is AJAX option one. Of course, this isn't just an AJAX peculiarity, any controller can just print out a slab of text or HTML. This pretty much defeats the purpose of an MVC framework, as we should be separating the display logic.

So, even though it's a trivial example, let's put our presentation in a view. Change the Add route in your controller. class Add < R '/add'
def get
@todo = @input.todo
render :add
end
end
Now add the add view. def add
text "#{@todo}: #{Time.now.to_s}"
end
Great, now that's separated out and we're using a view. The issue with this is that when your application gets a bit bigger, adding a couple of other views, you're going to want to use a layout. If you do, the AJAX call will now return the time, all wrapped up in the layout HTML. Probably not what you want. The way around this is to render the AJAX as a partial. So, a subtle change to the controller, calling a partial. class Add < R '/add'
def get
@todo = @input.todo
render :_add
end
end
And we have to rewrite the views to use a layout and the partial.module Ajax::Views
Camping::Mab.set(:indent, 2)
def layout
html do
head do
title 'Taking AJAX Camping'
script :src => R(Resource, 'jquery.js'), :type => 'text/javascript'
end
body { self << yield }
end
end
def index
script :type => 'text/javascript' do "
$(document).ready(function() {
$('#todo').focus();
$('#add').submit(function() {
var value = $('#todo').val();
$('#todo').val(''); // reset input box
if (value) { // don't add empty todos
$.ajax({
type: 'GET',
url: '/add',
data: 'todo=' + value,
success: function(msg){
// Add the todo to the top of the list
$('#todos').prepend('<li>' + msg + '</li>');
}
});
}
// focus the input field when the document loads
$('#todo').focus();
return false;
});
});"
end
h1 'Taking AJAX Camping'
form.add! do
legend "Another todo list that doesn't work"
input.todo! :type => 'text', :name => 'todo'
end
ul.todos! ""
end
def _add
text "#{@todo}: #{Time.now.to_s}"
end
end

Of course, you don't have to just return text. You could also return JSON or XML (setting the correct Content-type header) or any other kind of structured data you want.

Camping is great, yeah. JQuery too. Maybe you want to use them together? Here's how. First, you need to set up a route for your JQuery. I've talked about sending static files with Camping before, so this is just a modification of that. [You can serve static files more sensibly than setting up a route for each type. Maybe one day I'll write about that.] In your controller: module MyApp::Controllers class Index < R '/' def get render :index end end class JQuery < R '/resources/jquery.js' def get current_dir = File.expand_path(File.dirname(__FILE__)) @headers['Content-Type'] = "text/javascript" @headers['X-Sendfile'] = "#{current_dir}/resources/jquery.js" end end end ...
[read more]