Sunday, May 31, 2009

Ramaze, jQuery, and Ajax

This post tries to show how to use Ajax and jQuery with Ramaze. Since, as usual, I didn't really know anything about JavaScript, jQuery, or Ajax, this was quite a learning experience. Right up front, I need to thank the Ramaze email list is general and Gavin in particular for patiently answering my questions. The exchange that started this is documented here.

Also, this jQuery tutorial, provided me a convenient jumping off point and much of the code for the jQuery is taken directly from here or is based on this code and slightly modified.

What we're going to do in this example is some very simple Ajax which put up five numbers, let's you select one, and then prints the number you selected below along with the time you selected it at. You probably wouldn't normally do this with Ajax, but it does show how to use it.

Here's our start.rb file:


# start.rb

require 'rubygems'
require 'ramaze'

# You can access it now with http://localhost:7000/
class MainController < Ramaze::Controller

# Layout using page but not if it comes from an Ajax request.
layout(:page){ !request.xhr? }

def index
@title = "Ramaze / Ajax Example"
end

# This should only get called from an ajax request.
def choose
if request.xhr? # came from ajax request
# Create a JSON "object" of the choice that can in through the post
# request and the current time. For the time, we need to enclose it
# in double quotes so that it will be interpreted as a string.
"{ choice: #{request['choice']}, time: \"#{Time.now}\" }"
else
request['choice'].to_s
end
end
end

Ramaze.start



It's pretty straightforward but does have a couple of new items in it. First there's this:

layout(:page){ !request.xhr? }

This tells Ramaze to use page for layout accept in the case of an Ajax call. When you make an Ajax call, you want just the data returned (in our case some JSON) and not the entire formatted page. This took the help of the Ramaze email group (thanks Gavin!) to figure out. If you're searching you may find deny_layout. This has been removed in the latest Ramaze, so avoid it.

Our index method is very straightforward so we'll move on to the choice method. Here, we simply take in the data from the post in this case something that will look like {choice: 2} and create another JSON object that also contains the time from a Time.now call. Here, we need to enclose the time in double quotes, so that it will be interpreted as a string. Without the quotes, there is no way to interpret it correctly.

Our index.xhtml file is very simple


<!-- view/index.xhtml -->

<h2>#@title</h2>

<p>This example demonstrates the use of Ramaze and Ajax along with some JQuery and JSON.</p>

<!-- An empty container where we'll put the choices and the return values
values from the ajax/post call.
-->

<div id="ajaxContainer"></div>


The only interesting piece is the empty div element. We're going to put our ajax code into it via page.xhtml.

Finally, here's the 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>
<title>#@title</title>
<link rel="stylesheet" type="text/css" href="/page.css"/>

<!-- Our jQuery is in the public/js directory -->
<script src="js/jquery-1.3.2.js" type="text/javascript"></script>

<script type="text/javascript">

// For jQuery put everything inside of the document ready code.
$(document).ready(function() {

// Create an array of text and links. We'll add this to the
// ajaxContainer using join()
var choiceMarkup = ["Please choose a number: "];
for(var i=1; i <= 5; i++) {
choiceMarkup[choiceMarkup.length] = "<a href='#'>" + i + "</a> ";
}
choiceMarkup[choiceMarkup.length] = (" Your choice will be printed below.");

// Grab the ajaxContainer so we can add the choiceMarkup to it.
var container = $("#ajaxContainer");

// Add markup to container
container.html(choiceMarkup.join(''));

// Add click handlers. Here we're going to get all of the anchor elements <a>
// and add the function to them.
container.find("a").click(function(e) {

// Don
't do the normal thing you'd do when clicking a link.
e.preventDefault();

// Send a post request to the choose method when a link is
// clicked. Pass in the JSON "choice: item" to the function.
// The callback routine gets a resultObject(JSON) and a status
// (not used and not actually returned). The choose() method
// will return JSON, the fourth parameter, to post.
$.post(
"/choose",
{choice: $(this).html()},
function(resultObject, resultStatus) {

// Take the resultObject and grab the choice and time from it and format it.
var result = [
"<br>Thanks for choosing. You chose: ", resultObject.choice, " at ", resultObject.time
];
// Add the result to the ajaxContainer at the bottom. This will create a new
// item each time one of the numbers is clicked.
$("#ajaxContainer").append(result.join(
''));
},
'json' );
});
});
</script>
</head>
<body>
<div id="header">
<h1>SteamCode</h1>
</div>
#@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>



The first new thing we see here is the inclusion of the jQuery library. I've put it into the public/js directory. If you use a different version other than 1.3.2 you will need to change this line. Next is our actual script. All jQuery code is put inside the document ready block. After that, we create some text, links for the five choices, and a bit more text and shove these into an array, choiceMarkup. Then we grab the ajaxContainer defined in our view/index.xhtml file and put the choiceMarkup into it.

