Section 5.2. Putting the R in RJS


5.2. Putting the R in RJS

The kicker is that instead of writing the JavaScript by hand, Rails generates it. That's where RJS, Ruby-generated JavaScript, comes in. RJS is Ruby code that generates JavaScript code, which is sent as the result of an Ajax call. Whereas most actions render data (from .rhtml files or otherwise), RJS is differentit renders instructions. Of course, the instructions sent to the page often contain content (e.g., change the text in that box to this), but it's always within the context of JavaScript code.

The obvious consequence of using Ruby to generate JavaScript is that more of your application is written in Ruby, which drastically simplifies development. As a developer, it's just easier to think in one language, rather than mentally switching gears between Ruby and JavaScript. (Not to mention that we Rails developers tend to love writing Ruby, so we're always looking for new places to put it.)

The Ruby language is well suited for creating Domain Specific Languages (DSLs), mini-languages tuned to a particular task. The most common exemplar of a DSL in Ruby is Rake, Ruby's make-like build system. RJS is another fine examplethink of it as a DSL for generating JavaScript. In fact, once you become accustomed to using RJS, you may start to forget that JavaScript is being used behind the scenes; RJS just feels like a magic remote control for the browser.

5.2.1. Diving In

Eager to see how it all works? Let's look at some examples. To set the stage for the examples in this chapter, make a new clean slate for this chaptera controller and one action, index:

script/generate controller chapter5 index

We'll reuse the same layout (app/views/layouts/application.rhtml) and CSS file (public/stylesheets/application.css) that we set up in Chapter 3.

5.2.2. Rendering JavaScript Without RJS

Before getting into RJS proper, let's take a minute to see what it's like to return JavaScript in an Ajax call without RJS. In order for the browser to know it's JavaScript (and not HTML or some other content type), the response needs to include a Content-Type header, which is accomplished with an option to the render method. Define a new action in the controller, chapter5_controller.rb:

def alert_without_rjs   render :text => "alert('Hello without RJS')",           :content_type => "text/javascript" end

We've seen render :text => ... before, but now we're overriding the Content-Type header, telling the browser to interpret the response body as JavaScript.

Next, in index.rhtml, use the standard link_to_remote helper to send an Ajax call to the new action:

<p><%= link_to_remote "Alert without RJS",         :url => { :action => "alert_without_rjs" } %></p>

Notice a couple of things here. We aren't including an :update option in the link_to_remote because we don't want to insert the response into an element on the page; we want to evaluate it. Try out the link. When Prototype receives an Ajax response with a JavaScript content type, it evaluates the response bodyin this case, a simple alert( ) call. But imagine the power: JavaScript has the ability to change anything about the page.

5.2.3. RJS: Generating JavaScript with Ruby

So far, so goodbut we're still writing plain JavaScript in the controller code. In the case of a simple alert( ) statement, that's not so bad, but anything more complex will get ugly fast. Ruby developers have a low tolerance for ugly code, and eliminating ugly JavaScript is the specialty of RJS. Back in chapter5_controller.rb, define a new action, using render :update to trigger RJS:

def alert_with_rjs   render :update do |page|     page.alert "Hello from inline RJS"   end end

When the render method gets :update as its first argument, it expects a blockthe chunk of code between do and end. The block is passed an instance of the JavaScriptGenerator object, which is conventionally named page. The block can then call any number of methods on page, which generates the corresponding JavaScript, accumulating all the resulting code and returning it with a text/javascript content type.

To see it in action, edit index.rhtml and make a new Ajax link, this time pointing to the alert_with_rjs action, instead of alert_without_rjs. The result will be just the same as beforeexcept that your code has no hand-written JavaScript.

5.2.3.1. Using .rjs files

The last example was inline RJS, because the RJS statements were written right in the action method. Using inline RJS works fine when it's just one or two lines long. But as things get more complicated, you may want to extract the code into .rjs files, which live in the views directory, alongside your .rhtml files. For this example, create views/chapter5/external.rjs:

page.alert "Hello from an RJS file"

External RJS files like this one are identical to what's inside the do...end block of inline RJS. In this case, it's not even necessary to have an external action defined in the controllerRails is intelligent enough to find the correct file even if there is no action. Because it finds a file with the RJS extension, it automatically creates the page object and sets the correct content type for the response.

