Tabulatr – Index Tables made easy, finally

Notes:

Mission Objective

Tabulatr aims at making the ever-recurring task of creating listings of ActiveRecord or Mongoid models simple and uniform.

We found ourselves reinventing the wheel in every project we made, by using

  • different paging mechanisms,
  • different ways of implementing filtering/searching,
  • different ways of implementing selecting and batch actions,
  • different layouts.

We finally thought that whilst gems like Formtastic or SimpleForm provide a really cool, uniform, and concise way to implement forms, it’s time for a table builder. During a project with Magento, we decided that their general tables are quite reasonable, and enterprise-proven — so that’s our starting point.

Visual Components

Every Tabulatr table consists of the following components:

  • a wrapping form
  • a ‘Controls Container’ containing the table-controls as
    • the paginator,
    • the batch_actions,
    • the submit button,
    • controls to manage selecting/checking/marking,
    • an info_text
  • The actual table consisting of
    • A header row,
    • A filter row,
    • data rows,
    • possibly a selecting/checking/marking column
    • possibly action links/buttons

Whilst this is a strong decision, we made everything as-configurable-as-possible, there are roughly 150 options you can use to tweak almost every aspect of Tabulatr.

Features

Tabulatr tries to make these common tasks as simple/transparent as possible:

  • paging
  • selecting/checking/marking
  • filtering
  • batch actions
  • stateful index pages

Example

Models

We suppose we have a couple of models

  • tags has and belong to many products
  • vendors have many products
  • products belong to vendors and have and belong to many tags

Controller

In the products controllers index action we have:


  def index
    @products = Product.find_for_table(params)
  end

the find_for_table method is injected into ActiveRecord by Tabulatr, it retrieves all relevant data from the params hash automagically. If you want the the table to be stateful, i.e. remembering its sorting/filtering/selecting settings (very useful when processing a certain selection of entries), you can pass the option :stateful => session to the find_for_table call and the data will be stored. Also, a Reset button will be rendered to get rid of the state.

View

To get a simple Table, all we need to do is


  <%= table_for @products do |t|
    t.column :id
    t.column :title
    t.column :price
    t.column :active
    t.association :vendor, :name
    t.association :tags, :title
  end %>

but the result is pretty fancy, already:

To add a column with checkboxes (thus giving all the “Select …” buttons a reasonable semantics), we simply add a


  t.checkbox

row.

To add e.g. edit-buttons, we specify


  t.action do |record|
    link_to "Edit", edit_product_path(record)
  end

To add a select-popup with batch-actions (i.e., actions that are to be performed on all selected rows), we add an option to the table_for:


  <%= table_for @products, :batch_actions => {'foo' => 'Foo', 'delete' => "Delete"} do |t|
    ...
  end %>

If you prefer individual buttons for the batch actions over a select input, give the option :batch_actions_type => :button to the table_for call.

To handle the actual batch action, we have to add a block to the find_for_table call in the controller:


  @products = Product.find_for_table(params) do |batch_actions|
    batch_actions.delete do |ids|
      ids.each do |id|
        Product.find(id).destroy
      end
      redirect_to index_select_products_path()
      return
    end
    batch_actions.foo do |ids|
      ... do whatever is foo-ish to the records
    end
  end

where the ids parameter to the block is actually an Array containing the numeric ids of the currently selected rows.

Installation

Installation is pretty straightforward. Just include a line


  gem 'tabulatr', '...current version here'

in your applications Gemfile, then run bundle install.

If you want to use a predefined stylesheet and the images for sorting and paging, just issue


  $ rails g tabulatr:install
       create  public/stylesheets/tabulatr.css
       create  public/images/tabulatr/buttons_lite_background.png
       create  public/images/tabulatr/pager_arrow_left.gif
       create  public/images/tabulatr/pager_arrow_left_off.gif
       create  public/images/tabulatr/pager_arrow_right.gif
       create  public/images/tabulatr/pager_arrow_right_off.gif
       create  public/images/tabulatr/sort_arrow_down.gif
       create  public/images/tabulatr/sort_arrow_down_off.gif
       create  public/images/tabulatr/sort_arrow_up.gif
       create  public/images/tabulatr/sort_arrow_up_off.gif
  -------------------------------------------------------
  Please note: We have copied a sample stylesheet to
    public/stylesheets/tabulatr.css
  to actually use it in your application, please include
  it in your layout file. You may use s/th like
    <%= stylesheet_link_tag :tabulatr %>
  for that.
  -------------------------------------------------------