Next up, we find all of the anchor, a, links and add a function to them when they're clicked. The first thing we do in this function is disable the normal response to a click and then add our own code. Here we do a post function, calling the choose method in our controller and pass JSON that looks like {choice: 3} (we talked about this above). When the function returns, we have our data in the result object and as also discussed, this contains the choice and the time. We format this and then add it to our ajaxContainer. This will add a new line each time something is clicked and continue until we do a refresh. The final parameter to the post is the 'json' which tells jQuery that the data returned will be (obviously) JSON.

Everything else we've seen before and I won't go over it again.

There's quite a few new things to see in here and there aren't too many examples on the web showing the use of Ajax, jQuery, and Ramaze. Here's a post, but I'm not sure that it will work with the new Ramaze. It may take some modifications to get it working. Here's a nice jQuery link that helped a bit. Finally, for JSON, try here. JSON is not complex, but you do have to follow the rules (I didn't originally and it took a while and some help to figure out).

Let me know of any questions or comments.

Friday, May 22, 2009

Templates, Sequel, and Ramaze (Redux)

After I posted this morning, Jeremy Evans of Sequel fame pointed out a few issues. First he suggested that instead of using datasets in the controller as I had done, that I use the Model instead and showed me how to do it. Second, he suggested that instead of using the User.find(id) in the personal(id) method, I use instead User[id] as more idiomatic. Finally, in the view/index.xhtml view, he noted that Rs has been deprecated in favor of r. So with that, here's the three new files (I neglected to post the models/user.rb earlier anyway).

Here's the new main_controller.rb


# controllers/main_controller.rb
#
class MainController < Ramaze::Controller
# Use page.xhtml in the view directory for layout
layout :page

# You can access it now with http://localhost:7000/ This should display a
# welcome message. This is the home page.
def index
# Get all of the user's ids and names from the User model. We're going
# to display all of them on the index/home page.
@users = User.id_and_names
@title = "Welcome to SteamCode"
end

def personal(id)
# Find the user with the given id. This will come from the link created on the
# index/home page.
@user = User[id]

# Create the title with first name / last name 's Home Page.
@title = "#{@user[:first_name].capitalize} #{@user[:last_name].capitalize}'s Home Page"
end
end


Here's the new view/index.xhtml


