Gabriel Dehan - Life is a Design Pattern Archive Pages Categories Tags

{ Rails } Render multiple tags in a helper

07 August 2012

Well, I came across this issue lately : I had this helper render_each_microposts which was supposed to call a partial onto each record from my Micropost model.

First thoughts

Of course, the first thing I tried was to do something like :

def render_each_microposts
  @microposts.collect do |m|
    render(partial: 'microposts/single', locals: { micropost : m }
  end.join
end

So. What does it do ?
Obviously enough, we have a @microposts instance variable, that contains all the microposts we want to display. For it to be an Array or an ActiveRecord Scoped object, we don’t care, as long as it respond to #collect (alias for #map, just made more sense here.) and #join.
With this @microposts variable, we go through each and every micropost and render them one by one. We assume that our partial looks like :

<li>
  <h2>micropost.user.name</h2>
  <p>micropost.content</p>
</li>

Thus, once #collect has been executed, we have a beatiful array of rendered Microposts, looking probably like :

# Regular stuff, you see ?
[
  "<li><h2>Gabriel Dehan</h2><p>I'am a Stegosaurus !</p></li>",
  "<li><h2>Pinky Pie</h2><p>Hmmmmmm. Nah.</p></li>"
]

We joined it because well, helper methods should return Strings, not Arrays.
And… of course, it fails.

We have a problem : Our page just rendered our HTML as an escaped string.

This is a normal behavior, most of us already encountered. A common workaround to handle two rendered item in a helper would be to concatenate them :

def my_helper
  content_tag(:h1, 'Hello World') + content_tag(:h2, 'Oh haiz !')
end

# Or with partials which we assume does almost the same.
def my_helper
  # Note that we need to add the parenthesis for the renders to work,
  # render is a method, + is a method.
  # When methods are chained you need not to forget parenthesis.
  render(partial: 'my_view/first_heading') + render(partial: 'my_view/second_heading')
end

This works because we are, in fact, concatanating not two strings, but two ActiveSupport::SafeBuffer(s), as we can see :

my_helper.class
# => ActiveSupport::SafeBuffer

ActiveSupport::SafeBuffer

What is it ?

SafeBuffer, is a part of ActiveSupport Core Extensions, and it’s source code can be found in active_support/core_ext/string/output_safety.rb.

module ActiveSupport
  class SafeBuffer < String
    # ...
  end
end

SafeBuffer is a subclass of string and allows us to create HTML safe strings (Strings that will be considered safe no matter whether they have been escaped or not.)

So, when we wrote down our method using #+, it was (obviously) an alias for ActiveSupport::SafeBuffer#concat, not string. We could, of course, use #concat or #+ and a loop to do our job, but it would be ugly and slow.

What do we do, now ?

Now we have better knowledge of ActiveSupport::SafeBuffer, we can now do :

def render_each_microposts
  ActiveSupport::SafeBuffer.new(@microposts.collect do |m|
    render(partial: 'microposts/single', locals: { micropost : m }
  end.join)
end

Or even better, as ActiveSupport::SafeBuffer extends String with an #html_safe method :

def render_each_microposts
  @microposts.collect do |m|
    render(partial: 'microposts/single', locals: { micropost : m }
  end.join.html_safe
end

Which does exactly the same if we look into the source code :

class String
  def html_safe
    ActiveSupport::SafeBuffer.new(self)
  end
end

And now it works, as simple as that.

blog comments powered by Disqus
Fork me on GitHub