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

THE RAILS WAY 376

XMLHttpRequest vs. <iframe>

So, you ask, what’s all the hype about? I did this with <iframe> for years!

While it’s true you can do something along the lines of what XMLHttpRequest does, iframes are not nearly as flexible nor as clean as AJAX to use. Unlike the <iframe> approach, with AJAX

it’s easy to do GET, POST, and other HTTP request types,

the DOM is not altered in any way,

you have powerful callback hooks,

there’s a clean API, and

you can customize HTTP headers.

Considering all this, it’s obvious that XMLHttpRequest provides a far cleaner and more powerful programming model than that of iframes.

18.2 The Rails Way

Rails has built-in support for AJAX calls, which makes it very easy to put your application on track with the Web, version 2.0.

First of all, it has the prototype,4effects, dragdrop, and controls JavaScript

prototype

libraries built-in. These library neatly wrap all sorts of useful AJAX and

 

DOM manipulation stuff in a nice, object-oriented way.

 

The second thing is JavascriptHelper, a module that defines the methods

JavascriptHelper

we’ll be looking at in the rest of this chapter. It wraps JavaScript access in

 

pristine Ruby code, so you won’t have to switch to another language when

 

using AJAX. Talk about total integration.

 

To use any of the functions defined by JavascriptHelper, you first have to

 

include the prototype.js file in your application. Do this by making this call

 

in the <head> section of your .rhtml page.

 

<%= javascript_include_tag "prototype" %>

For the code in this chapter, we’ve added the call to javascript_include_tag to our overall application.rhtml layout file, making the library available to all of our examples.

4http://prototype.conio.net

Prepared exclusively for Rida Al Barazi

Report erratum

 

THE RAILS WAY

377

 

You also need the prototype.js file in your application’s public/javascripts

 

 

directory. It’s included by default if you generate your application’s struc-

 

 

ture by running the rails command.

 

 

link_to_remote

 

 

The syntax for making a basic AJAX call from an .rhtml template can be as

 

 

simple as

 

File 196

<%= link_to_remote("Do the Ajax thing",

 

 

:update => 'mydiv',

 

 

:url => { :action => :say_hello }) %>

 

 

<div id="mydiv">This text will be changed</div>

 

 

This basic form of the link_to_remote( ) method takes three parameters.

 

 

• The text for the link

 

 

• The id= attribute of the element on your page to update

 

 

• The URL of an action to call, in url_for( ) format

 

 

When the user clicks on the link, the action (say_hello in this case) will be

 

 

invoked in the server. Anything rendered by that action will be used to

 

 

replace the contents of the mydiv element on the current page.

 

 

The view that generates the response should not use any Rails layout

 

 

wrappers (because you’re updating only part of an HTML page). You can

 

 

disable the use of layouts by making a call to render( ) with the :layout option

 

 

set to false or by specifying that your action shouldn’t use a layout in the

 

 

first place (see Section 17.9, Locating Layout Files, on page 357, for more

 

 

on this).

 

 

So, let’s define an action.

 

File 186

def say_hello

 

 

render(:layout => false)

 

 

end

 

 

And then define the corresponding say_hello.rhtml view.

 

File 200

<em>Hello from Ajax!</em> (Session ID is <%= session.session_id %>)

 

 

Try it. The text “This text will be changed” in the <div> element with

 

id="mydiv" will be changed (see Figure 18.2, on the following page) to something like

Hello from Ajax! (Session ID is <some string>)

It’s that easy. The session id is included to show off one more thing— cookie handling. Session information is handled transparently by the

Prepared exclusively for Rida Al Barazi

Report erratum

THE RAILS WAY 378

Figure 18.2: Before and After Calling the Action via AJAX

underlying XMLHttpRequest. You’ll always get the correct user’s session, regardless of whether an action is called by AJAX or not.

Behind the Scenes

Let’s have a look at what happened during our link_to_remote example.

First, let’s take a quick look at the HTML code generated by link_to_remote( ).

<a href="#" onclick="new Ajax.Updater('mydiv', '/example/say_hello', {asynchronous:true}); return false;">

Do the AJAX thing

</a>

link_to_remote( ) generates an HTML <a> tag that, when clicked, generates a new instance of Ajax.Updater (which is defined in the Prototype library).

This instance calls XMLHttpRequest internally, which in turn generates an HTTP POST request to the URL given as second parameter.5 This process is shown in Figure 18.3, on the next page.

Let’s see what happens on the server.

127.0.0.1 - - [21/Apr/2005:19:55:26] "POST /example/say_hello HTTP/1.1" 200 51

5For security reasons you can safely call URLs only on the same server/port as the page that includes the call to XMLHttpRequest.

Prepared exclusively for Rida Al Barazi

Report erratum

THE RAILS WAY 379

Browser

new

 

 

 

