Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 1st Edition (2005).pdf
Скачиваний:
28
Добавлен:
17.08.2013
Размер:
7.99 Mб
Скачать

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