Options

Literally every aspect of Tabulatr can be tweaked to fit your needs. You may add or remove components, change the class or other html attributes of the generated html nodes. There are options of several types:

Table Options

These options are to be specified at the form_for level and change the appearance and behaviour of the table. This is taken from lib/tabulatr/tabulatr/settings.rb


  remote: false,                                # add data-remote="true" to form
  form_class: 'tabulatr_form',                  # class of the overall form
  table_class: 'tabulatr_table',                # class for the actual data table
  sortable_class: 'sortable',                   # class for the header of a sortable column
  sorting_asc_class: 'sorting-asc',             # class for the currently asc sorting column
  sorting_desc_class: 'sorting-desc',           # class for the currently desc sorting column
  page_left_class: 'page-left',                 # class for the page left button
  page_right_class: 'page-right',               # class for the page right button
  page_no_class: 'page-no',                     # class for the page no <input>
  control_div_class_before: 'table-controls',   # class of the upper div containing the paging and batch action controls
  control_div_class_after: 'table-controls',    # class of the lower div containing the paging and batch action controls
  paginator_div_class: 'paginator',             # class of the div containing the paging controls
  batch_actions_div_class: 'batch-actions',     # class of the div containing the batch action controls
  select_controls_div_class: 'check-controls',  # class of the div containing the check controls
  submit_class: 'submit-table',                 # class of submit button
  pagesize_select_class: 'pagesize_select',     # class of the pagesize select element
  select_all_class: 'select-btn',               # class of the select all button
  select_none_class: 'select-btn',              # class of the select none button
  select_visible_class: 'select-btn',           # class of the select visible button
  unselect_visible_class: 'select-btn',         # class of the unselect visible button
  select_filtered_class: 'select-btn',          # class of the select filtered button
  unselect_filtered_class: 'select-btn',        # class of the unselect filteredbutton
  info_text_class: 'info-text',                 # class of the info text div

  batch_actions_label: 'Batch Action: ',        # Text to show in front of the batch action select
  batch_actions_type: :select,                  # :select or :button depending on the kind of input you want
  batch_actions_class: 'batch-action-inputs',   # class to apply on the batch action input elements
  submit_label: 'Apply',                        # Text on the submit button
  select_all_label: 'Select All',               # Text on the select all button
  select_none_label: 'Select None',             # Text on the select none button
  select_visible_label: 'Select visible',       # Text on the select visible button
  unselect_visible_label: 'Unselect visible',   # Text on the unselect visible button
  select_filtered_label: 'Select filtered',     # Text on the select filtered button
  unselect_filtered_label: 'Unselect filtered', # Text on the unselect filtered button
  reset_label: 'Reset',                         # Text on the reset button
  info_text: "Showing %1$d, total %2$d, selected %3$d, matching %4$d",

  # which controls to be rendered above and below the tabel and in which order
  before_table_controls: [:submit, :paginator, :batch_actions, :select_controls, :info_text],
  after_table_controls: [],

  # whih selecting controls to render in which order
  select_controls: [:select_all, :select_none, :select_visible, :unselect_visible,
                    :select_filtered, :unselect_filtered],

  image_path_prefix: '/images/tabulatr/',
  pager_left_button: 'left.gif',
  pager_left_button_inactive: 'left_off.gif',
  pager_right_button: 'right.gif',
  pager_right_button_inactive: 'right_off.gif',
  sort_up_button: 'up.gif',
  sort_up_button_inactive: 'up_off.gif',
  sort_down_button: 'down.gif',
  sort_down_button_inactive: 'down_off.gif',

  make_form: true,                            # whether or not to wrap the whole table (incl. controls) in a form
  table_html: false,                          # a hash with html attributes for the table
  row_html: false,                            # a hash with html attributes for the normal trs
  header_html: false,                         # a hash with html attributes for the header trs
  filter_html: false,                         # a hash with html attributes for the filter trs
  filter: true,                               # false for no filter row at all
  paginate: true,                             # true to show paginator
  sortable: true,                             # true to allow sorting (can be specified for every sortable column)
  selectable: true,                           # true to render "select all", "select none" and the like
  action: nil,                                # target action of the wrapping form if applicable
  batch_actions: false,                       # name: value hash of batch action stuff
  translate: false,                           # call t() for all 'labels' and stuff, possible values are true/:translate or :localize
  row_classes: ['odd', 'even']                # class for the trs

Column Options


  header: false,                   # a string to write into the header cell
  width: false,                    # the width of the cell
  align: false,                    # horizontal alignment
  valign: false,                   # vertical alignment
  wrap: true,                      # wraps
  type: :string,                   # :integer, :date
  td_html: false,                  # a hash with html attributes for the cells
  th_html: false,                  # a hash with html attributes for the header cell
  filter_html: false,              # a hash with html attributes for the filter cell
  filter: true,                    # false for no filter field,
                                   # container for options_for_select
                                   # String from options_from_collection_for_select or the like
                                   # :range for range spec
                                   # :checkbox for a 0/1 valued checkbox
  checkbox_value: '1',             # value if checkbox is checked
  checkbox_label: '',              # text behind the checkbox
  filter_width: '97%',             # width of the filter <input>
  range_filter_symbol: '&ndash;',  # put between the <inputs> of the range filter
  format: false,                   # a sprintf-string or a proc to do special formatting
  method: false,                   # if you want to get the column by a different method than its name
  link: false,                     # proc or symbol to make the content a link
  map: true,                       # whether to map the call on individual records (true) of an association or call on the list (false)
  join_symbol: ', ',               # symbol used to join the elements of 'many' associations
  sortable: true                   # if set, sorting-stuff is added to the header cell

Global settings

These settings are not supposed to be changed on a per-table basis. This is necessary, because the find_for_table method needs to rely on a priori information.

Table Form Options

This hash contains all the information about the naming of the generated form elements. To override them, you should, in an initializer, call


  Tabulatr.table_form_options(my_options_hash)

  batch_action_name: 'batch_action',       # name of the batch action param
  sort_by_key: 'sort_by_key',              # name of key which to search, format is 'id asc'
  pagination_postfix: '_pagination',       # name of the param w/ the pagination infos
  filter_postfix: '_filter',               # postfix for name of the filter in the params hash: xxx_filter
  sort_postfix: '_sort',                   # postfix for name of the filter in the params hash: xxx_filter
  checked_postfix: '_checked',             # postfix for name of the checked in the params hash: xxx_filter
  associations_filter: '__association',    # name of the associations in the filter hash
  method: 'post',                          # http method for that form if applicable
  batch_postfix: '_batch',                 # postfix for name of the batch action select
  checked_separator: ','                   # symbol to separate the checked ids

Paginate options

To override these, you should, in an initializer, call


  Tabulatr.paginate_options(my_options_hash)

  page: 1,
  pagesize: 10,
  pagesizes: [10, 20, 50]

Dependencies

We use

  • whiny_hash to handle the options in a fail-early-manner,
  • id_stuffer to compress the remembered ids.
  • And… eh… It’s an extension for Rails 3, so it might be handy to have a version of Rails 3 and Ruby 1.8.? or 1.9.?.

Bugs

Request-URI Too Large error

Since everyhing about the state of the table is intentionally encoded in the URI, the URI may become somewhat bloated if fancy selecting is used. By using id_stuffer, we drastically reduced the size of the URI (that’s the reason we actually made it), but it may still become rather large. This is a problem particulary when using WEBrick, because WEBricks URIs must not exceed 2048 characters. And this limit is hard-coded IIRC. So – If you run into this limitation – please consider using another server. (Thanks to stepheneb for calling my attention back to this.)

Other, new bugs

There are roughly another 997 bugs in Tabulatr, although we do some testing (see spec/). So if you hunt them, please let me know using the GitHub Bugtracker.

Contributing to tabulatr

  • Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet
  • Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it
  • Fork the project
  • Start a feature/bugfix branch
  • Commit and push until you are happy with your contribution
  • Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.
  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
  • Feel free to send a pull request if you think others (me, for example) would like to have your change incorporated into future versions of tabulatr.

License

Copyright © 2010-2011 Peter Horn, Provideal GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.