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

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