XMLHttpRequest

 

 

 

 

XMLHttpRequest

 

Do something with

Ajax.Request()

 

 

 

send()

 

 

 

readyState == complete

 

the returned HTML

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

asynchronous (non-blocking) call

 

 

raises event

 

 

 

Server

 

 

 

 

 

 

 

 

 

 

HTML

 

 

 

 

 

 

 

Rails action

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Figure 18.3: XMLHttpRequest Connects Browser and Server

The web server (WEBrick, in this case) got an HTTP POST request to call /example/say_hello. From the server’s perspective this looks just like a normal, run-of-the-mill HTTP POST. That’s not surprising, because that’s what it is.

The server then returns the output of the action being called (in this case say_hello( )) to the XMLHttpRequest object that got created behind the scenes by link_to_remote( ). The Ajax.Updater instance takes over and replaces the contents of the element given as first parameter (in this case mydiv) with the data returned from the XMLHttpRequest object. The browser updates the page to reflect the new content. As far as the user is concerned, the page simply changes.

form_remote_tag()

You can easily change any Rails form to use AJAX by replacing the call to form_tag( ) with form_remote_tag( ).

This method automatically serializes all form elements and sends them to the server, again using XMLHttpRequest. No change to your action is required—it simply receives its data as it normally would.6

Let’s build a game. says “Ruby on ...” the controller.

The object is to complete a simple phrase: the game and the user has to supply the missing word. Here’s

File 187

class

GuesswhatController < ApplicationController

 

def

index

 

end

 

 

def

guess

@guess = params[:guess] || ''

if @guess.strip.match /^rails$/i

6There is one exception: you can’t use file upload fields with form_remote_tag( ), because JavaScript can’t get at file contents. This is a security (and performance) constraint imposed by the JavaScript model.

Prepared exclusively for Rida Al Barazi

Report erratum

THE RAILS WAY 380

render(:text => "You're right!") else

 

render(:partial => 'form')

 

end

 

end

 

end

 

The index.rhtml template file looks like this.

File 204

<h3>Guess what!</h3>

 

<div id="update_div" style="background-color:#eee;">

 

<%= render(:partial => 'form') %>

 

</div>

 

Finally, the main part of this hip new game that will make you rich and

 

famous is the _form.rhtml partial.

File 203

<% if @guess %>

 

<p>It seems '<%=h @guess %>' is hardly the correct answer</p>

 

<% end %>

 

<%= form_remote_tag(:update => "update_div",

 

:url

=> { :action => :guess } ) %>

<label for="guess">Ruby on .....?</label>

<%=

text_field_tag :guess %>

<%=

submit_tag "Post form with AJAX" %>

<%= end_form_tag %>

 

Try it out—it’s not too hard to find the answer, as shown in Figure 18.4, on the following page.

form_remote_tag( ) is a great way to add on-the-fly inline forms for things such as votes or chats to your application, all without having to change anything about the page it’s embedded in.

Partial templates help you honor the DRY principle—use the partial when initially displaying the form, and use it from your AJAX’d action. No change necessary.

Observers

Observers let you call AJAX actions when the user changes data on a form or in a specific field of a form. You can put this to good use to build a real-time search box.

File 198

<label for="search">Search term:</label>

 

<%= text_field_tag :search %>

 

<%= observe_field(:search,

:frequency

=> 0.5,

:update

=> :results,

:url

=> { :action => :search }) %>

<div id="results"></div>

 

Prepared exclusively for Rida Al Barazi

Report erratum

THE RAILS WAY 381

Figure 18.4: AJAX-Style Forms Update Inside Existing Window

Prepared exclusively for Rida Al Barazi

Report erratum

THE RAILS WAY 382

Figure 18.5: Build Real-Time Searches with Observers

The observer waits for changes to the given form field, checking every :frequency seconds. By default, observe_field uses the current value of the text field as the raw POST data for the action. You can access this data in your controller using request.raw_post.

Having set up the observer, let’s implement the search action. We want to implement a search over a list of words in an array, with nice highlighting of the search term in the result.

File 186

WORDLIST = %w(Rails is a full-stack, open-source web framework in Ruby

 

for writing real-world applications with joy and less code than most

 

frameworks spend doing XML sit-ups)

File 186

def search

 

 

@phrase

= request.raw_post || request.query_string

 

matcher

= Regexp.new(@phrase)

 

@results = WORDLIST.find_all { |word| word =~ matcher }

 

render(:layout => false)

 

end

 

 

The view, in search.rhtml, looks like this.

File 201

<% if @results.empty? %>

 

'<%=h @phrase %>' not found!

 

<% else %>

 

'<%=h @phrase %>' found in

 

<%= highlight(@results.join(', '), @phrase) %>

 

<% end %>

 

Point your browser at the observer action, and you’ll get a nice text field

 

with real-time search capability (see Figure 18.5 ). Note that in this exam-

 

ple, the search supports regular expressions.

Prepared exclusively for Rida Al Barazi

Report erratum

 

 

THE RAILS WAY

383

 

The @phrase = request.raw_post || request.query_string line allows you to test

 

 

your search by entering a URL such as /controller/search?ruby directly in

 

 

the browser—the raw POST data won’t be present, so the action will use

 

 

the query string instead.

 

 

 

The action invoked by an observer shouldn’t be overly complex. It might

 

 

get called very often, depending on the frequency you set and how quickly

 

 

your user types. In other words, avoid heavy database lifting or other

 

 

expensive operations. Your user will thank you for it too, as he or she will

 

 

experience a snappier interface.

 

 

 

Periodic Updates

 

 

 

The third helper function, periodically_call_remote( ), helps if you want to

 

 

keep part of your page refreshed by periodically calling the server via AJAX.

 

 

As an example, we’ll show a process list from the server, updating it every

 

 

couple of seconds. This example uses the ps command, so it’s fairly Unix-

 

 

specific. Putting the command in backquotes returns its output as a

 

 

string. Here’s the controller.

 

 

File 186

def periodic

 

 

 

# No action...

 

 

 

end

 

 

 

# Return a process listing (Unix specific code)

 

 

def ps

 

 

 

render(:text => "<pre>" + CGI::escapeHTML(‘ps -a‘) + "</pre>")

 

 

end

 

 

 

And here’s the periodic.rhtml template. This contains the call to periodi-

 

 

cally_call_remote( ).

 

 

File 199

<h3>Server processes:</h3>

 

 

 

<div id="process-list" style="background-color:#eee;">

 

 

</div>

 

 

 

<%= periodically_call_remote(:update

=> 'process-list',

 

:url

=>

{

:action => :ps },

:frequency =>

2

)%>

If you’ve paid extra for the embedded web server version of this book, you’ll see Figure 18.6, on the following page update the list every two seconds (you should see the TIME column for the “ruby script/server” process go up with each iteration!). If you just bought the paper or PDF copies, you’ll have to take our word for it.

Prepared exclusively for Rida Al Barazi

Report erratum

THE USER INTERFACE, REVISITED 384

Figure 18.6: Keeping Current Using periodically_call_remote

18.3 The User Interface, Revisited

Web applications traditionally offer a less interactive user interfaces than traditional desktop applications. They didn’t really need more—until now. With the emergence of Web 2.0 this has to change, as we’ve been given boatloads of control over what happens on a web page with AJAX.

The Prototype library overcomes this problem, helping your application communicate with the user in an intuitive way. And it’s fun, too!

Besides the support for making AJAX calls, the Prototype library offers a wealth of useful objects to make your life easier and your users’ experience better at the same time.

The functionality offered by the Prototype library falls into the following groups.

AJAX calls (which we’ve already discussed)

Document Object Model (DOM) manipulation

Visual effects

Document Object Model Manipulation

The standard support for DOM manipulation in JavaScript is cumbersome and clunky, so Prototype delivers handy shortcuts for a number of often-

Prepared exclusively for Rida Al Barazi

Report erratum

THE USER INTERFACE, REVISITED 385

used operations. These functions are all JavaScript and are intended to be invoked from within the pages delivered to the browser.

$(id)

Pass the $( ) method a string, and it returns the DOM element with the given id. Otherwise it returns its argument. (This behavior means you can pass in either an element’s id= attribute or the element itself and get an element returned.)

$('mydiv').style.border = "1px solid red"; /* sets border on mydiv */

Element.toggle(element, ...)

Element.toggle( ) toggles whether the given element (or elements) are shown. Internally, it switches the value of the CSS display attribute between ’inline’ and ’none’.

Element.toggle('mydiv');

/* toggles mydiv */

Element.toggle('div1', 'div2', 'div3'); /* toggles div1-div3 */

Element.show(element, ...)

Element.show( ) ensures all elements it receives as parameters will be shown.

Element.show('warning'); /* shows the element with id 'warning' */

Element.hide(element, ...)

Opposite of Element.show( ).

Element.remove(element)

Element.remove( ) completely removes an element from the DOM.

Element.remove('mydiv');

/* completely erase mydiv */

Insertion methods

The various insertion methods make it easy to add HTML fragments to existing elements. They are discussed in Section 18.4, Replacement Techniques, on page 389.

Visual Effects

Because AJAX works in the background, it’s transparent to the user. The server may receive an AJAX request, but the user doesn’t necessarily get any feedback about what’s going on. The browser doesn’t even indicate that a page is loading. The user might click a button to delete an entry from a to-do list, and that button might send off a request to the server, but without feedback, how is the user to know what’s happening? And, typically, if they don’t see something happening, the average user will just click the button, over and over.

