RJS Templates and the Replace semantics

I have an issue with the way replace_html works in an RJS template.
This is a copy of a post on my blog (http://blog.craz8.com
http://blog.craz8.com/ ) that describes the problem and my working
solution to the problem.

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

<% @things.each do |thing| %> <%= render :partial => 'thing' %> <% end %>

Or

<%= render :partial => 'thing', :collection => @things %>

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

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:

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:

page.remove “thing-id”
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:

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):

Update the JavaScriptGenerator to add our own functionality

module ActionView
module Helpers
module PrototypeHelper
class JavaScriptGenerator
def replace_html_element(id, *options_for_render)
html = render(*options_for_render)
record “Element.replace(#{id.inspect}, #{html.inspect})”
end
end
end
end
end

Add this code to your application javascript file:

// Extend the object for our RJS extension to work
Object.extend(Element, {
replace: function(element, html) {
var el = $(element);
if (el.outerHTML) { // IE
el.outerHTML = html.stripScripts();
} else { // Mozilla
var range = el.ownerDocument.createRange();
range.selectNodeContents(el);

el.parentNode.replaceChild(range.createContextualFragment(html.stripScri
pts()), el);
}
setTimeout(function() {html.evalScripts()}, 10);
}
}
);