2007/4/23

An Insanely Handy Table Generator in Ruby

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 意見: