- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •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 A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
RJS TEMPLATES 558
a toggle effect, the generated JavaScript will take care of alternating between the states. The available togglers are
toggle_appear: toggle_slide: toggle_blind:
toggles using appear and fade toggles using slide_down and slide_up toggles using blind_down and blind_up
You can use the visual_effect helper pretty much anywhere you could provide a snippet of JavaScript.
23.3RJS Templates
So far we’ve covered Prototype and Script.aculo.us almost strictly from the point of view of returning HTML from the server during XHR calls. This HTML is almost always used to update the innerHTML property of some DOM element in order to change the state of the page. It turns out that there is another powerful technique you can use that can often solve problems that otherwise require a great deal of complex JavaScript on the client: your XHR calls can return JavaScript to execute in the browser.
In fact, this pattern became so prevalent in 2005 that the Rails team came up with a way to codify it on the server the same way they use .rhtml files to deal with HTML output. That technique was called RJS templates. As people began to use the RJS templates, though, they realized that they wanted to have the same abilities that the templates provided but be able to do it inline within a controller. Thus was born the render :update construct.
What is an RJS template? It is simply a file, stored in the app/views hierarchy, with an .rjs extension. It contains commands that emit JavaScript to the browser for execution. The template itself is resolved the same way that .rhtml templates are: when an action request is received, the dispatcher tries to find a matching .rhtml template. If the request came in from XHR, the dispatcher will preferentially look for an .rjs template. The template is parsed, JavaScript is generated and returned to the browser, where it is finally executed.
RJS templates can be used to provide standard interactive behavior across multiple pages or to minimize the amount of custom JavaScript code embedded on a given page. One of the primary usage patterns of RJS is to cause multiple client-side effects to occur as the result of a single action.
Let’s go back and revisit the drag-and-drop example from earlier. When the user drags a to-do item from one list to the other, that item’s id is sent to the server. The server has to recategorize that particular item by removing it from its original list and adding it to the new list. That means the server must then update both lists back on the view. However, the server can return only one response as a result of a given request.
Report erratum
RJS TEMPLATES 559
This means that you could
•Structure the page so that both drop targets are contained in a larger element, and update the entirety of that parent element on update
•Return structure data to a complex client-side JavaScript function that parses the data and divvies it up amongst the two drop targets
•Use RJS to execute several JavaScript calls on the client, one to update each drop target and then one to reset the sortability of the new lists
Here is the server-side code for the todo_pending and todo_completed methods on the server. When the user completes an item, it has a completed date assigned to it. When the user moves it back out of completed, the completed date is set to nil.
Download pragforms/app/controllers/user_controller.rb
def todo_completed update_todo_completed_date Time.now
end
def todo_pending update_todo_completed_date nil
end
private
def update_todo_completed_date(newval) @user = User.find(params[:id])
@todo = @user.todos.find(params[:todo]) @todo.completed = newval
@todo.save!
@completed_todos = @user.completed_todos @pending_todos = @user.pending_todos render :update do |page|
page.replace_html 'pending_todos' , :partial => 'pending_todos' page.replace_html 'completed_todos' , :partial => 'completed_todos' page.sortable "pending_todo_list" ,
:url=>{:action=>:sort_pending_todos, :id=>@user}
end end
After performing the standard CRUD operations that most controllers contain, you can see the new render :update do |page| section. When you call render :update, it generates an instance of JavaScriptGenerator, which is used to create the code you’ll send back to the browser. You pass in a block, which uses the generator to do the work.
In our case, we are making three calls to the generator: two to update the drop target lists on the page and one to reset the sortability of the pending todos. We have to perform the last step because when we overwrite the original version,
Report erratum
RJS TEMPLATES 560
any behavior bound to it disappears, and we have to re-create it if we want the updated version to act the same way.
The calls to page.replace_html take two parameters: the id (or an array of ids) of elements to update and a hash of options that define what to render. That second hash of options can be anything you can pass in a normal render call. Here, we are rendering partials.
The call to page.sortable also takes the id of the element to make sortable, followed by all of the possible options to the original sortable_element helper.
Here is the resulting response from the server as passed back across to the browser (reformatted slightly to make it fit).
try {
Element.update("pending_todos", "<ul id='pending_todo_list'> <li class=\"pending_todo\" id='todo_38'>Build a house</li>
<script type=\"text/javascript\">\n//<![CDATA[\nnew Draggable(\"todo_38\", {ghosting:true, revert:true})\n//\n</script>
<li class=\"pending_todo\" id='todo_39'>Read the Hugo Award Winners</li> <script type=\"text/javascript\">\n//<![CDATA[\nnew Draggable(\"todo_39\",
{ghosting:true, revert:true})\n//]]>\n</script>\n |
\n</ul>\n"); |
// . . . |
|
Sortable.create(\"pending_todo_list\" ,
{onUpdate:function(){new AJAX.Request(\'/user/sort_pending_todos/10\' , {asynchronous:true, evalScripts:true,
parameters:Sortable.serialize(\"pending_todo_list\" )})}});'); throw e }
]]>
The response is pure JavaScript; the Prototype helper methods on the client must be set to execute JavaScripts, or nothing will happen on the client. It updates the drop targets with new HTML, which was rendered back on the server into string format. It then creates the new sortable element on top of the pending to-dos. The code is wrapped in a try/catch block. If something goes wrong on the client, a JavaScript alert box will pop up and attempt to describe the problem.
If you don’t like the inline style of render :update, you can use the original version, an .rjs template. If you switch to the template style, the action code would reduce to
def update_todo_completed_date(newval) @user = User.find(params[:id])
@todo = @user.todos.find(params[:todo]) @todo.completed = newval
@todo.save!
@completed_todos = @user.completed_todos @pending_todos = @user.pending_todos
end
Then, add a file called todo_completed.rjs in app/views/user/ that contains
Report erratum
RJS TEMPLATES 561
page.replace_html 'pending_todos' , :partial => 'pending_todos' page.replace_html 'completed_todos' , :partial => 'completed_todos' page.sortable "pending_todo_list" ,
:url=>{:action=>:sort_pending_todos, :id=>@user}
Rails will autodiscover the file, create an instance of JavaScriptGenerator called page, and pass it in. The results will be rendered back to the client, just as with the inline version.
Let’s take a categorized look at the available RJS helper methods.
Editing Data
You might have several elements on a page whose data needs to be updated as a result of an XHR call. If you need to replace only the data inside the element, you will use replace_html. If you need to replace the entire element, including its tag, you need replace.
Both methods take an id and a hash of options. Those options are the same as you would use in any normal render call to render text back to the client. However, replace_html merely sets the innerHTML of the specified element to the rendered text, while replace first deletes the original element and then inserts the rendered text in its place.
In this example, our controller mixes using RJS to update the page upon successful edit or redraws the form with a standard render if not.
def edit_user
@user = User.find(params[:id])
if @user.update_attributes(params[:user]) render :update do |page|
page.replace_html "user_#{@user.id}", :partial => "_user" end
else
render :action => 'edit' end
end
Inserting Data
Use the insert_html method to insert data. This method takes three parameters: the position of the insert, the id of a target element, and the options for rendering the text to be inserted. The position parameter can be any of the positional options accepted by the update Prototype helper (:before, :top, :bottom, and
:after).
Here is an example of adding an item to a todo list. The form might look like
<ul id="todo_list" >
<% for item in @todos %> <li><%= item.name %></li>
<% end %>
</ul>
Report erratum
RJS TEMPLATES 562
<% form_remote_tag :url => {:action => 'add_todo'} do %> <%= text_field 'todo', 'name' %>
<%= submit_tag 'Add...' %> <% end %>
On the server, you would store the to-do item and then add the new value into the existing list at the bottom.
def add_todo
todo = Todo.new(params[:todo]) if todo.save
render :update do |page|
page.insert_html :bottom, 'todo_list', "<li>#{todo.name}</li>" end
end end
Showing/Hiding Data
You’ll often need to toggle the visibility of DOM elements after the completion of an XHR call. Showing and hiding progress indicators are a good example; toggling between an Edit button and a Save button is another. There are three major methods you can use to handle these states: show, hide, and toggle. Each takes a single id or an array of ids to modify.
For example, when using AJAX calls instead of standard HTML requests, the standard Rails pattern of assigning a value to flash[:notice] doesn’t do anything because the code to display the flash is executed only the first time the page is rendered. Instead, you can use RJS to show and hide the notification.
def add_todo
todo = Todo.new(params[:todo]) if todo.save
render :update do |page| page.insert_html :bottom, 'todo_list',
"<li>#{todo.name}</li>"
page.replace_html 'flash_notice', "Todo added: #{todo.name}" page.show 'flash_notice'
end end
end
Alternatively, you can choose to delete an element from the page entirely by calling remove. Successful execution of remove means that the node or nodes specified will be removed from the page entirely. This does not mean just hidden; the element is removed from the DOM and cannot be retrieved.
Here’s an example of our to-do list again, but now the individual items have an id and a Delete button. Delete will make an XHR call to remove the item from the database, and the controller will respond by issuing a call to delete the individual list item.
Report erratum
RJS TEMPLATES 563
<ul id="todo_list" >
<% for item in @todos %>
<li id='todo_<%= item.id %>'><%= item.name %> <%= link_to_remote 'Delete',
:url => {:action => 'delete_todo', :id => item} %>
</li>
<% end %>
</ul>
<% form_remote_tag :url => {:action => 'add_todo'} do %> <%= text_field 'todo', 'name' %>
<%= submit_tag 'Add...' %> <% end %>
def delete_todo
if Todo.destroy(params[:id]) render :update do |page|
page.remove "todo_#{params[:id]}" end
end end
Selecting Elements
If you need to access page elements directly, you can select one or more of them to call methods on. The simplest method is to look them up by id. You can use the [ ] syntax to do that; it takes a single id and returns a proxy to the underlying element. You can then call any method that exists on the returned instance. This is functionally equivalent to using the Prototype $ method in the client.
In conjunction with the fact that the newest versions of Prototype allow you to chain almost any call to an object, the [ ] syntax turns out to be a very powerful way to interact with the elements on a page. Here’s an alternate way to show the flash notification upon successfully adding a to-do item.
def add_todo
todo = Todo.new(params[:todo]) if todo.save
render :update do |page|
page.insert_html :bottom, 'todo_list', "<li>#{todo.name}</li>" page['flash_notice' ].update("Added todo: #{todo.name}").show
end end
end
Another option is to select all the elements that utilize some CSS class(es). Pass one or more CSS classes into select; all DOM elements that have one or more of the classes in the class list will be returned in an array. You can then manipulate the array directly or pass in a block that will handle the iteration for you.
Report erratum
RJS TEMPLATES 564
Direct JavaScript Interaction
If you need to render raw JavaScript that you create, instead of using the helper syntax described here, you can do that with the << method. This simply appends whatever value you give it to the response; it will be evaluated immediately along with the rest of the response. If the string you provide is not executable JavaScript, the user will get the RJS error dialog box.
render :update do |page|
page << "cur_todo = #{todo.id};" page << "show_todo(#{todo.id});"
end
If, instead of rendering raw JavaScript, you need to call an existing JavaScript function, use the call method. call takes the name of a JavaScript function (that must already exist in page scope in the browser) and an optional array of arguments to pass to it. The function call will be executed as the response is parsed. Likewise, if you just need to assign a value to a variable, use assign, which takes the name of the variable and the value to assign to it.
render :update do |page| page.assign 'cur_todo', todo.id page.call 'show_todo', todo.id
end
There is a special shortcut version of call for one of the most common cases, calling the JavaScript alert function. Using the RJS alert method, you pass a message that will be immediately rendered in the (always annoying) JavaScript alert dialog. There is a similar shortcut version of assign called redirect_to. This method takes a URL and merely assigns it to the standard property window.location.href.
Finally, you can create a timer in the browser to pause or delay the execution of any script you send. Using the delay method, you pass in a number of seconds to pause and a block to execute. The rendered JavaScript will create a timer to wait that many seconds before executing a function wrapped around the block you passed in. In this example, we will show the notification of an added to-do item, wait three seconds, and then remove the message from the <div> and hide it.
def add_todo
todo = Todo.new(params[:todo]) if todo.save
render :update do |page| page.insert_html :bottom, 'todo_list',
"<li>#{todo.name}</li>"
page.replace_html 'flash_notice', "Todo added: #{todo.name}" page.show 'flash_notice'
page.delay(3) do
page.replace_html 'flash_notice', '' page.hide 'flash_notice'
Report erratum