Recently we have been developing an application that displays a slew of information. Most of the items displayed are in the form of table. So there we are, first writing each table component by hand, then we get tired of it. It's just simply... not DRY. So like every other sane developer, we started writing our own table tag helpers, to save our life (and our poor fingers) to generate those pesky <table> tags.
With the help of some good Ruby, the table generator can be an all-season, yet still very terse, helper.
The Code
def generate_table(array_object, titles=[], columns=[], callbacks={})
html = ""
html += '<table class="auto">'
html += "<tr>"
titles.each { |t| html += "<th>#{h(t)}</th>"}
html += "</tr>"
row_number = 0
array_object.each do |row|
html += "<tr>"
columns.each do |col|
if col == :_row_number
data = row_number
else
if row.class == Hash
data = (col.class == Array ? col.collect {|c| row[c]} : row[col])
else
data = (col.class == Array ? col.collect {|c| row.send(c)} : row.send(col))
end
end
if callbacks[col]
if data.class == Date || data.class == Time || data.class == DateTime
str = callbacks[col].call(data)
else
str = callbacks[col].call(*data)
end
else
str = h(data)
end
html += "<td>#{str}</td>"
end
html += "</tr>"
row_number += 1
end
html += "</table>"
end
Here is the code of the generator. We made it into a Rails helper class (ApplicationHelper), but this snippet is not just good for Rails.
Usage
This generator expects the table data source to be in either an array of hases, of an array of ActiveRecord-like objects, ie. objects that have attribute accessors, such as student.name. So say we want to generate a table of students, we do:
generate_table Students.find(:all),
["Name", "Address", "Mobile", "Homepage"],
[:name, :address, :mobile, :homepage],
{
:homepage => lambda { |hp| link_to "visit the homepage", hp }
}
This helper has three required arguments and one optional. The first three are an array of objects, an array of titles, an array of attributes to be fetched. The optional argument is an hash of callbacks.
In our case, the generator iterates the array of Student objects, and retrieves each's name, address, mobile phone number and homepage. The column homepage will be post-processed by a callback (lambda), which in turn yields the URL with a title "visit the homepage".
Array of hashes fits in the picture well. The secret is, the generator knows if the row object is a hash. If it is, it uses Hash key-value accessor Hash#[] to retrieve the column, instead of sending messages to the row object.
Fancier Usage Is Just a Hack Away
In some cases, we want to reuse the same attribute in different columns, say we want to use student id to fetch another data. The problem being, since the callback is a hash, an attribute can have exactly only one callback. Or does it have to be? Once again, some Ruby comes to help:
generate_table Student.find(:all),
["Student ID", "Extra Info"],
[:id, [:id, :name]],
{
[:id, :name] => lambda{ |i, n| retrieve_extra(i) }
}
The point is that arrays can actually be fine hash keys, and when the callback of such column is given exact the same number of arguments. In this case, whether you want to use the attribute name is irrelevant. We just use this "virtual" attribute to let us get by.
... And One More Thing
An attribute, :_row_number, yields the current row number, starting from zero. This doubles as a counter, and can be useful in items such as a ranked list.
Possible Extensions
There are dozens of ways to extend the generator. Styling options, even/odd row handler, more hidden attributes (:_row_object anyone?) and column-span support all come to mind. For the time being at least, the snippet works perfectly for me, and I love it to stay the way it is.
Ruby Tuesday Taiwan
So that's so much for today! If you happen to be living in northern Taiwan and are interested in Ruby and/or Rails, we are having a regular meet-up at OP Café in Hsinchu City. The next meet-up will be on Tuesday, May 1, 2007, starting from 7:30 pm. I'll be talking about the sorry state of affair of the login thingy--how we end up rewriting our own login system each time for each app, and some possible remedies. See you there!
0 意見:
張貼意見