Prepared exclusively for Rida Al Barazi

Report erratum

THE USER INTERFACE, REVISITED 386

Our job then is to provide feedback when the browser doesn’t. We need to let the user know visually that something is happening. This is a two-step process. First, we can use various DOM manipulation techniques to do things to the browser display to mirror what is happening on the server. However, on its own, this approach might not be enough.

For example, take a link_to_remote( ) call that deletes a record from your database and then empties out the DOM element that displayed that data. For your user, the element seems to disappear on their display instantly. In a traditional desktop application, this would be not be a big deal, as users take this behavior for granted. In a web application, this can cause problems: your user might just not “get it.”

That’s why there’s the second step. You should use effects to provide feedback that the change has been made. If the record disappears in an animated “puff” or fades out smoothly, your user will be happier believing that the action he or she chose really took place.

Visual effects support is bundled into its own JavaScript library, effects.js. As it depends on prototype.js, you’ll need to include both if you want to use effects on your site (probably by editing the layout template).

<%= javascript_include_tag "prototype", "effects" %>

There are two types of effects: one-shot effects and repeatedly callable effects.

One-Shot Effects

These effects are used to convey a clear message to the user: something is gone, or something had been changed or added. All these effects take one parameter, an element on your page. You should use a JavaScript string containing the id of an element: new Effect.Fade(’id_of_an_element’). If you use an effect inside an element’s events, you can also use the new Effect.Fade(this) syntax—this way you won’t have to use an id attribute if you don’t otherwise need it.

Effect.Appear(element)

This effect changes the opacity of the given element smoothly from 0% to 100%, fading it in smoothly.

Effect.Fade(element)

The opposite of Effect.Appear( )—the element will fade out smoothly, and its display CSS property will be set to none at the end (which will take the element out of the normal page flow).

Prepared exclusively for Rida Al Barazi

Report erratum

THE USER INTERFACE, REVISITED 387

Effect.Highlight(element)

Use the illustrious Yellow Fade Technique7 on the element, making its background fade smoothly from yellow to white. A great way to tell your user that some value has been updated not only in the browser but on the server, too.

Effect.Puff(element)

Creates the illusion that an element disappears in a gently expanding cloud of smoke. Fades out the element, and scales it up at the same time. At the end of the animation, the display property will be set to none (see Figure 18.7, on the following page).

Effect.Squish(element)

Makes the element disappear by smoothly making it smaller.

The screenshots in Figure 18.7 were generated by the following template. The code at the top is a helper method that sets up an alternating style for the squares in the grid. The loop at the bottom creates the initial set of 16 squares. When a Destroy link is clicked, the destroy action in the controller is called. In this example, the controller does nothing, but in real life it might remove a remove from a database table. When the action completes, the Puff effect is invoked on the square that was clicked, and away it goes.

File 194

<% def style_for_square(index)

 

color = (index %

2).zero? ? "#444" : "#ccc"

 

%{ width: 150px;

height: 120px; float: left;

padding: 10px; color: #fff; text-align:center; background: #{color} }

end

%>

<% 16.times do |i| %>

<div id="mydiv<%= i %>" style="<%= style_for_square(i) %>"> <div style="font-size: 5em;"><%= i %></div>

<%= link_to_remote("Destroy",

:complete => "new Effect.Puff('mydiv#{i}')", :url => { :action => :destroy, :id => i }) %>

</div> <% end %>

Repeatedly Callable Effects

Effect.Scale(element, percent)

This effect smoothly scales the given element. If you scale a <div>, all contained elements must have their width and height set in em

7As evangelized by 37signals; see http://www.37signals.com/svn/archives/000558.php.

Prepared exclusively for Rida Al Barazi

Report erratum

THE USER INTERFACE, REVISITED 388

Figure 18.7: Up and Away...

units. If you scale an image, width and height are not required to be set.

Let’s do some scaling on an image.

<%= image_tag("image1",

:onclick => "new Effect.Scale(this, 100)") %>

You can also do this with text, if you use em units for your font sizes.

<%= content_tag("div",

"Here is some text that will get scaled.",

:style

=>

"font-size:1.0em; width:100px;",

:onclick

=>

"new Effect.Scale(this, 100)") %>

Element.setContentZoom(element, percent)

This effect provides a nonanimated way to set the scale of text and other elements that use em units.

<div id="outerdiv"

style="width:200px; height:200px; border:1px solid red;"> <div style="width:10em; height:2em; border:1px solid blue;">

First inner div

</div>

<div style="width:150px; height: 20px; border:1px solid blue;"> Second inner div

</div> </div>

<%= link_to_function("Small", "Element.setContentZoom('outerdiv', 75)") %>

Prepared exclusively for Rida Al Barazi

Report erratum