<!-- view/index.xhtml -->
<h2> Users <h2>
<ul>
<!-- Loop through each of the users (from MainController/index and
Create a link from MainController/personal and the "id" of the user.
This will then call MainController/personal/id and the id will be used
as a parameter to personal. This in turn will allow us to find the user
and go to their "home" page. The "angle bracket question mark r" are
used to put ruby code inside the template and any valid ruby code can
go here.
-->

<?r @users.each do | user | ?>
<li> <a href="#{r(:personal)}/#{user[:id]}">#{user[:first_name].capitalize} #{user[:last_name].capitalize}</a> </li>
<?r end ?>
</ul>


And finally here's the models/user.rb (not posted earlier)


# models/user.rb
#
# This is the model for the User and is backed by the :users table in the
# database. We add the id_and_names for the index method in the
# main_controller.
require 'rubygems'
require 'sequel'

# Create the Account model.
class User < Sequel::Model

# Create a class method to grab the id, and first and last names from
# the default User dataset.
def self.id_and_names
select(:id, :first_name, :last_name).all
end
end


If you have questions or comments on this or the previous post (or any post for that matter), please don't hesitate to ask.

Templates, Sequel, and Ramaze

Well, after the last really large post, I thought that I'd work on something a bit more manageable for this one. There are three new things in this post, some with Sequel, some with Ramaze, and we'll take a look at a slightly more interesting use of templates (really only slightly though). Let's start with our migration:


# This is the "new" way to do migrations by using the Class.new form. 
# It means that a new class name is not required and so there is no chance
# of creating a second class in the migration files that is the same as the
# first causing problems.
Class.new(Sequel::Migration) do
def up
create_table(:users) do
primary_key :id
String :first_name
String :last_name
String :user_name
Integer :age
end

# Here we're going to go ahead and add a couple of users in the migration. This saves us from having
# to add them via forms are similar.
from(:users).insert(:first_name => 'john', :last_name => 'smith', :user_name => 'jsmith', :age => '22')
from(:users).insert(:first_name => 'jane', :last_name => 'johnson', :user_name => 'jjohnson', :age => '33')
end

def down
drop_table(:users)
end
end



The first thing to notice is that we don't name the class here, we use Class.new(Sequel::Migration). This was recommended by Jeremy on the Sequel list and it means that there won't be any issues if you accidentally reuse a class name in multiple migrations. There's nothing new in the creation of the table, but right after that we go ahead and add some data. This probably isn't a great idea for a normal application, but for our simple demonstration, it simplifies things. Here what it means is that we can add a couple of users without all of the machinery from the last post on registering users. Finally, we have the down() method which drops the table. To generate the table run:
sequel -m dbMigration/ -M 1 sqlite://users.db which will create the users.db sqlite database with the defined users table and the two rows we defined.

Next we have the start.rb file:


# start.rb
#
# This is the main program for the example. It loads the Sequel database,
# loads the controller and model, and then starts up Ramaze.
#
#
# You can add new accounts on the registration page.
require 'rubygems'
require 'ramaze'
require 'sequel'

# The database should have been set up using the database migrations (there are
# currently two of them) in the dbMigration directory. Run them with:
# sequel -m dbMigration -M 2 sqlite://accounts.db
# Open the accounts database. This must be done before we access the models
# that use it.
DB = Sequel.sqlite("users.db")


# Load the controllers and models (one of each in this case).
require 'controllers/main_controller'
require 'models/users'

# Start Ramaze.
Ramaze.start



Nothing too much interesting in here, just the normal includes, grabbing the database, models, and controllers, and then starting up ramaze.

Our first "interesting" code is in controllers/main_controller.rb.


# controllers/main_controller.rb
#
class MainController < Ramaze::Controller
# Use page.xhtml in the view directory for layout
layout :page

# You can access it now with http://localhost:7000/ This should display a
# welcome message. This is the home page.
def index
# Get all of the users from the database. We're going to display
# all of them on teh index/home page.
@users = DB[:users].select(:first_name, :last_name, :id)
@title = "Welcome to SteamCode"
end

def personal(id)
# Find the user with the given id. This will come from the link created on the
# index/home page.
@user = User.find(:id => id)

# Create the title with first name / last name 's Home Page.
@title = "#{@user[:first_name].capitalize} #{@user[:last_name].capitalize}'s Home Page"
end
end



We have our normal layout using view/page.xhtml. Then there is the index() method. Here we're going to grab all of the users from the database DB[:users] giving us a dataset and then we're going to get only three of the four columns using the select(:first_name, :last_name, :id). In a normal application, we'd have many more than four columns, so grabbing just the ones you need make a bit more sense than it might appear here. Given what we've put into the database in our migration, we know we should have the ids, first name, and last name of the two users and we're going to store them in @users which will then be available in our view. Next we just go ahead and set the @title for the page.

Here's the view that uses this:


<!-- view/index.xhtml -->
<h2> Users <h2>
<ul>
<!-- Loop through each of the users (from MainController/index and
Create a link from MainController/personal and the "id" of the user.
This will then call MainController/personal/id and the id will be used
as a parameter to personal. This in turn will allow us to find the user
and go to their "home" page. The "angle bracket question mark r" are
used to put ruby code inside the template and any valid ruby code can
go here.
-->

<?r @users.each do | user | ?>
<li> <a href="#{Rs(:personal)}/#{user[:id]}">#{user[:first_name].capitalize} #{user[:last_name].capitalize}</a> </li>
<?r end ?>
</ul>


We start with the header and then next we create a list with the ul element. Next is the templating. We have a:
<?r @users.each do | user | ?&gt.

This is really just ruby code that's going to loop through each of the users that we created in the controller. The next line is a bit complex, although there's still no magic:
<li> <a href="#{Rs(:personal)}/#{user[:id]}">#{user[:first_name].capitalize} #{user[:last_name].capitalize}</a> </li>

We're going to create a link here that looks like personal/id. Remember that one of the columns we added to user was the id. Next we're going to make the text of the link the first name and last name and we're going to capitalize them to make them look nicer in case they weren't input this way (which we didn't in the migration).

Finally, we have:
<?r end ?>

which ends the each we started above. What all of this does is create a list of links to the personal page of each user using their name as the text.

Let's go back now to the personal(id) method in the main_controller. The first thing to notice is that we have a paramter on the method and that this is the first time we've seen that on a controller method. Basically, if we have a method x in a controller and we have a url that looks like http://localhost:70000/x/y/z, then y and z are passed to the x method as parameters. Here we're going to use the id to grab the correct user via the User.find() method, and generate a "home" page for them.

Here's the view/personal.xhtml file

<!-- view/personal.xhtml -->
<!-- Just create a headline with the user's first and last name plus Home Page
and then their "personal" data. In this case just their age.
-->

<h2> #{@user.first_name.capitalize} #{@user.last_name.capitalize}'s Home Page <h2>
#{@user.first_name.capitalize} is #{@user.age} years old.


Once again, this is pretty simple. We just grab variables from the @user (note this is not the same user variable as in the view/index.xhtml file. The latter was a local variable for the loop, the one here is from the personal method.

The view/page.xhtml and public/page.css are the same as our last post