To see it at work, add another link to index.rhtml, pointing to the external action. The result will be just the same as beforeexcept that your code has no handwritten JavaScript.

<p><%= link_to_remote "Alert with external RJS",         :url => { :action => "external" } %></p>

5.2.3.2. Testing and debugging RJS

Debugging Ajax calls with RJS can be tricky, because if there is an error in the returned JavaScript, it will often fail silently. Rails helps out by making failures noisier during development. When the application is running in the development environment (or if config.action_view.debug_rjs is set to true), all RJS-generated JavaScript will be wrapped in a JavaScript try/catch block, and you'll be notified of any errors in the code. The notification happens with two alert boxes: first, the exception message; second, the actual JavaScript that was generated by the RJS.

As helpful as the RJS debug mode is, intense RJS development usually demands more powerful tools and techniques. Chapter 7 examines the subject of Rails testing and debugging in depth.

5.2.4. Element Proxies

Of course, there's far more to RJS than the alert method. The most common tasks involve interacting with the page elementsthe DOMin some way. RJS makes that natural with element proxies: Ruby objects that represents DOM objects. When you call a method on the proxy, it's passed on directly to the generated JavaScript.

To see it in action, switch to index.rhtml and add a DIV to interact with:

<div  >DIV</div>

To expose a DOM element that was previously hidden, you'd write:

page[:my_div].show

In this example, page[:my_div] is the element proxy, standing for the DOM element with the ID my_div. This is translated into generated JavaScript that's passed to the client:

$('my_div').show(  );

Any method that you can use with $( ) in JavaScript, you can use with element proxies in RJS. In addition to show, you can call hide, toggle, and remove to modify page elements. So to affect the element with the ID my_div, the RJS would look like this:

page[:my_div].hide page[:my_div].toggle page[:my_div].remove

Methods on element proxies can take arguments as well. For example, look at adding and removing CSS classes on an element, through the use of add_class_name and remove_class_name:

page[:my_div].add_class_name :pink page[:my_div].remove_class_name :green

Even JavaScript methods that take a set of options can be generated from Ruby hashes. For example, to set CSS styles on an element, use set_style:

page[:my_div].set_style :width => '500px'

To create a script.aculo.us effect, use the visual_effect method. For example:

page[:my_div].visual_effect :highlight page[:my_div].visual_effect :blind_down, :duration => 5

(See Chapter 4 for an explanation of visual effects and their options.)

Because script.aculo.us' visualEffect method returns the element after creating an effect, you can chain calls with it in RJS. For example:

page[:my_div].visual_effect(:highlight).remove_class_name(:green)

Keep in mind, none of these methods are hard-wired into the RJS element proxythe proxy just passes what it receives through to the JavaScript output. The only difference is that method names in RJS use underscores (following the Ruby convention), but the generated counterparts use camelCase, following the JavaScript convention. For example, note the difference between this RJS statement and its result:

page[:my_div].set_style :width => '500px' #=> '$("my_div").setStyle({"width": "500px"});'

See Chapter 10 for the full details of all of the methods in Prototype's Element object.

RJS can also be used to assign values to properties on element proxies. For example, suppose you have a text field with the ID my_field. To set its value property (i.e., the text inside the field), simply assign it with the element proxy:

page[:my_field][:value] = 'New value'

Nested properties are assignable as well:

page[:foo][:style][:color] = 'red'

5.2.4.1. Custom methods with element proxies

Even custom methods added to Prototype's Element object can be called from RJS. For example, take this bit of JavaScript and put it in public/javascripts/application.js:

Element.addMethods({   upcase: function(element) {     if (!(element = $(element))) return;     element.update(element.innerHTML.toUpperCase(  ));     return element;   },   toggleClassName: function(element, className) {     if (!(element = $(element))) return;     element.hasClassName(className) ?        element.removeClassName(className) :        element.addClassName(className);     return element;   } });

With this code, we extend Prototype by adding two new methods to Element, which is mixed into all DOM elements accessed by $( ). In this case, we're adding an upcase( ) method, which converts all the text inside an element to uppercase, and toggleClassName( ), which adds and removes a given CSS class from an element. The new methods could be used in JavaScript like this:

$('text_div').upcase(  ); $('text_div').toggleClassName('green');

And here's the payoff: without any additional work, your custom methods can be called from your RJS as well, via the element proxy:

page[:text_div].upcase page[:text_div].toggle_class_name 'green'

