Rails RJS Templates need better replace semantics

December 15, 2005 Link to post  Permalink

<!--adsense-->

If I have a collection of things that are output like this:

1div id="things">
2<% @things.each do |thing| %>
3  <%= render :partial => 'thing' %>
4<% end %>
5</div>

or

1div id="things">
2<%= render :partial => 'thing', :collection => @things %>
3</div>

I can use AJAX to insert a new ‘thing’ by implementing an RJS template that does this, reusing the same partial layout:

1 page.insert_html :bottom, :partial => 'thing'

but I can’t then replace the inserted thing by doing this, as this is implemented as element.innerHTML:

1 page.replace_html "thing-id", :partial => 'thing'

There are ways around this, but they involve moving the outer element of the thing out of the partial code, splitting the HTML across two files, or removing and re-adding the element:

1 page.remove "thing-id"
2  page.insert_html :bottom, :partial => 'thing'

I don’t like either of these ideas, so I came up with an improvement that replaces the entire element in the DOM, similar to IE’s element.outerHTML:

1 page.replace_html_element "thing-id", :partial => 'thing'

So, I’ve written an implementation of replace_html_element that comes in two parts:

  • The addition to the JavaScriptGenerator class to add a replace_html_element method
  • An update to the Prototype Element implementation to perform the client side update.

Add this code to the Application.rb (or in a separate file required by Application.rb):

 1 Update the JavaScriptGenerator to add our own functionality
 2module ActionView
 3  module Helpers
 4    module PrototypeHelper
 5      class JavaScriptGenerator
 6        def replace_html_element(id, *options_for_render)
 7          html = render(*options_for_render)
 8          record "Element.replace(#{id.inspect}, #{html.inspect})"
 9        end
10      end
11    end
12  end
13end  

 1 Update the JavaScriptGenerator to add our own functionality
2module ActionView
3 module Helpers
4 module PrototypeHelper
5 class JavaScriptGenerator
6 def replace_html_element(id, options_for_render)
7 html = render(
options_for_render)
8 record "Element.replace(#{id.inspect}, #{html.inspect})"
9 end
10 end
11 end
12 end
13end

Add this code to your application javascript file:

 1/ Extend the object for our RJS extension to work
 2Object.extend(Element, {
 3  replace: function(element, html) {
 4          var el = $(element);
 5          if (el.outerHTML) { // IE
 6            el.outerHTML = html.stripScripts();
 7    } else {  // Mozilla
 8              var range = el.ownerDocument.createRange();
 9            range.selectNodeContents(el);
10            el.parentNode.replaceChild(range.createContextualFragment(html.stripScripts()), el);
11    }
12    setTimeout(function() {html.evalScripts()}, 10);
13  }
14 }
15);