First thing is to get the new code you'll need for this project. First off, grab the JSON gem. Type:
sudo gem install json_pure
This will install a pure ruby implementation of the gem. You can go here to find out more about the gem and how to use it.
Next, you'll have to download the FullCalendar. This will give you the JavaScript and CSS you'll need for the calendar.
OK, so let's create the directory structure for this project. You'll need a top level directory, called say FullCalendarTest. Under that you'll need controllers, layout, public, and view directories. Under the public, you'll need a js directory and that's pretty much it. We're not going to use a database here, so you won't need our normal dbMigration or models directories.
Let's get the calendar code and CSS into the correct spots for this demo. From the directory you unzipped your FullCalendar, copy ui_core.js, ui_draggable.js, and fullcalendar.js to the public/js directory. You should also copy the jquery-1.3.2.js there also. Finally, copy from the same FullCalendar unzipped directory the fullcalendar.css file to the public directory. With that, we should be all set up to start writing our own code.
First up is our start.rb file.
# start.rb
#
# This is the main program for the example. It loads the loads the controller
# and then starts up Ramaze.
#
require 'rubygems'
require 'ramaze'
require 'json'
require 'controllers/main_controller'
Ramaze.start :port => 7001
Nothing too much here different that what we've done before. We aren't using a database here so there's none of the normal sequel code that we've seen before (add it back in if you decide to flesh this example out). The only other thing is the
Ramaze.start :port => 7001
. Normally, we wouldn't put a port number on this, but I was running my original application while developing the demo and put this in so I could run both. Normally, the default port for Ramaze is 7000 and we don't specify it. Here, where we do want a different port, we'll put it in. In a "real" web application, you'd probably put in the normal http port of 80.Let's take a look at our controller, controllers/main_controller.rb next.
# controllers/main_controller.rb
#
# This example shows how to do use the FullCalendar (http://arshaw.com/fullcalendar/) and AJAX.
class MainController < Ramaze::Controller
# The controller will be accessed using "/" as in:
# http://localhost:7001/.
map '/'
# Layout using page but not if it comes from an AJAX request.
layout(:page){ !request.xhr? }
helper(:xhtml)
# You can access it now with http://localhost:7001/
def index
# Set the title for the page.
@title = "Ramze Calendar Test"
# Set the javascript for this page. In this case it's the script to
# set up and display the calendar in public/js/show_calendar.js.
@page_javascript = 'show_calendar'
if request.xhr? # came from ajax request
# Here's how to get the dates that will be sent by FullCalendar. We're not actually going
# to use them here, but this will show what to do when you actually need them.
startDate = Time.at(request['start'].to_i).strftime("%Y-%m-%d")
endDate = Time.at(request['end'].to_i).strftime("%Y-%m-%d")
Ramaze::Log.debug("show_calendar: Have an Ajax request start: #{startDate} end: #{endDate}")
# Use the JSON gem to generate JSON for the events. We're just going to add a
# couple of events here. One will be on the 15th of September 2009 and the other
# on the 17th. Normally, these would come out of the database and the "id" would be
# their id in a database table, the "start" would a date from a table in the database. Finally,
# the url would be a link to a page where you could chang the event and then save it. Of course,
# you don't have to do it that way, but it would be one way to generate the events.
json = JSON.generate [
{"id"=>1, "title" => "Ramaze", "start" => "2009-09-15", "url" => "http://ramaze.net/"},
{"id"=>2, "title" => "Sequel", "start" => "2009-09-17", "url" => "http://sequel.rubyforge.org/"}
]
# It looks like we a) MUST use the respond command and b)MUST use the 200 return value. This was
# determined by just trying different things.
respond(json, 200)
end
end
end
This is actually much smaller that it looks due to the excessive amounts of commenting in it. We have our normal "startup" code with the map, layout, and helper lines. The
layout(:page){ !request.xhr? }
just makes sure that we don't use the layout when we're handling an AJAX request. We've seen this before in our previous AJAX tutorial. Next up, we have the index
method (our only one). This will set the title and then the JavaScript for the page, in this case it will end up being public/js/show_calendar.js (the actual script tag will get created in the layout). Next we put the code for handling an AJAX request. The calendar is going to pass us start and end dates in the request hash table with the keys of "start" and "end" appropriately enough. Here, we're not going to actually use them, but I've shown how to parse them out for when you do actually want to go to a database to get some actual events. After this, we create a json structure from an array of hashes. Here we create a couple of events for the 15th and 17th of September, 2009. These are hard coded, so feel free to change them if you'd like. Finally, we send the json back to the FullCalendar code. In our previous AJAX example, we just passed the json back directly. For whatever reason, that doesn't work with the FullCalendar code and we use the respond(json, 200)
to send it back. This could be because the FullCalendar code uses the getJSON()
call rather than the post()
. I haven't investigated this, but if you do, let me know what you find in the comments section.Let's take a quick look at the layout in layout/page.xhtml
<!-- view/page.xhtml -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!-- Use the page.css in the public directory and set title based on
what's set in the associated method.
-->
<link rel="stylesheet" type="text/css" href="/page.css"/>
<link rel='stylesheet' type='text/css' href='/fullcalendar.css' />
<!-- Our jQuery is in the public/js directory -->
<script type="text/javascript" src="/js/jquery-1.3.2.js" ></script>
<script type='text/javascript' src='/js/jquery/ui.core.js'></script>
<script type='text/javascript' src='/js/jquery/ui.draggable.js'></script>
<script type='text/javascript' src='/js/fullcalendar.js'></script>
#{ js @page_javascript }
<title>#@title</title>
</head>
<body>
<div id="header">
<h1>Ramaze Calendar Test</h1>
</div>
<!-- Display the actual content. This will come from the method or the
associated view/*.xhtml file
-->
#@content
<!-- Set the footer in the center of the screen. -->
<div id="footer" style="text-align: center;">
<h5> Powered by Ramaze </h5>
</div>
</body>
</html>
We include the two style sheets for our normal page.css and also for the calendar, fullcalendar.css. Next we have our JavaScript code for jQuery and and the FullCalendar followed by the page_javascript. In this case the only page JavaScript we'll have is the public/js/show_calendar.js. Everything else, we've seen before.
Here's the show_calendar.js (formatter doesn't work on javascript code, sorry)
/* public/js/show_calendar.js */
$(document).ready(function() {
$('#calendar').fullCalendar({
/* The draggable looks like it might be pretty cool, but when I try to enable it
* only the first event will be shown. Go ahead and give it a try and if you find
* a solution, let me know.
*/
/* draggable: true, */
/* The events will use the "index" method in the main controller to get the events from. */
events: "/index",
/* We're not using drag/drop here (see above), but it would be nice to have it. */
eventDrop: function(event, delta) {
alert(event.title + ' was moved ' + delta + ' days\n' +
'(should probably update your database)');
},
/* What to do while we're loading. */
loading: function(bool) {
if (bool) $('#loading').show();
else $('#loading').hide();
}
});
});
This starts out like all jQuery functions with the
ready()
function. Next, we have the commented out draggable: true
which when I left it in, would only show the first event. This was true whether I put events in-line or got them from AJAX. Then we have the events: "/index",
line which tells FullCalendar to get the events from an AJAX call to our index method in the main_controller. Finally, we have a couple of items for dragging (not used) and loading.Here's the view/index.xhtml:
<div id='calendar'></div>
OK, the only thing in here is the calendar itself.
Our page.css is also very simple and is just like what we've used numerous times before.
/* Header CSS */
#header {
background:#9DA9EE;
color: white;
margin-bottom: 0;
padding: 1.5em;
}
/* Footer CSS */
#footer {
background:#9DA9EE;
color: black;
}
/* Calendar */
#calendar {
width: 900px;
margin: 0 auto;
}
The only addition is for the calendar.
Everything else used is from FullCalendar itself and you can check the documentation for it here.
I think that's about everything. FullCalendar works well for what I'm going to be using it for, although it would be nice to have the drag and drop interface working. If I get the issues with that worked out, I'll either edit the post or create another show post on how I solved it. If you do end up trying to use some of this with a database and sequel and have problems, let me know and I'll be glad to post some code on how I've managed it.
One final thing is the help I received from the Ramaze mailing list on this. First, thanks to hrnt for the hint on
respond
and to Greg for telling me how to view AJAX repsonses in Firebug. The thread is here.Let me know if I've missed anything or if you have questions.
I implemented FullCalendar in Ramaze yesterday, after your post, and thought it would be good to share JSON structure generation from a Sequel model...
ReplyDelete....
@events = Events.select(:id, :name, :date).order(:date.asc).all
json = JSON.generate @events.map{|x,y| {"id" => x[:id], "title" => x[:name], "start" => x[:date].strftime('%Y-%m-%d')} }
respond(json,200)
....
(Pastie: http://paste.ramaze.net/32128)
It's working rather nicely for one line of Ruby.
Kez,
ReplyDeleteThat's pretty much where I ended up with my "real" applicaton (except I'm not clever enough to remember to use map). The one thing I did do was have the model have a method for getting just the dates being asked for and it looks something like:
def self.all_by_dates(startDate, endDate)
filter(:date => startDate..endDate).all
end
This will give only the dates for the month in question. Then you can use your function on fewer items and pass fewer items to the view.
Thanks for sharing the code!