Thursday 12 June 2008

Rails: Refreshing Multiple Partials in a Page

So you have a page with a load of partials in it, all of which have forms that update their own partials by AJAX calls (form_for_remote, submit_to_remote etc.) - no worries so far. But then what if you update a field in one partial that has a knock on effect to other partials e.g. a price in an order booking partial could change the total price in an order summary partial. It seems like a fairly normal thing to want to do but ActionView's support for this sort of stuff is rubbish!

An ActionView controller will automatically render a partial with the same name as itself unless it is given an alternative render command. The problem is that ActionView will not allow more than one render per method.

Using JSP's 'page.replace_html' call it is possible to do multiple 'innerHTML' type javascript replacements. However you first must render the partials to strings using the 'render_to_string' method as shown:


charges_partial = render_to_string(:partial => 'transactions/charges_and_payments', :object => @transaction)
order_partial = render_to_string(:partial => 'transactions/order_totals', :object => transaction)

render :update do |page|
page.replace_html 'charges_and_payments', charges_partial
page.replace_html 'order_totals', order_partial
end


You cannot call the render_to_string method from within the render block as they lose their scope and you get 'method not found' errors.

Remember though that including this method will mean that your default partial (the one with the same name as the controller method) will not render, you will have to add it manually to the call above.

But what if you have a set of partials that will need to be updated by several different calls - you don't want to repeat the same code in every method but you also can't do a redirect to a master function (as they aren't allowed after a render) and you can't include separate renderer method as ActionView counts that as being multiple renderers in the one call which it doesn't allow. Bugger.

Here's the way that I got round it.

First I made a master 'refresh partials' method:


class TransactionsController < transaction =" Transaction.find_by_id(params[:id])" pending_partial =" render_to_string(:partial"> 'transactions/pending_order', :object => @transaction)
order_partial = render_to_string(:partial => 'transactions/order_totals', :object => @transaction)
charges_partial = render_to_string(:partial => 'transactions/charges_and_payments', :object => @transaction)
comments_partial = render_to_string(:partial => 'transactions/comments', :object => @transaction)

render :update do |page|
page.replace_html 'pending_order', pending_partial
page.replace_html 'order_view_totals', order_partial
page.replace_html 'charges_and_payments', charges_partial
page.replace_html 'transaction_comments', comments_partial
end
end

end


Then (remember that I want these partials to be refreshed when a form in another is submitted) I added the following code to the partials that were being directly updated by their own forms. This will now call the refresh partials function whenever the parent partial is refreshed and the 'do_not_refresh_totals' variable is not set.


<% if not defined?(do_not_refresh_totals) or do_not_refresh_totals = true %>
<script type="text/javascript">
<%= remote_function(:url => { :controller => 'transactions', :action => 'refresh_partials', :id => transaction.id }) %>
</script>
<% end %>


The point of the 'do_not_refresh_totals' variable is that when the page is loaded initially I don't want a load of refresh calls to be immediately fired off - there would be no point as no data is being submitted so no changes are being made. So in the parent view where the partials are being initially rendered from you simply need something like the following:


<%= render :partial => "flight_booking", :locals => { :flight_booking => flight_booking, :do_not_refresh_totals => true } %>


Enjoy!

Note: Actually I've found a more intuitive way of doing this using separate RJS files. For any method in any controller, if you put an RJS file with the name of that method in that controller's view folder then its code will be executed immediately after the controller method has run and the code will have access to any instance variables you created in the that method. Put your 'replace_html' syntax in your RJS file and you're good to go. Easy!
This however does totally spam your view folders with RJS files...

No comments: