Listalicious

Semantic listing; a semantic way to build datagrid structures in Rails.

The Story

I started this project to create a pretty simple and clean way to create datagrids. I quickly learned that datagrids aren’t especially easy to make amazing, and there’s a lot of things that one might want to do with them… grouping (with a row that creates a separator), sorting, ordering, additional informational rows, footers, ajax pagination, etc. etc. etc.. The list is actually pretty long, but I’ve only written a few of those things. This is a good start, and if it doesn’t contain the features you’re looking for feel free to fork and add them.

So, as always, I set up some requirements for what the project should do, and here’s what I came up with (a lot of these are standard):

  • Should be simple
  • Should be easily customizable
  • Should handle sorting
  • Should handle grouping
  • Should handle ordering columns
  • Should handle additional informational rows
  • Should be pleasant to use
  • Should use less code to create than it generates

I wrote a DSL that met those requirements:


  <% semantic_list_for @users, :as => :user do |l| %>
    <%= l.head do %>
      <%= l.column 'User Login' %>
      <%= l.column 'Email' %>
    <% end %>
    <%= l.columns do |user, index| %>
      <%= l.column :login %>
      <%= l.column link_to(user.email, "mailto:#{user.email}") %>
      <%= l.controls link_to('edit', edit_user_path(user)) %>
    <% end %>
    <%= l.foot do %>
      <%= l.full_column will_paginate(@users) %>
    <% end %>
  <% end %>

I liked this, but could it be reduced further if the use case allowed? I came up with the following with help from a co-worker (which outlines the different ways you can pass args)


  <% semantic_list_for @users, :as => :user do |l| %>
    <%= l.columns [:head, :body] do |user, index| %>
      <%= l.column :login, :width => '20%' %>
      <%= l.column "#{user.first_name} #{user.last_name}", :title => 'Name' %>
      <%= l.column :email, proc { link_to user.email, "mailto:#{user.email}" } %>
      <%= l.controls do %>
        <%= link_to('edit', edit_user_path(user)) %>
      <% end %>
    <% end %>
  <% end %>

I implemented both DSLs and leave it to your discretion on which one is best to use in your case.

Since using builders on previous projects has worked out fairly well, I tried the same thing here. There’s a single builder provided for now, TableBuilder (which inherits from GenericBuilder). GenericBuilder provides some basic functionality (ordering links for example), and isn’t intended to be used as a builder by itself. And as always, the builders can be extended or replaced if you need more custom markup.

Installation

The gem is hosted on gemcutter, so if you haven’t already, add it as a gem source:


  sudo gem sources -a http://gemcutter.org/

Then install the listalicious gem:


  sudo gem install listalicious

Usage

Listalicious works fine as a simple view helper, but it also handles ordering of the lists as well. To add ordering to any list just use the ordered_from named scope with your find method. You can also specify fields that are allowed to be ordered in your model by using the orderable_fields method.

Controllers


  def index
    @user = User.ordered_from(params).paginate :page => params[:page], :per_page => 20
    # respond ...
  end

Models


  class User < ActiveRecord::Base
    orderable_fields do
      only :first_name, :last_name, :login, :email
      default :login, :desc, :stable => true
    end
  end

Views (I prefer HAML, so no ERB examples, but it should work with ERB fine as well)

It’s important to note that all the methods take blocks or content strings. For instance, look at the extra and controls methods in the simple usage example.

Simple Usage


  - semantic_list_for @users, :as => :user, :html => {:class => 'list'} do |l|
    = l.columns [:head, :body] do |user, index|
      = l.column :login, :width => '20%'
      = l.column :email, :title => 'Email Address'
      = l.extra do
        = "You can add more information about #{user.first_name} here."
      = l.controls link_to('edit', edit_user_path(user))
