In this tutorial, I’m going to show you how to build an AJAX style multi-day calendar with Ruby on Rails and Prototype. The code assumes that you have a number of multi-day bookings or events that you need to display on a calendar. The final result looks like this:

We’ll start with the code for the controller. I’ve removed some of the code where I create the @hours variable. In my case, the calendar is displaying bookings relating to a particular aircraft:
def index
@plane = Plane.find(params[:plane_id])
@booking = @plane.bookings.new
@hours = ["06:00","07:00","08:00"...]
@date = Time.parse("#{params[:start_date]} || Time.now.utc")
@start_date = Date.new(@date.year, @date.month, @date.day)
@bookings = @plane.bookings.where('bookings.from BETWEEN ? and ? OR bookings.to BETWEEN ? and ?', @start_date, @start_date + 1, @start_date, @start_date + 1).order('bookings.from ASC').all
if params[:start_date] && request.xhr?
render :json => @bookings
end
end
This retrieves all bookings for a particular aircraft on a given day: either today or the day passed in as a query string. The following HTML is used to display the calendar:
<section id="calendar">
<ul id="dates-menu">
<% for date in @start_date..@start_date + 6 %>
<li><a href="/planes/<%= @plane.id %>/bookings?start_date=<%= Date.new(date.year,date.month,date.day) %>" class="dates-link"><%= date.strftime("%d %B") %></a></li>
<% end %>
</ul><!-- #dates-menu -->
<br class="clear" />
<div class="day">
<div id="bookings">
<img src="/images/ajax-loader.gif" id="ajax-loader"/>
</div><!-- #bookings -->
<div id="hours">
<% @hours.each do |hour| %>
<div class="hour"><%= hour %></div>
<% end %>
</div><!-- #hours -->
</div><!-- .day -->
</section>
You’ll notice that the div with the id of bookings is populated with an AJAX-style loader.
Moving on to the javascript. I am no Javascript expert so it may be possible to refactor some of this to make it leaner. The first part of the code applies an active class to the first li element in the dates menu.
if ($('dates-menu')) {
$('dates-menu').down(1).addClassName('active');
var firstLink = $('dates-menu').down(1);
if (firstLink.hasClassName('active')) {
grabBookings(firstLink);
};
};
The grabBookings function gets the bookings for a particular day using Prototype’s Ajax.Request.
function grabBookings(date) {
$$(".booking").each(function(b) {
b.remove();
});
if (!$('ajax-loader')) {
$('bookings').insert("<img src='/images/ajax-loader.gif' id='ajax-loader' />");
}
new Ajax.Request(date.readAttribute("href"), {method:"get", evalJS:true, onSuccess:function(transport) {
var bookings = transport.responseJSON;
$('ajax-loader').remove();
bookings.each(function(b) {
var item = "<a href='/planes/" + b.plane_id +"/bookings/" + b.id + "'<div id='booking-" + b.id + "' class='booking' style='height:" + calculateBookingHeight(b.from,b.to) + "px;top:" + calculateBookingPosition(b.from,b.to) + "px;'><p><strong>From: </strong>" + formatTime(b.from) + "</p><p><strong>To: </strong>" + formatTime(b.to) + "</p></div></a>";
$('bookings').insert(item);
});
}});
}
I’ve added this as a gist. In summary, this obtains the bookings for a particular day and the controller returns the bookings in JSON format. I then iterate over each booking and insert it into the calendar. The two key functions used in the code are to calculate the position of the booking to ensure it aligns with the appropriate time and the height of the booking to ensure it covers the appropriate time period. Looking at the function to calculate the height first:
function calculateBookingHeight(from, to) {
var today = new Date();
var end = new Date(today.getUTCFullYear(),today.getUTCMonth(),today.getUTCDate(),23);
var start = new Date(today.getUTCFullYear(),today.getUTCMonth(),today.getUTCDate(),6);
var from = parseDate(from);
var fromUTC = new Date(from[0],(from[1] - 1),from[2],from[3],from[4],from[5])
var to = parseDate(to);
var toUTC = new Date(to[0],(to[1] - 1),to[2],to[3],to[4],to[5])
if (fromUTC > start && toUTC < end) {
var difference = (toUTC - fromUTC) / 120000;
} else if (fromUTC > start && toUTC > end) {
var difference = (end - fromUTC) / 120000;
} else if (fromUTC < start && toUTC < end) {
var difference = (toUTC - start) / 120000
} else {
var difference = 510
}
return difference;
}
The key elements here are the use of the figure 120000 and the multiple if statements. The height of 1 hour on the calendar is 30px. Thus, if a booking is 1 hour long, that is equivalent to 3,600,000 milliseconds. When divided by 120000, this produces a result of 30 i.e. 30px. The if statement is to deal with the situation where bookings either take up the whole day or cross with the previous or next day. The height is adjusted accordingly.
function calculateBookingPosition(from, to) {
var today = new Date();
var today = new Date(today.getFullYear(),today.getMonth(),today.getDate(),6,0,0)
var start = parseDate(from);
var fromUTC = new Date(start[0],(start[1] - 1),start[2],start[3],start[4],start[5])
var from_hours = (parseFloat(giveMeTime(from).split(":")[0]) - 6) * 30
var from_minutes = (parseFloat(giveMeTime(from).split(":")[1]) / 60)
var minutes = parseInt(from_minutes * 30)
if (fromUTC > today) {
var top = parseInt(from_hours + minutes)
} else {
var top = 0
}
return top
}
Again, based on the height of 30px per hour, this function calculates the absolute position from the top of the calendar at which the booking needs to be displayed.
You will note a number of functions within these 2 functions that are used to extract certain aspects of the date and time of the booking. Based on an input in the format of 2010-04-25T12:00Z, I use the following function to extract the time in format 12:00.
function giveMeTime(time) {
var time = time.split("T")[1].gsub(/[A-Za-z]/,"")
return time
}
Leave a comment if you spot any obvious flaws in my design or with suggestions for improving the code. I’m also considering releasing this as a plugin for Rails if there is any interest?