5.2.4.2. Updating content with element proxies

Ever since Chapter 2 introduced link_to_remote :update => ..., we've been using Ajax to update parts of the page. While that technique is simple and expedient, it has two big drawbacks. First, it can only be used to update one page element at a time. And second, the element that you want to update has to be known ahead of time, when the page is originally rendered. With RJS, those limitations are gone: you can update as many elements as you like, and the targets can be determined on the server side, during the Ajax call.

There are three methods for updating page content with RJS element proxies: replace_html (which replaces just the contents of an element), replace (which replace an entire element), and reload (which automatically renders and replaces a partial with the same name as an element). We'll look at each in turn.

Note that RJS has one other major method for updating element content: insert_html is used to insert content into or around an element. Because it doesn't use element proxies, it's discussed in the upcoming "JavaScriptGenerator Methods" section.

5.2.4.2.1. replace_html and replace

The replace_html and replace methods for element proxies are very similar. The only difference is that replace_html replaces the contents of an element (accessed as innerHTML), while replace replaces the whole element, including its start and end tags (accessed as outerHTML). To see it in action, let's add a couple of links to index.rhtml:

<%= link_to_remote "replace_html", :url => { :action => 'replace_html' } %> <%= link_to_remote "replace", :url => { :action => 'replace' } %>

And then we create our RJS file. First replace_html.rjs:

page[:my_div].replace_html "New Text"

And then replace.rjs:

page[:my_div].replace "New Text"

Try out the replace_html link, and you'll see that the contents of the DIV are replaced with the new text, but the DIV itself remains untouched. Try out replace, and you'll see the whole DIV disappear and be replaced by plain text.

Table 5-1 illustrates the effects of the replace and replace_html methods.

Table 5-1. The effects of the replace and replace_html methods
 replace_htmlreplace
Original
<body>   <div >     DIV   </div> </body>

<body>   <div >     DIV   </div> </body>

RJS
page[:my_div].replace_html    "New Text"

page[:my_div].replace    "New Text"

Result
<body>   <div >     New Text   </div> </body>

<body>   New Text </body>


Note that after calling replace on the element proxy, the DIV itself is goneso calling the RJS a second time would fail, because it has nothing to replace.

Instead of passing a string argument to replace_html and replace as we've been doing, we can pass a hash, which will be interpreted as options to render a partial (Rails partials were introduced in Chapter 2). For example:

page[:my_div].replace_html :partial => "my_div" page[:my_div].replace :partial => "my_div"

To see it in action, create the partial in app/views/chapter5/_my_div.rhtml:

<div  >DIV (partial)</div>

Options for rendering partials (such as :locals and :collection) can be provided as well; for example:

page[@scott.id].replace :partial => "person",                         :locals => { :person => @scott } page[:people].replace_html :partial => "people",                            :collection => @people

5.2.4.2.2. reload

In the last example, notice that the ID of the element (my_div) is the same as the name of the partialit doesn't have to be that way, but it affords a nice opportunity to apply the DRY principle. RJS helps out with the reload method. It works just like replace, but it automatically renders the partial of the same name. For example:

page[:my_div].reload

That line is equivalent to this:

page[:my_div].replace :partial => "my_div"

Just like rendering partials with replace and replace_html, reload can be given options for rendering the partial. For example:

page[:person].reload :locals => { :person => @scott } page[:people].reload :collection => @people

Knowing that the reload method is available, it's a good idea to correlate the names of your partials with their DIVspaving the way for incredibly succinct and readable RJS.

5.2.5. Collection Proxies

There is another powerful method way to work with the DOM in RJS: using collection proxies. A collection proxy acts like an array of element proxies, and it brings all the power of Ruby's Enumerable module to RJS. The cornerstone of collection proxies in RJS is the select method, which corresponds to the "double-dollar" method ($$( )) in Prototype. The $( ) method is used to find a collection of elements according to a CSS selector rulethe same strings you use in CSS files to isolate a particular element or group of elements.

CSS selectors can be based on tag name, ID, class, even element attributes. For example, the CSS selector div references every DIV in a page; the selector p.welcome span represents every span within a paragraph with the class welcome; the selector ol#todo li.active represents the list items with the class active that descend from the ordered list identified by todo. For more information about Prototype's $$( ) method, see Chapter 11.


To create a collection proxy from RJS, use the select method. For example, to create a collection proxy representing all DIVs on the page:

page.select('div')

Collection proxies act like a Ruby array, so all of the usual Array methods are available. For example, to find the first span that descends from a paragraph with the class welcome, you'd use this:

page.select('p.welcome span').first

The members of the collection are element proxies, so they support all of the features discussed in the previous section. For example, to hide the last item in the ordered list with the ID todo:

page.select('ol#todo li').last.hide

The members of a collection proxy are also element proxies, so all of the methods discussed in the previous section apply (e.g., hide).

5.2.5.1. each

Ruby's Enumerable methods can be used with collection proxies as well, and they'll generate equivalent JavaScript code. Here's an example of the most common Enumerable method, each:

page.select('#todo li').each do |item|   item.visual_effect :highlight end

This code selects all list items that descend from the element identified as items, and then iterates through the elements, creating a visual effect for each one. The generated JavaScript will use Prototype's each method, like this:

$$("#todo li").each(function(value, index) {   value.visualEffect("highlight"); });

5.2.5.2. invoke

The invoke method takes the name of a method and calls that method for every member of the collection. For example:

page.select('#todo li').invoke('upcase')

In this case, we're selecting a group of list items and invoking their upcase( ) method (the extension to Prototype's Element we defined earlier in this chapter).

5.2.5.3. pluck

The pluck method is similar to invoke, except that it retrieves a property instead of invoking a function. The property is plucked from each element and stored in a JavaScript variable named according to the first argument.

page.select('#todo li').pluck('results', 'innerHTML') page << "alert(results)"

5.2.5.4. collect/map

The collect method (and its synonym, map) evaluates a block for each member of the collection and to store the result of each block in a new array. The name of the new array is given as the first argument to collect. For example:

page.select('#todo li').collect('results') do |el|   el.has_class_name 'foo' end page << "alert(results)"

This code iterates through the specified list items, and evaluate the block for each memberin this case, testing whether the element has a certain class. The result (an array of true/false values) will be stored in a JavaScript object named results. The last line creates an alert box to show the values.

5.2.5.5. detect/find

The detect method (and its synonym, find) is used to find the first member of the collection for which the block is true and store it in a JavaScript object. For example:

page.select('#todo li').detect('result') do |el|   el.has_class_name 'foo' end page.call 'result.upcase'

This code iterates through the set of DOM elements until the block evaluates to true, i.e., until the first element with the class foo is found. The element is then stored in result, and the last line calls upcase( ) on it.

5.2.5.6. select/find_all

Not to be confused with the select method of JavaScriptGenerator, the select method on collection proxies (and its synonym, find_all) finds all the members of the collection for which the block is true and store them in a JavaScript object. For example:

page.select('#todo li').select('results') do |el|   el.has_class_name 'foo' end page << "results.invoke('upcase')"

This code iterates through the set of DOM elements and adds each element to the results array if the block evaluates to true, i.e., if the element has the class foo. The last line calls upcase( ) on each element of the results array.

5.2.5.7. reject

The reject method is the opposite of selectit's used to find all the members of the collection for which the block is false and store then in a JavaScript object. For example:

page.select('#todo li').reject('results') do |el|   el.has_class_name 'foo' end page << "results.invoke('upcase')"

This code iterates through the set of DOM elements and adds each element to the results array if the block evaluates to false, i.e., if the element doesn't have the class foo. The last line calls upcase( ) on each element of the results array.

5.2.5.8. partition

The partition method divides a collection in two, split according to the results of the block.

page.select('#todo li').partition('results') do |el|   el.has_class_name 'foo' end page << "results[0].invoke('upcase')"

In this example, each element will be tested for the class foo; those that have it will be placed in results[0]; those that don't will be in results[1].

5.2.5.9. min and max

These methods evaluate the block for each member of the collection and store the largest or smallest result in a JavaScript variable. For example:

page.select('#todo li').max('max') { |el| el.length } page.select('#todo li').min('min') {|el| el.length } page << "alert(max)" page << "alert(min)"

This example depends on a custom extension to Prototype's Element object:

length: function(element) { return element.innerHTML.length; }

The RJS example determines the length of innerHTML for each element and stores the largest or smallest result in the max or min variable.

5.2.5.10. all and any

The all and any methods evaluate the block for each member of the collection, and store whether all iterations returned true, or any iteration returned true, respectively. For example:

page.select('#todo li').all('all') { |el| el.has_class_name 'foo' } page.select('#todo li').any('any') { |el| el.has_class_name 'foo' } page << "alert(all)" page << "alert(any)"

This code will iterate through the collection and test each element for the class foo. If the block is true for every element, the JavaScript variable all will be true. If the block is true for any element, the JavaScript variable result will be true.

5.2.5.11. inject

The inject method combines all the members of the collection according to the iterator. The iterator is passed the result of the previous iteration (or in the case of the first iteration, the second argument of inject). The result is stored in a JavaScript variable.

page.select('#todo li').inject('result', '') do |memo, value|   page << 'memo + value.innerHTML' end page << "alert(result)"

In this example, the innerHTML of all the elements will be appended together and put into the variable result.

5.2.5.12. zip

The zip method merges the elements of the collection with one or more arrays. The result is an array of arrays, stored in a JavaScript variable. For example:

page.select('#todo li').zip('results', ['a','b','c','d']) page << "alert(results.inspect(  ))"

This code will result in the JavaScript array results having four elements, each one a subarray with two elements: a DOM object and a string.

The zip method can also take a block, which can be used to alter the members of the new collection. For example:

page.select('#todo li').zip('results', ['a','b','c','d']) do |array|   page.call 'array.reverse' end page << "alert(results.inspect(  ))"

This code works the same as before, except that each subarray will be in the reverse order: first a string, then a DOM element.

5.2.5.13. sort_by

The sort_by method evaluates a block for each member of the collection, sorts each element by the result of the block, and stores the sorted collection in a JavaScript variable. For example:

page.select('#todo li').sort_by('results') { |el| el.length } page << "alert(results)"

5.2.6. JavaScriptGenerator Methods

JavaScriptGenerator methods are those available on the page object. We've already seen a few examples, like alert and select. Here we'll explore the rest.

5.2.6.1. Manipulating DOM elements

The standard way of manipulating DOM elements (e.g., hiding, showing, etc.) is with element proxies. Instead of using the element proxy syntax, you can also call these methods directly on page. The advantage over element proxies is that you can affect multiple elements at once. For example:

page.hide :my_div, :text_div page.show :my_div, :text_div page.toggle :my_div, :text_div page.remove :my_div, :text_div

5.2.6.2. Inserting content

While element proxies support replace and replace_html for changing element content, they lack a way to insert content into an element. To accomplish that, we can use insert_html. For example:

page.insert_html :bottom, :my_div, 'New Text'

Click the new link a few times and you'll see the result: with each call, an additional piece of content is added to the element. The available insertion positions are :before, :top, :bottom, and :after; they are examined in detail in Chapter 10.

5.2.6.3. Redirecting

One frustration of Ajax is that the XMLHttpRequest object doesn't respond to HTTP redirects, so using the standard redirect_to controller method will have no effect on Ajax requests. With RJS, there's a workaround: JavaScriptGenerator's redirect_to simulates a redirect using window.location.hrefJavaScript's method for changing the current URL. For example:

page.redirect_to url_for(:action => 'index') page.redirect_to some_url

This is especially useful when handling form submissions. If the submitted data is invalid, you can add an error message to the formbut if it the data is accepted, you can redirect the user to a new page.

5.2.6.4. Delaying execution

delay wraps code in a JavaScript timeout. The argument should be the delay time in seconds, followed by a block of code to be executed after the delay. For example:

page.delay(5) { page[:my_div].visual_effect :fade }

Note that the only the code in the block will have its execution delayedanything that comes after the delay statement won't be affected. Take this example:

page.delay(5) { page.alert 'Delayed alert' } page.alert 'Alert after delay statement'

The first alert to appear will be "Alert after delay statement," and the alert written on the line above it will be triggered five seconds later.

5.2.6.5. Creating drag-and-drop elements

RJS provides three convenient methods for creating script.aculo.us drag-and-drop elements: draggable, drop_receiving, and sortable. They are used like this:

page.draggable :my_div page.drop_receiving :wastebasket, :url => { :action => 'delete' } page.sortable :todo, :url => { :action => 'change_order' }

This RJS does three things: makes my_div draggable, makes wastebasket droppable, and triggers an Ajax call when something is dropped on it; makes the todo list sortable, and specifies the Ajax target to call when it's rearranged.

