Loop & output content_tags within content_tag in helper

20,309

Solution 1

Try this:

def foo_list items
  content_tag :ul do
      items.collect {|item| concat(content_tag(:li, item))}
  end
end

Solution 2

I couldn't get that work any better.

If you were using HAML already, you could write your helper like this:

def foo_list(items)
  haml_tag :ul do
    items.each do |item|
      haml_tag :li, item
    end
  end
end

Usage from view:

- foo_list(["item_one", link_to("item_two", "#"), ... ])

Output would be correctly intended.

Solution 3

You could use content_tag_for, which works with collections:

def foo_list(items)
  content_tag(:ul) { content_tag_for :li, items }
end

Update: In Rails 5 content_tag_for (and div_for) were moved into a separate gem. You have to install the record_tag_helper gem in order to use them.

Solution 4

Along with answers above, this worked for me well:

(1..14).to_a.each do |age|
  concat content_tag :li, "#{link_to age, '#'}".html_safe
end

Solution 5

The big issue is that content_tag isn't doing anything smart when it receives arrays, you need to send it already processed content. I've found that a good way to do this is to fold/reduce your array to concat it all together.

For example, your first and third example can use the following instead of your items.map/collect line:

items.reduce(''.html_safe) { |x, item| x << content_tag(:li, item) }

For reference, here is the definition of concat that you're running into when you execute this code (from actionpack/lib/action_view/helpers/tag_helper.rb).

def concat(value)
  if dirty? || value.html_safe?
    super(value)
  else
    super(ERB::Util.h(value))
  end
end
alias << concat
Share:
20,309
DEfusion
Author by

DEfusion

Awesome!

Updated on September 22, 2020

Comments

  • DEfusion
    DEfusion over 3 years

    I'm trying a helper method that will output a list of items, to be called like so:

    foo_list( ['item_one', link_to( 'item_two', '#' ) ... ] )
    

    I have written the helper like so after reading Using helpers in rails 3 to output html:

    def foo_list items
        content_tag :ul do
            items.collect {|item| content_tag(:li, item)}
        end
    end
    

    However I just get an empty UL in that case, if I do this as a test:

    def foo_list items
        content_tag :ul do
            content_tag(:li, 'foo')
        end
    end
    

    I get the UL & LI as expected.

    I've tried swapping it around a bit doing:

    def foo_list items
        contents = items.map {|item| content_tag(:li, item)}
        content_tag( :ul, contents )
    end
    

    In that case I get the whole list but the LI tags are html escaped (even though the strings are HTML safe). Doing content_tag(:ul, contents.join("\n").html_safe ) works but it feels wrong to me and I feel content_tag should work in block mode with a collection somehow.

  • DEfusion
    DEfusion over 13 years
    That works, I'd tried doing the concat around the collect, previously rather than within. With concat inside the proc then collect isn't needed and you can use a items.each (or other iterators).
  • Ivailo Bardarov
    Ivailo Bardarov about 13 years
    This way you are calling concat on each element. I think that it would be cheaper to do items.collect{}.join("").html_safe
  • nruth
    nruth about 8 years
    May need to change <%= to <% in the template when doing this.
  • hellion
    hellion over 7 years
    content_tag_for removed in Rails 5
  • Tallboy
    Tallboy about 6 years
    Can you explain how concat is working here? I thought concat has to be called on another object?
  • zetetic
    zetetic about 6 years
    @Tallboy concat is explained here: api.rubyonrails.org/classes/ActionView/Helpers/…. The method is being called on the view context (I think). You may be confusing it with String#concat
  • Tallboy
    Tallboy about 6 years
    Thank you, I couldnt find that method before. That explains it