- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •Models, Views, and Controllers
- •Installing Rails
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Unix/Linux
- •Rails and Databases
- •Keeping Up-to-Date
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B2: Add Page Decorations
- •Task C: Cart Creation
- •Sessions
- •More Tables, More Models
- •Iteration C1: Creating a Cart
- •Iteration C3: Finishing the Cart
- •Task D: Checkout!
- •Iteration D2: Show Cart Contents on Checkout
- •Task E: Shipping
- •Iteration E1: Basic Shipping
- •Task F: Administrivia
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Finishing Up
- •More Icing on the Cake
- •Task T: Testing
- •Tests Baked Right In
- •Testing Models
- •Testing Controllers
- •Using Mock Objects
- •Test-Driven Development
- •Running Tests with Rake
- •Performance Testing
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Active Support
- •Logging in Rails
- •Debugging Hints
- •Active Record Basics
- •Tables and Classes
- •Primary Keys and IDs
- •Connecting to the Database
- •Relationships between Tables
- •Transactions
- •More Active Record
- •Acts As
- •Aggregation
- •Single Table Inheritance
- •Validation
- •Callbacks
- •Advanced Attributes
- •Miscellany
- •Action Controller and Rails
- •Context and Dependencies
- •The Basics
- •Routing Requests
- •Action Methods
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Builder templates
- •RHTML Templates
- •Helpers
- •Formatting Helpers
- •Linking to Other Pages and Resources
- •Pagination
- •Form Helpers
- •Layouts and Components
- •Adding New Templating Systems
- •Introducing AJAX
- •The Rails Way
- •Advanced Techniques
- •Action Mailer
- •Sending E-mail
- •Receiving E-mail
- •Testing E-mail
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Securing Your Rails Application
- •SQL Injection
- •Cross-Site Scripting (CSS/XSS)
- •Avoid Session Fixation Attacks
- •Creating Records Directly from Form Parameters
- •Knowing That It Works
- •Deployment and Scaling
- •Picking a Production Platform
- •A Trinity of Environments
- •Iterating in the Wild
- •Maintenance
- •Finding and Dealing with Bottlenecks
- •Case Studies: Rails Running Daily
- •Appendices
- •Introduction to Ruby
- •Ruby Names
- •Regular Expressions
- •Source Code
- •Cross-Reference of Code Samples
- •Resources
- •Index
ADVANCED TECHNIQUES 389
<%= link_to_function("Medium", "Element.setContentZoom('outerdiv', 100)") %> <%= link_to_function("Large", "Element.setContentZoom('outerdiv', 125)") %>
Note that the size of the second inner <div> does not change, as it does not use em units.
18.4 Advanced Techniques
In this section we’ll look at some more advanced AJAX.
Replacement Techniques
As we’ve mentioned earlier, the Prototype library provides some advanced replacement techniques that do more than just overwrite an element’s contents. You call these using the various Insertion objects.
Insertion.Top(element, content)
Inserts an HTML fragment after the start of an element.
new Insertion.Top('mylist', '<li>Wow, I\'m the first list item!</li>');
Insertion.Bottom(element, content)
Inserts an HTML fragment immediately before the end of an element. You can use this for example to insert new table rows at the end of a <table> element or new list items at the end of an <ol> or <ul> element.
new Insertion.Bottom('mytable', '<tr><td>We\'ve a new row here!</td></tr>');
Insertion.Before(element, content)
Inserts an HTML fragment before the start of an element.
new Insertion.Before('mypara', '<h1>I\'m dynamic!</h1>');
Insertion.After(element, content)
Inserts an HTML fragment after the end of an element.
new Insertion.After('mypara', '<p>Yet an other paragraph.</p>');
More on Callbacks
You can use four JavaScript callbacks with the methods link_to_remote( ), form_remote_tag( ), and observe_xxx. These callbacks automatically have access to a JavaScript variable called request, which contains the corresponding XMLHttpRequest object.
:loading( )
Invoked when the XMLHttpRequest starts sending data to the server (that is, when it makes the call).
Prepared exclusively for Rida Al Barazi
Report erratum
ADVANCED TECHNIQUES 390
:loaded( )
Invoked when all the data has been sent to the server, and XMLHttpRequest now waits for the server response.
:interactive( )
This event is triggered when data starts to come back from the server. Note that this event’s implementation is very browser-specific.
:complete( )
Invoked when all data from the server’s response has been received and the call is complete.
For now, you probably don’t want to use the :loaded( ) and :interactive( ) callbacks—they can behave very differently depending on the browser. :loading( ) and :complete( ) will work with all supported browsers and will always be called exactly once.
link_to_remote( ) has several additional parameters for more flexibility.
:confirm
Use a confirmation dialog, just like :confirm on link_to( ).
:condition
Provide a JavaScript expression that gets evaluated (on clicking the link); the remote request will be started only if the expression returns true.
:before,:after
Evaluate a JavaScript expression immediately before and/or after the AJAX call is made. (Note that :after doesn’t wait for the return of the call. Use the :complete callback instead.)
The request object holds some useful methods.
request.responseText
Returns the body of the response returned by the server (as a string).
request.status
Returns the HTTP status code returned by the server (i.e., 200 means success, 404 not found).8
request.getResponseHeader(name)
Returns the value of the given header in the response returned by the server.
8See Chapter 10 of RFC 2616 for possible status codes. It’s available online at
http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
Prepared exclusively for Rida Al Barazi
Report erratum
ADVANCED TECHNIQUES 391
Progress Indicators
You can use the callbacks to give your users feedback that something’s going on.
Take a look at this example.
<%= text_field_tag :search %> |
||
<%= image_tag("indicator.gif", |
||
:id |
=> 'search-indicator', |
|
:style => 'display:none') %> |
||
<%= observe_field("search", |
||
:update |
=> :results, |
|
:url |
=> { :action => :search}, |
|
:loading |
=> "Element.show('search-indicator')", |
|
:complete |
=> "Element.hide('search-indicator')") %> |
The image indicator.gif will be displayed only while the AJAX call is active. For best results, use an animated image.9
For the text_field( ) autocompletion feature, indicator support is already built in.
<%= text_field(:items, :description,
:remote_autocomplete => { :action => :autocomplete }, :indicator => "/path/to/image") %>
|
Multiple Updates |
|
|
If you rely heavily on the server to do client-side updates, and need more |
|
|
flexibility than the :update => ’elementid’ construct provides, callbacks may |
|
|
be the answer. |
|
|
The trick is to have the server send JavaScript to the client as part of an |
|
|
AJAX response. As this JavaScript has full access to the DOM, it can |
|
|
update as much of the browser window as you need. To pull off this |
|
|
magic, use :complete => "eval(request.responseText)" instead of :update. You |
|
|
can generate JavaScript within your view that is then delivered to the client |
|
|
and executed. |
|
|
Let’s trigger some random fade effects. First we need the controller. |
|
File 186 |
def multiple |
|
|
end |
|
|
def update_many |
|
|
render(:layout => false) |
|
|
end |
|
|
|
|
|
9Take a look at the various throbbing images that browsers use to indicate page loading |
|
|
is in progress. |
Prepared exclusively for Rida Al Barazi
Report erratum
ADVANCED TECHNIQUES |
392 |
|
Not much going on there. The multiple.rhtml template is more interesting. |
File 197 |
<%= link_to_remote("Update many", |
|
:complete => "eval(request.responseText)", |
|
:url => { :action => :update_many }) %> |
|
<hr/> |
|
<% style = "float:left; width:100px; height:50px;" %> |
|
<% 40.times do |i| |
|
background = "text-align: center; background-color:##{("%02x" % (i*5))*3};" %> |
|
<%= content_tag("div", |
|
"I'm div #{i}", |
|
:id => "div#{i}", |
|
:style => style + background) %> |
|
<% end %> |
|
This generates 40 <div> elements. The eval(request.responseText) code on |
|
the second line allows us to generate JavaScript in the update_many.rhtml |
|
template. |
File 202 |
<% 3.times do %> |
|
new Effect.Fade('div<%= rand(40) %>'); |
|
<% end %> |
|
Each time “Update many” is clicked, the server sends back three lines of |
|
JavaScript, which in turn fade out up to three random <div> elements! |
|
To insert arbitrary HTML more easily, use the escape_javascript( ) helper |
|
function. This makes sure all ’ and " characters and newlines will get |
|
properly escaped to build a JavaScript string. |
|
new Insertion.Bottom('mytable', |
|
'<%= escape_javascript(render(:partial => "row")) %>'); |
|
If you return JavaScript in the view to be executed by the web browser, you |
|
have to take into account what happens if there is an error while rendering |
|
the page. By default, Rails will return an HTML error page, which is not |
|
what you want in this case (as a JavaScript error will occur). |
|
As this book is going to press, work is underway to add error event han- |
|
dlers to link_to_remote( ) and form_remote_tag( ). Check the documentation |
|
for the latest details. |
|
Dynamically Updating a List |
|
One of the canonical uses for AJAX is updating a list on the user’s browser. |
|
As the user adds or deletes items, the list changes without refreshing the |
|
full page. Let’s write code that does this. It’s a useful technique that also |
|
lets us combine many of the concepts we’ve covered in this chapter. |
Prepared exclusively for Rida Al Barazi
Report erratum
|
ADVANCED TECHNIQUES |
393 |
|
Our application is (yet another) to-do list manager. It displays a simple |
|
|
list of items and has a form where users can add new items. Let’s start by |
|
|
writing the non-AJAX version. It uses conventional forms. |
|
|
Rather than bother with database tables, we’ll experiment using an in- |
|
|
memory model class. Here’s item.rb, which goes in app/models. |
|
File 190 |
class Item |
|
|
attr_reader :body |
|
|
attr_reader :posted_on |
|
|
FAKE_DATABASE = [] |
|
|
def initialize(body) |
|
|
@body = body |
|
|
@posted_on = Time.now |
|
|
FAKE_DATABASE.unshift(self) |
|
|
end |
|
|
def self.find_recent |
|
|
FAKE_DATABASE |
|
|
end |
|
|
# Populate initial items |
|
|
new("Feed cat") |
|
|
new("Wash car") |
|
|
new("Sell start-up to Google") |
|
|
end |
|
|
The controller provides two actions, one to list the current items and the |
|
|
second to add an item to the list. |
|
File 189 |
class ListNoAjaxController < ApplicationController |
|
|
def index |
|
|
@items = Item.find_recent |
|
|
end |
|
|
def add_item |
|
|
Item.new(params[:item_body]) |
|
|
redirect_to(:action => :index) |
|
|
end |
|
|
end |
|
|
The view has a simple list and a form to add new entries. |
|
File 208 |
<ul id="items"> |
|
|
<%= render(:partial => 'item', :collection => @items) %> |
|
|
</ul> |
|
|
<%= form_tag(:action => "add_item") %> |
|
|
<%= text_field_tag('item_body') %> |
|
|
<%= submit_tag("Add Item") %> |
|
|
<%= end_form_tag %> |
|
|
It uses a trvial partial template for each line. |
|
File 207 |
<li> |
|
|
<p> |
|
<%= item.posted_on.strftime("%H:%M:%S") %>: <%= h(item.body) %>
</p> </li>
Prepared exclusively for Rida Al Barazi
Report erratum
ADVANCED TECHNIQUES 394
Now let’s add AJAX support to this application. We’ll change the form to submit the new to-do item via XMLHttpRequest, having it store the resulting rendered item into the top of the list on the existing page.
<ul id="items">
<%= render(:partial => 'item', :collection => @items) %>
</ul>
<%= form_remote_tag(:url => { :action => "add_item" }, :update => "items",
:position => :top) %> <%= text_field_tag('item_body') %>
<%= submit_tag("Add Item") %> <%= end_form_tag %>
We then change the controller to render the individual item in the add_item method. Note how the action shares the partial with the view. This is a common pattern; the view uses the partial template to render the initial list, and the controller uses it to render new items as they are created.
File 188 |
def add_item |
|
item = Item.new(params[:item_body]) |
|
render(:partial => "item", :object => item, :layout => false) |
|
end |
However, we can do better than this. Let’s give the user a richer experience. We’ll use the :loading and :complete callbacks to give them visual feedback as their request is handled.
•When they click the Add Item button, we’ll disable it and show a message to say we’re handling the request.
•When the response is received, we’ll use the hip Yellow Fade to highlight the newly added item in the list. We’ll remove the busy message, reenable the Add Item button, clear out the text field, and put focus into the field ready for the next item to be entered.
That’s going to require two JavaScript functions. We’ll put these in a <script> section in our page header, but the header is defined this the page template. We’d rather not write a special template for each of the different actions in the controller, so we’ll parameterize the layout for the whole controller using the content for system. This is both simple and powerful. In the template for the action, we can use the content_for declaration to capture some text and store it into an instance variable. Then, in the template, we can interpolate the contents of that variable into (in this case) the HTML page header. In this way, each action template can customize the shared page template.
In the index.rhtml template we’ll use the content_for( ) method to set the @contents_for_page_scripts variable to the text of the two function definitions. When this template is rendered, these functions will be included in the
Prepared exclusively for Rida Al Barazi
Report erratum
ADVANCED TECHNIQUES |
395 |
|
layout. We’ve also added callbacks in the form_remote_tag call and created |
|
|
the message that we toggle to say the form is processing a request. |
|
File 206 |
<% content_for("page_scripts") do -%> |
|
|
function item_added() { |
|
|
var item = $('items').firstChild; |
|
|
new Effect.Highlight(item); |
|
|
Element.hide('busy'); |
|
|
$('form-submit-button').disabled = false; |
|
|
$('item-body-field').value = ''; |
|
|
Field.focus('item-body-field'); |
|
|
} |
|
|
function item_loading() { |
|
|
$('form-submit-button').disabled = true; |
|
|
Element.show('busy'); |
|
|
} |
|
|
<% end -%> |
|
|
<ul id="items"> |
|
|
<%= render(:partial => 'item', :collection => @items) %> |
|
|
</ul> |
|
|
<%= form_remote_tag(:url => { :action => "add_item" }, |
|
|
:update |
=> "items", |
|
:position => :top, |
|
|
:loading |
=> 'item_loading()', |
|
:complete => 'item_added()') %> |
|
|
<%= text_field_tag('item_body', '', :id => 'item-body-field') %> |
|
|
<%= submit_tag("Add Item", :id => 'form-submit-button') %> |
|
|
<span id='busy' style="display: none">Adding...</span> |
|
|
<%= end_form_tag %> |
|
|
Then in the page template we’ll include the contents of the instance vari- |
|
|
able @contents_of_page_scripts in the header. |
|
File 205 |
<html> |
|
|
<head> |
|
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> |
|
|
<%= javascript_include_tag("prototype", "effects") %> |
|
|
<script type="text/javascript"><%= @content_for_page_scripts %></script> |
|
|
<title>My To Do List</title> |
|
|
</head> |
|
|
<body> |
|
|
<%= @content_for_layout %> |
|
|
</body> |
|
|
In general, this approach of starting with a non-AJAX page and then |
|
|
adding AJAX support lets you work on the application level first and then |
|
|
focus in on presentation. |
|
Using Effects without AJAX
Using the effects without AJAX is a bit tricky. While it’s tempting to use the window.onload event for this, your effect will occur only after all elements in the page (including images) have been loaded.
Prepared exclusively for Rida Al Barazi
Report erratum
|
ADVANCED TECHNIQUES |
396 |
|
Placing a <script> tag directly after the affected elements in the HTML is |
|
|
an alternative, but this can cause rendering problems (depending on the |
|
|
contents of the page) with some browsers. If that is the case, try inserting |
|
|
the <script>tag at the very bottom of your page. |
|
|
The following snippet from an RHTML page would apply the Yellow Fade |
|
|
Technique to an element. |
|
File 195 |
<div id="mydiv">Some content</div> |
|
|
<script type="text/javascript"> |
|
new Effect.Highlight('mydiv');
</script>
Testing
Testing your AJAX’d functions and forms is straightforward, as there is no real difference between them and normal HTML links and forms. There is one special provision to simulate calls to actions exactly as if they were generated by the Prototype library. The method xml_http_request( ) (or xhr( ) for short) wraps the normal get( ), post( ), put( ), delete( ), and head( ) methods, allowing your test code to invoke controller actions as if it were JavaScript running in a browser. For example, a test might use the following to invoke the index action of the post controller.
xhr :post, :index
The wrapper sets the result of request.xhr? to true (see Section 18.4, Called by AJAX?, on the next page).
If you’d like to add browser and functional testing to your web application, have a look at Selenium.10 It lets you check for things such as DOM changes right in your browser. For JavaScript unit testing, you might want to try JsUnit.11
If you stumble across some unexpected behavior in your application, have a look at your browser’s JavaScript console. Not all browsers have good support for this. A good tool is the Venkman12 add-on for Firefox, which supports advanced JavaScript inspection and debugging.
10http://selenium.thoughtworks.com/
11http://www.edwardh.com/jsunit/
12http://www.mozilla.org/projects/venkman/
Prepared exclusively for Rida Al Barazi
Report erratum
|
|
ADVANCED TECHNIQUES |
397 |
|
Backward Compatibility |
|
|
|
Rails has several features that can help make your AJAX’d web application |
|
|
|
work with non-AJAX browsers or browsers with no JavaScript support. |
|
|
|
You should decide early in the development process if such support is |
|
|
|
necessary or not; it may have profound implications on your development |
|
|
|
plans. |
|
|
|
Called by AJAX? |
|
|
|
Use the request.xml_http_request? method, or its shorthand form request.xhr?, |
|
|
|
to check if an action was called via the Prototype library. |
|
|
File 186 |
def checkxhr |
|
|
|
if request.xhr? |
|
|
|
render(:text => "21st century Ajax style.", :layout => false) |
|
|
|
else |
|
|
|
render(:text => "Ye olde Web.") |
|
|
|
end |
|
|
|
end |
|
|
|
Here is the check.rhtml template. |
|
|
File 191 |
<%= link_to_remote('Ajax..', |
|
|
|
:complete => 'alert(request.responseText)', |
|
|
|
:url => { :action => :checkxhr }) %> |
|
|
|
<%= link_to('Not ajax...', :action => :checkxhr) %> |
|
|
|
Adding Standard HTML Links to AJAX |
|
|
|
To add support for standard HTML links to your link_to_remote calls, just |
|
|
|
add an :href => URL parameter to the call. Browsers with disabled JavaScript |
|
|
|
will now just use the standard link instead of the AJAX call—this is partic- |
|
|
|
ularily important if you want your site to be accessible by users with visual |
|
|
|
impairments (and who therefore might use specialized browser software). |
|
|
File 192 |
<%= link_to_remote("Works without JavaScript, too...", |
|
|
|
{ :update => 'mydiv', |
|
|
|
:url |
=> { :action => :say_hello } }, |
|
|
{ :href |
=> url_for( :action => :say_hello ) } ) %> |
|
This isn’t necessary for calls to form_remote_tag( ) as it automatically adds a conventional action= option to the form which invokes the action specified by the :url parameter.. If JavaScript is enabled, the AJAX call will be used, otherwise a conventional HTTP POST will be generated. If you want to different actions depending on whether JavaScript is enabled, add a :html => { :action => URL, : method => ’post’ } parameter. For example, the
Prepared exclusively for Rida Al Barazi
Report erratum
ADVANCED TECHNIQUES 398
following form will invoke the guess action if JavaScript is enabled and the post_guess action otherwise.
File 193 |
<%= form_remote_tag( |
||
|
:update => |
"update_div", |
|
|
:url |
=> |
{ :action => :guess }, |
|
:html |
=> |
{ |
:action => url_for( :action => :post_guess ), :method => 'post' } ) %>
<% # ... %>
<%= end_form_tag %>
Of course, this doesn’t save you from the addtional homework of paying specific attention on what gets rendered—and where. Your actions must be aware of the way they’re called and act accordingly.
Back Button Blues
By definition, your browser’s Back button will jump back to the last page rendered as a whole (which happens primarily via standard HTML links).
You should take that into account when designing the screen flow of your app. Consider the grouping of objects of pages. A parent and its child objects typically fall into a logical group, whereas a group of parents normally are each in disjoint groups. It’s a good idea to use non-AJAX links to navigate between groups and use AJAX functions only within a group. For example, you might want to use a normal link when you jump from a weblog’s start page to an article (so the Back button jumps back to the start page) and use AJAX for commenting on the article.13
Web V2.1
The AJAX field is changing rapidly, and Rails is at the forefront. This makes it hard to produce definitive docmentation in a book—the libraries have moved on even while this book is being printed.
Keep your eyes open for additions to Rails and its AJAX support. As I’m writing this, we’re seeing the start of support for autocompleting text fields (à la Google Suggest) file uploads with progress information, drag-and- drop support, lists where the user can reorder elements on screen, and so on.
A good place to check for updates (and to play with some cool effects) is Thomas Fuch’s site http://script.aculo.us/.
13In fact, that’s what the popular Rails-based weblog software Typo does. Have a look at
http://typo.leetsoft.com/.
Prepared exclusively for Rida Al Barazi
Report erratum