Showing posts with label ajax. Show all posts
Showing posts with label ajax. Show all posts

Sunday, January 29, 2012

Rails / Ajax / Autocomplete

As you all know if you're regular readers here, I started out in the Ruby web framework world with Ramaze and I'm definitely glad that I did. However, it doesn't seem like there's much of a job market for Ramaze programmers, so I've taken up using Rails, specifically Rails 3.1. There's a number of great books for Rails and the one I'd recommend is Ruby on Rails 3 Tutorial by Michael Hartl. In this book, he develops a Twitter like application in Rails starting with basic pages and moving on to more complex topics.

I've been working on a simple project for a greeting card application and I came up with a little something that might be interesting. The cards have a recipient and can have multiple signers. I added autocomplete for the recipient, but it was a bit harder for the comma separated list of signers. What I ended up doing was was simply grabbing the last piece of the list in the controller and passing that back to the view.

Here's the view code ...


Create a Card


<%= image_tag "card_images/#{@template.image_name}", :size => "200x200" %>
<%= form_tag ("create_from_image") do %>

<%= label_tag("Add the recipient's email: ") %>


<%= text_field_tag(:recipient_email, "#{@recipient_email}")%>



<%= label_tag("Add A Greeting: ") %>


<%= text_field_tag(:greeting, "#{@greeting}")%>



<%= label_tag("Add the signers' emails (comma separated): ") %>


<%= text_field_tag(:signers_email, "#{@signers}")%>



<%= hidden_field_tag :template_id, @template.id %>

<%= submit_tag("Preview the card!") %>
<%= submit_tag("Send the card!") %>
<% end %>



There's nothing very interesting in here, but the main field we're interested in is the :signers_email text field.

Here's the javascript code for it ...


$(document).ready(function(){
// Below is the name of the textfield that will be autocomplete
$('#recipient_email, #signers_email').autocomplete({
// This shows the min length of charcters that must be
// typed before the autocomplete looks for a match.
            minLength: 2,
// This is the source of the auocomplete suggestions. In this case a
// list of emails from the users controller, in JSON format.
            source: '/users/index.json'
})

});


What this says is that for both the recipient_email and signers_email fields, use autocomplete, send when we have two characters, and use the /users/index.json function to get the data we need.

Finally, we have the controller code ...


def index
@title = "All users"
@suggestion = "Pick someone and send them a card!"
if params[:term]
search_term = params[:term].split(",").last.strip
@users = User.find(:all, :conditions => ['email LIKE ?', "#{search_term}%"])
@users_hash = []
@users.each do |user|
@users_hash << { "label" => user.email }
end

else
@users = User.where(:active => true).paginate(:page => params[:page])
end

respond_to do |format|
format.html

# Here is where you can specify how to handle the request for "/people.json"
format.json { render :json => @users_hash }
end
end


Here, we have a couple of things going on ... for the Ajax side, we'll get the params[:term] field so we know that we need to get the constrained list. First, we're going to grab the params[:term] value, which should look something like "abc@test.com, def". Here we'd like to look for emails that start with the "def" and ignore the "abc@test.com" piece. So ... we split on the comma, grab the final element in the array, and then remove any leftover spaces at the end or beginning of the string. Next, we'll create an array where each element is a hash of the form { "label" => "defghi@test.com" }. This is the form that the jquery autocomplete needs. Finally, we use the respond_to section to send this back as json.

As always, let me know if you have questions.

Wednesday, September 16, 2009

Ramaze, Full Calendar, JSON, and AJAX

Where I work we have numerous environments for different customers and each of these environments can have different versions of our software running depending on where the customers are in terms of their release cycles. It became apparent a while back that what we really needed was a calendar to plan the deployments to these different environments. Since we also wanted multiple views, I decided to look into writing a little Ramaze application that would handle this task. I'm not going to show the entire application here, but will show how to use the jQuery calendar I selected, FullCalendar (http://arshaw.com/fullcalendar/) and how to use the Ruby JSON gem.

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.

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.