…produces…

  <table class="list semantic-list" id="user_list">
    <thead>
      <tr class="header">
        <th width="20%"><a class="sort-ascending" href="?user_sort_desc=login">Login</a></th>
        <th><a href="?user_sort_asc=email">Email Address</a></th>
      </tr>
    </thead>
    <tbody>
      <tr class="even">
        <td>jejacks0n</td>
        <td>[email protected]</td>
        <td class="controls"><a href="/users/1/edit">edit</a></td>
      </tr>
      <tr class="even">
        <td colspan="3">You can add more information about Jeremy here.</td>
      </tr>
      <tr class="odd">
        <td>user1</td>
        <td>[email protected]</td>
        <td class="controls"><a href="/users/2/edit">edit</a></td>
      </tr>
      <tr class="odd">
        <td colspan="3">You can add more information about User here.</td>
      </tr>
    </tbody>
  </table>

Extended Usage


  - semantic_list_for @users do |l|
    = l.head do
      = l.column 'Name', :sort => 'last_name', :width => '20%'
      = l.column 'Email Address'
    = l.columns do |user, index|
      = l.column "#{user.first_name} #{user.last_name}"
      = l.column :html => {:class => 'email'} do
        = link_to(user.email, "mailto:#{user.email}")
      = l.controls link_to('edit', edit_protosite_user_path(user))
    = l.foot do
      = l.full_column will_paginate(l.collection)
…produces…

  <table class="semantic-list" id="user_list">
    <thead>
      <tr class="header">
        <th width="20%"><a class="ascending" href="?!sort_query!">Name</a></th>
        <th>Email Address</th>
      </tr>
    </thead>
    <tbody>
      <tr class="even">
        <td>Jeremy Jackson</td>
        <td class="email"><a href="mailto:[email protected]">[email protected]</a></td>
        <td class="controls"><a href="/users/1/edit">edit</a></td>
      </tr>
      <tr class="odd">
        <td>User 1</td>
        <td class="email"><a href="mailto:[email protected]">[email protected]</a></td>
        <td class="controls"><a href="/users/2/edit">edit</a></td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <th colspan="3">
          <div class="pagination">[removed for your sanity]</div>
        </th>
      </tr>
    </tfoot>
  </table>

Grouping — You can group lists, and this will add extra header rows as separators. This will include sort links if those were provided as well.


  - semantic_list_for @users, :as => :user, :group_by => :login do |l|

Sorting — Sorting requires javascript. It’s part of the javascript code that comes with Listalicious and requires Prototype.js. If you want to add sorting to the list, just provide a url for a sort action and it will put that into the HTML5 data-sorturl attribute. You can use your own javascript if you would like. This feature is incomplete.


  - semantic_list_for @users, :as => :user, :sort_url => { :action => 'sort' } do |l|

UL / OL Datagrids

The nature of doing a UL / OL based datagrid requires a certain level of CSS, and I haven’t had time or reason to provide that level yet. When I finish up this project and include the JS and CSS needed for it (in a generator) I may add a builder for this, but there isn’t plans for one currently.

Other

It’s important to note that a lot of the functionality of these lists do not play nicely with one another — I don’t believe this is a shortcoming, and consider it more an effort to avoid overkill. A good example of this is the sortable features. For example, if you need the extra information and sorting together, you should consider using the :expandable => true option. Grouping and sorting don’t play together nicely for obvious reasons as well. The javascript handles moving the extra container, in sorting, but doesn’t attempt to do it gracefully. If I do the ListBuilder (UL/OL) to compliment the TableBuilder, it would likely handle these things somewhat better, but I haven’t had a need for it yet.

And, as always, you can create your own builder by extending one of the existing ones, or by creating one from scratch.

Then just specify your builder, or do it as a configuration.


  semantic_list_for @users, :builder => MyCustomBuilder do

  Listalicious::SemanticListHelper.builder = MyCustomBuilder

Documentation

RDoc documentation should be automatically generated after each commit and made available on the rdoc.info website.

Documentation is pretty sparse right now, and I’m working to resolve it.

Project Info

Listalicious is hosted on Github: http://github.com/jejacks0n/listalicious, and the gem is available on Gemcutter: http://gemcutter.org/gems/listalicious

Copyright © Jeremy Jackson, released under the MIT license.