The drag-and-drop features of script.aculo.us are introduced in Chapter 4 and detailed in Chapter 11.

5.2.6.6. Generating arbitrary JavaScript

RJS's <<, assign, and call methods enable easy interaction with existing JavaScript code.

Although RJS methods are powerful, there are times when it's easier to simply write a custom JavaScript statement or two. The << method allows that:

page << "alert('Hello from <<!')"

The given snippet will be sent along with the rest of the generated JavaScript to the browser.

The assign method is used to assign a value to a JavaScript variable. For example:

page.assign :greeting, "Hello from assign!" page << "alert(greeting)"

The call method is used to call an arbitrary JavaScript methodsuch as one you define yourself. The first argument is the name of the method, and the rest of the arguments are passed as parameters. For example:

page.call :alert, "Hello from call!"

5.2.6.7. Class proxies

Any method called on page that's not defined elsewhere will become a class proxy. Like element proxies and collection proxies explored earlier, class proxies represent client-side objects: JavaScript classes. Class proxies can be used call static methods on JavaScript classes. Prototype, for example, defines a number of convenient methods for working with forms, like Form.reset(element) and Field.focus(element). To use those methods from RJS, you'd use a class proxy:

page.form.reset :my_form page.field.focus :my_field

Class proxies are commonly used to call methods on custom, application-specific classes. For example, the Review Quiz example application defines a JavaScript method in application.js like this:

var Quiz = {   updateHints: function(  ) {     // ...   } }

That method is then called from RJS (in create_q.rjs), using a class proxy, like this:

page.quiz.update_hints

This facility to call application-specific JavaScript libraries makes your RJS statements feel perfectly tailored to your application. If RJS is a DSL for generating JavaScript, class proxies allow it to become a DSL for your exact application.

5.2.7. RJS Helpers

If you find common bits of RJS that you're repeating multiple places in your application, it's probably a good idea to DRY things up with helpers, just like you would with common pieces of .rhtml templates. RJS helpers go right in the same files as view helpers. For example, add the following method to app/helpers/application_helper.rb:

def my_helper   page.alert "Hello from a helper" end

Then, back in the controller, your RJS can call the helper like so:

page.my_helper

If a helper name conflicts with one of the standard JavaScriptGenerator methods, it won't be mixed in.

5.2.8. RJS Without Ajax

As you know, RJS was designed with Ajax in mind, particularly for returning JavaScript to Ajax requests. But you might be surprised that it can also be used outside of the context of remote Ajax requests, for example, generating JavaScript to be used with link_to_function. The helper takes a block, which is passed an instance of JavaScriptGeneratoralso known as our familiar page object. Here's how it looks:

<%= link_to_function "update_page" do |page|   page.alert "Hello from update_page" end %>

Granted, this example isn't too persuasiveit would be less work to just enter a JavaScript statement by hand. But for more complicated scripts, the RJS syntax can be much more readable than its JavaScript equivalent. Here's a more complicated example of using RJS with link_to_function:

<%= link_to_function "Show content", nil, :id => "more_link" do |page|   page[:more_link].toggle_class_name "yellow"   page[:content].toggle end %>

Using RJS with link_to_function can help keep your code DRY, because RJS helpers are available inside the block as well:

<%= link_to_function "update_page w/ helper" do |page|   page.my_helper  end %>

Keep in mind, however, that the helper is rendered into JavaScript when the page is created, so it's not able to update the page with new content from the server, the way an Ajax call could.

To generate JavaScript with RJS in other contexts, use update_page. The update_page helper returns JavaScript, so it can be used with other Rails helpers anywhere JavaScript is expected. For example, you might define an RJS helper to handle failures on Ajax requests, and then use update_page to call it when needed:

<%= link_to_remote "Check Time",     :update  => 'current_time',     :url     => { :action => 'get_time' },     :failure => update_page { |page| page.handle_failure } %>

A companion helper, update_page_tag, works just like update_page but wraps the generated JavaScript in <script> tags. For example, this helper will output the rendered result of an RJS helper in a <script> tag so that the browser executes it as soon as it's loaded:

<%= update_page_tag { |page| page.my_helper } %>




Ajax on Rails
Ajax on Rails
ISBN: 0596527446
EAN: 2147483647
Year: 2006
Pages: 103
Authors: Scott Raymond

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net