I am trying to write a Rails helper method to convert a nested hash into a nested HTML list.
For example:
{
:parent => "foo",
:children => [
{
:parent => "bar",
:children => [
{
:parent => "baz",
:children => []
}
]
}
]
}
should become:
<ul>
<li>foo</li>
<ul>
<li>bar</li>
<ul>
<li>baz</li>
</ul>
</ul>
</ul>
The hash may have any number of levels, and any number of parents per level.
What is the best way to achieve this please?
You can make a recursive method to render to hash to a nested set of lists. Place this in your relevant helper:
def hash_list_tag(hash)
html = content_tag(:ul) {
ul_contents = ""
ul_contents << content_tag(:li, hash[:parent])
hash[:children].each do |child|
ul_contents << hash_list_tag(child)
end
ul_contents.html_safe
}.html_safe
end
Zach Kemp's answer very effectively addresses the question. If you are looking for something a bit more generic (a nested hash for which you will not know the key names), as I was, the following module may be helpful (also at https://github.com/sjohnson/auto_hash_display with more details):
module HashFormatHelper
# These methods add classes to the HTML structure that are defined in Bootstrap (and can be defined for other CSS frameworks)
def format_hash(hash, html = '')
hash.each do |key, value|
next if value.blank?
if value.is_a?(String) || value.is_a?(Numeric)
html += content_tag(:ul, class: 'list-group') {
ul_contents = ''
ul_contents << content_tag(:li, content_tag(:h3, key.to_s.underscore.humanize.titleize), class: 'list-group-item')
ul_contents << content_tag(:li, value, class: 'list-group-item')
ul_contents.html_safe
}
elsif value.is_a?(Hash)
html += content_tag(:ul, class: 'list-group') {
ul_contents = ''
ul_contents << content_tag(:li, content_tag(:h3, key.to_s.underscore.humanize.titleize), class: 'list-group-item')
inner = content_tag(:li, format_hash(value), class: 'list-group-item')
ul_contents << inner
ul_contents.html_safe
}
elsif value.is_a?(Array)
html += format_array(value)
else
Rails.logger.info "Unexpected value in format_hash: #{value.inspect}"
Rails.logger.info "value type: #{value.class.name}"
end
end
html.html_safe
end
def format_array(array, html = '')
array.each do |value|
if value.is_a?(String)
html += content_tag(:div, value).html_safe
elsif value.is_a?(Hash)
html += format_hash(value)
elsif value.is_a?(Array)
html += format_array(value)
else
Rails.logger.info "Unexpected value in format_array: #{value.inspect}"
Rails.logger.info "value type: #{value.class.name}"
end
end
html
end
end
This code can also be used to display XML by setting the hash value equal to Hash.from_xml(your_xml_data) and then passing that in to format_hash(hash).
Please note that the from_xml method may strip off XML tag attributes, so it works best for XML that doesn't have attributes.
Related
as in title I'm trying to create helper that does that but I'm struggling. I'm getting errors or simply empty list like this:
And I want to achieve this:
There is to much logic to simply put this code in view. A results is a hash where the key is a website id and value is either an array of bookmarks ids or just bookmark id.
My code:
module WebsitesHelper
def present_search_results(results)
content_tag(:ul, class: "websites-list") do
results.each do |key, value|
website = Website.find(key)
concat(content_tag(:li, website.url, class: "website-#{key}") do
bookmarks = website.bookmarks.select do |b|
if value.is_a?(Array)
value.include?(b.id)
else
value = b.id
end
end
content_tag(:ul, nil, id: "website-#{key}") do
bookmarks.each do |b|
content_tag(:li, b.title)
end
end
end)
end
end
end
end
If you want to stick with helpers, then something like this could help:
def present_search_results(results)
content_tag(:ul, class: "websites-list") do
results.map do |website_id, bookmarks|
bookmarks = [bookmarks] unless bookmarks.is_a?(Array)
content_tag(:li, class: "website-#{website_id}") do
website = Website.find(website_id)
concat(website.url)
concat(
content_tag(:ul, class: "bookmarks-list") do
bookmarks.map do |bookmark_id|
bookmark = Bookmark.find(bookmark_id)
content_tag(:li, bookmark.title)
end.reduce(:+)
end
)
end
end.reduce(:+)
end
end
But, in my opinion, that code is not easy to read, so you could use plain html instead, like this:
def present_search_results(results)
list = "<ul class='websites-list'>"
results.each do |(website_id, bookmarks)|
bookmarks = [bookmarks] unless bookmarks.is_a?(Array)
website = Website.find(website_id)
list += "<li class='website-#{website_id}'>#{website}"
list += "<ul class='bookmarks-list'>"
bookmarks.each do |bookmark_id|
bookmark = Bookmark.find(bookmark_id)
list += "<li>#{bookmark.title}</li>"
end
list += "</ul></li>"
end
list += "</ul>"
list.html_safe
end
I like this one better, since it is easier to read. But both with output the list you want.
I have my custom presenter
class ShiftPresenter
def initialize(shift, template)
#shift = shift
#template = template
end
def h
#template
end
def users_list
logs = ShiftLog.by_shift(#shift)
names = logs.map do |log|
log.cardiologist.name
end
h.content_tag :div, names unless names.empty?
end
end
and #index view
- present shift do |shift_presenter|
= shift_presenter.user_list
How to present users names using li instead of ['tom', 'jerry']
You can add this to your presenter method:
def users_list
logs = ShiftLog.by_shift(#shift)
names = logs.map(&:cardiologist).map(&:name)#.compact.uniq # you can add this if you want
h.content_tag :div do
h.content_tag :ul do
ul_content = ''.html_safe
names.each do |name|
ul_content << h.content_tag :li, name
end
ul_content
end
end
The thing is it works as block with the return statement: the last used/returned object will be put inside the content_tag.
Try to wrap each element of names in users_list method into <li> tag and join them in a string. To do this you need to change this line:
h.content_tag :div, names unless names.empty?
into this:
h.content_tag :div, names.map{|str| '<li>' + str + '</li>'}.join unless names.empty?
I have notes attribute in Product model with text "something, something else".
In views I wanted see:
<div>
<span>Something</span>
<span>Something else</span>
</div>
Also I have working code, but I want refactor with decorator(draper) or maybe use helpers.
%div
- product.notes.split(/,/).each do |e|
%span= e.strip.capitalize
In decorator:
def notes_list
model.notes.split(/,/).each do |e|
h.content_tag(:span, e.strip.capitalize)
end
end
In views:
%div
= product.notes_list
(or analog in helpers:
def notes_list(product)
product.notes.split(/,/).each do |element|
content_tag(:span, element.strip.capitalize)
end
end
call:
%div
= notes_list(product)
)
But this returns
<div>
"
["something", " something else"]
"
</div>
What is wrong?
your notes_list is returning product.notes.split(/,/)
Try
def notes_list(product)
result = product.notes.split(/,/).inject([]) do |result, element|
result << content_tag(:span, element.strip.mb_chars.capitalize)
end
result.join("\n")
end
I'm developing an application that displays tabular data in many different areas and I find myself constantly using the same HTML table structure over and over. For example a particular table looks like this:
%table.zebra-striped#user-table{ :cellspacing => "0" }
%colgroup
%col{:id => "email"}
%col{:id => "username"}
%col{:id => "sign-in-count"}
%col{:id => "last-sign-in-at"}
%thead
%tr
%th{:id => "email-head", :scope => "col"} E-mail
%th{:id => "username-head", :scope => "col"} Username
%th{:id => "sign-in-count-head", :scope => "col"} Sign Ins
%th{:id => "last-sign-in-at-head", :scope => "col"} Last Sign In
%tbody
- #users.each do |user|
%tr{ :class => zebra }
%td
=h user.email
%td
=h user.username
%td
=h user.sign_in_count
%td
=h user.last_sign_in_at
Ideally, I would like to create some kind of helper method where I could do something like:
= custom_table_for #users do
= column :email
= column :username do |user|
= link_to user.username, user_path(user)
= column "Sign Ins", :sign_in_count
= column :last_sign_in_at
This way I can change the formatting of the data in the columns and the column header names if I'm not happy with default values, but have the table generated for me.
I suppose I could create a normal helper, but I'd have to use arrays and I have no idea how I could include custom data formatting per column.
active_admin has something similar to this which you can see here: http://activeadmin.info/docs/3-index-pages/index-as-table.html
Any leads or ideas would be greatly appreciated.
I just came up with this:
A few points:
The line #columns = [] is a reset so you can call it more than once.
The yield in the custom_table_for calls the block that you pass it.
The block in the column method is stored and called in custom_table_for if it is set.
I included a sample class to show the usage too.
please note I did this outside of a rails app and you almost certainly want to use http://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag instead of the p "<table>" this is merely for sample purposes when you run it in the console.
module TableHelper
def custom_table_for(items)
#columns = []
yield
p "<table>"
#columns.each do |c|
p "<th>#{c[:value]}</th>"
end
items.each do |e|
p "<tr>"
#columns.each do |c|
e[c[:name]] = c[:block].call(e[c[:name]]) if c[:block]
p "<td>#{e[c[:name]]}</td>"
end
p "</tr>"
end
p "</table>"
end
def column(name, value = nil, &block)
value = name unless value
#columns << {:name => name, :value => value, :block => block}
end
end
class ExampleTable
include TableHelper
def test
#users = [{:email => "Email 1", :username => "Test User"}, {:email => "Email 2", :username => "Test User 2"}]
custom_table_for #users do
column :email, "Email"
column :username do |user|
user.upcase
end
end
end
end
et = ExampleTable.new
et.test
UPDATE
I migrated this to rails to use content_tags
module TableHelper
def custom_table_for(items)
#columns = []
yield
content_tag :table do
thead + tbody(items)
end
end
def thead
content_tag :thead do
content_tag :tr do
#columns.each do |c|
concat(content_tag(:th, c[:value]))
end
end
end
end
def tbody(items)
content_tag :tbody do
items.each { |e|
concat(content_tag(:tr){
#columns.each { |c|
e[c[:name]] = c[:block].call(e[c[:name]]) if c[:block]
concat(content_tag(:td, e[c[:name]]))
}
})
}
end
end
def column(name, value = nil, &block)
value = name unless value
#columns << {:name => name, :value => value, :block => block}
end
end
To compliment #gazler's response, here's a way to make a table of a single resource-- column one for attribute names, column two for their values:
module TableHelper
#resource = nil
def simple_table_for(resource)
#resource = resource
content_tag :table do
content_tag :tbody do
yield
end
end
end
def row(key, label = nil, &block)
if key.is_a? String
label = key
end
content_tag(:tr) {
concat content_tag :td, label || key.capitalize
concat content_tag(:td ){
if block_given?
yield
else
#resource.send(key)
end
}
}
end
end
I am using a block method to print a list, but it is generating error.
class MyDataListBuilder
attr_accessor :object
def initialize(object)
#object = object
end
def column (&block)
content_tag :li, block.call
end
end
and using it as
<%= my_data_list_for #leads, [" :10", "Age:30", "Contact:140", "Phone:140", "Email:180", "Company:100", ""] do |l| %>
<%= l.column do %>
<%= object.age %>
<% end %>
<% end %>
other methods are
def list_headers(args=[])
args = Array.new(args)
columns = []
args.map { |o| columns << content_tag(:li, o.split(":").first, :style=>"width:#{o.split(":").second}px;") }
content_tag(:ul, columns.join(" ").html_safe, :class=>"list-headers")
end
def my_data_list_for(object, headers=[], &block)
arr = []
object.each do |o|
arr = capture(DataListHelper::MyDataListBuilder.new(o), &block)
end
content_tag(:ol, list_headers(headers) + arr, :class=>"data-list")
end
it is generating an error and i can not figure out why:
ActionView::Template::Error (undefined local variable or method `object' for #<#<Class:0xcaa1ca0>:0xca9ebf4>):
Please help me in it.
This solves the issue.
class MyDataListBuilder
include ActionView::Helpers::TagHelper
include ActionView::Helpers::CaptureHelper
attr_accessor :object, :output_buffer
def initialize(object)
#object = object
#output_buffer = nil
end
def column (&block)
if block_given?
content_tag(:li, capture(self, &block))
else
content_tag(:li, "")
end
end
end