Skip to content

mvdamme/dining-table

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

78 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dining-table

Build Status

dining-table allows you to write clean Ruby classes instead of messy view code to generate HTML tables. You can re-use the same classes to generate csv or xlsx output as well.

dining-table was inspired by the (now unfortunately unmaintained) table_cloth gem. This gem is definitely not a drop-in replacement for table_cloth, it aims to be less dependent on Rails (no Rails required to use dining-table, in fact it has no dependencies (except if you chose to generate xlsx output)) and more flexible.

Installation

Add the following to your Gemfile:

gem 'dining-table'

Basic example

A table is defined by creating a table class (usually placed in app/tables) that inherits from DiningTable::Table and implements the `define' method:

class CarTable < DiningTable::Table
  def define
    column :brand
    column :number_of_doors
    column :stock
  end
end

In your views, you can now provide a collection of @cars and render the table in HTML:

<%= CarTable.new(@cars, self).render %>

By default, a table will have a header with column names, and no footer. There is one row per element in the collection (@cars in this example), and rows are rendered in order.

Table class

Defining columns

Columns are defined by using column in the define method of the table class. The content of the cell is determined by calling the corresponding method of the object: for column :brand, the brand method is called for each car in the collection, and the result is placed in the appropriate cells. If this is not what you want, you can provide a block:

class CarTable < DiningTable::Table
  def define
    column :brand do |object|
      object.brand.upcase
    end
  end
end

Headers and footers

When you don't explicitly specify a header, the header is set using human_attribute_name (if the objects in the collection respond to that method). You can also manually specify a header:

class CarTable < DiningTable::Table
  def define
    column :brand, header: I18n.t('car.brand') do |object|
      object.brand.upcase
    end
  end
end

The custom header can be a string, but also a lambda or a proc.

If for some reason you don't want a header, call skip_header:

class CarTable < DiningTable::Table
  def define
    skip_header
    column :brand
  end
end

By default, dining-table doesn't add a footer to the table, except when at least one column explicitly specifies a footer:

class CarTable < DiningTable::Table
  def define
    column :brand
    column :stock, footer: lambda { "Total: #{ collection.sum(&:stock) }" }
  end
end

Please note how the collection passed in when creating the table object (@cars in CarTable.new(@cars, self)) is available as collection.

Similarly to skip_header, if for some reason you don't want a footer (even though at least one column defines one), call skip_footer:

class CarTable < DiningTable::Table
  def define
    skip_footer
    column :brand, footer: 'Footer'
  end
end

Empty collection

Note that when the collection to be presented in the table is empty, dining-table can't determine table headers that aren't explicitly specified as there are no objects to use with human_attribute_name. In order to avoid this edge case, you can pass in the class of the objects normally present in the collection when creating the table:

<%= CarTable.new(@cars, self, class: Car).render %>

Links and view helpers

When rendering the table in a view using <%= CarTable.new(@cars, self).render %>, the self parameter is the view context. It is made available through the h method (or the helpers method if you prefer to be more explicit). You can use h to get access to methods like Rails' link_to, path helpers, and view helpers:

class CarTable < DiningTable::Table
  def define
    column :brand do |object|
      h.link_to( object.brand, h.car_path(object) )
    end
    column :stock do |object|
      h.number_with_delimiter( object.stock )
    end
  end
end

You can also use h in headers or footers:

class CarTable < DiningTable::Table
  def define
    column :brand, header: h.link_to('Brand', h.some_path)
  end
end

When you want to render a table outside of a view (or when rendering csv or xlsx tables, see further) you have the following options:

  • Pass in nil if you don't use the h helper in any column: CarTable.new(@cars, nil).render
  • If you do use h, pass in an object that responds to the methods you use. This might be a Rails view context, or any other object:
# in app/tables/car_table.rb
class CarTable < DiningTable::Table
  def define
    column :brand do |object|
      h.my_helper( object.brand )
    end
  end
end

# somewhere else
class FakeViewContext
  def my_helper(a)
    a
  end
end

# when rendering
cars = Car.order(:brand)
CarTable.new(cars, FakeViewContext.new).render

Actions

In the case of HTML tables, one often includes an actions column with links to show, edit, delete, etc. the object. While you can do that using a regular column it is easier using a special actions column:

class CarTable < DiningTable::Table
  def define
    column :brand
    actions header: I18n.t('shared.action') do |object|
      action { |object| h.link_to( I18n.t('shared.show'), h.car_path(object) ) }
      action { |object| h.link_to( I18n.t('shared.edit'), h.edit_car_path(object) ) if object.editable? }
    end
  end
end

Options

When creating the table object to render, you can pass in an options hash:

<%= CarTable.new(@cars, self, admin: current_user.admin? ).render %>

The passed in options are available as options when defining the table. One example of where this is useful is hiding certain columns or actions from non-admin users:

class CarTable < DiningTable::Table
  def define
    admin = options[:admin]
  
    column :brand
    column :number_of_doors
    column :stock if admin
    actions header: I18n.t('shared.action') do |object|
      action { |object| h.link_to( I18n.t('shared.show'), h.car_path(object) ) }
      action { |object| h.link_to( I18n.t('shared.edit'), h.edit_car_path(object) ) if admin }
    end
  end
end

Presenters

HTML

Introduction

The default presenter is HTML (i.e. DiningTable::Presenters::HTMLPresenter), so CarTable.new(@cars, self).render will generate a table in HTML. When defining columns, you can specify options that apply only when using a certain presenter. For example, here we provide css classes for td and th elements for some columns in the html table:

class CarTable < DiningTable::Table
  def define
    column :brand
    column :number_of_doors, html: { td: { class: 'center' }, th: { class: 'center' } }
    column :stock, html: { td: { class: 'center' }, th: { class: 'center' } }
  end
end

The same table class can also be used with other presenters (csv, xlsx or a custom presenter), but the options will only be in effect when using the HTML presenter.

Presenter configuration

By instantiating the presenter yourself it is possible to specify options for a specific table. Using the :tags key you can specify options for all HTML tags used in the table. Example:

<%= CarTable.new(@cars, self, 
                 presenter: DiningTable::Presenters::HTMLPresenter.new( 
                   tags: { table: { class: 'table table-bordered', id: 'car_table' }, 
                           tr: { class: 'car_table_row' } } )).render %>

In the above example, we specify the CSS class and HTML id for the table, and the CSS class to be used for all rows in the table. The supported HTML tags are: table, thead, tbody, tfoot, tr, th, td.

It is also possible to wrap the table in another tag (a div for instance), and specify options for this tag:

<%= CarTable.new(@cars, self, 
                 presenter: DiningTable::Presenters::HTMLPresenter.new( 
                   tags: { table: { class: 'table table-bordered', id: 'car_table' }, 
                   wrap: { tag: :div, class: 'table-responsive' } )).render %>

Most of the html options are usually best set as defaults, see Configuration.

Note that configuration information provided to the presenter constructor is added to the default configuration, it doesn't replace it. This means you can have the default configuration define the CSS class for the table tag, for instance, and add the html id attribute when initializing the presenter, or from inside the table definition.

Configuration inside the table definition

It is possible to specify or modify the configuration from within the table definition. This allows you to use custom CSS classes, ids, etc. per row or even per cell. Example:

class CarTableWithConfigBlocks < DiningTable::Table
  def define
    table_id = options[:table_id]  # custom option, see 'Options' above

    presenter.table_config do |config|
      config.table.class = 'table-class'
      config.table.id    = table_id || 'table-id'
      config.thead.class = 'thead-class'
    end if presenter.type?(:html)

    presenter.row_config do |config, index, object|
      if index == :header
        config.tr.class = 'header-tr'
        config.th.class = 'header-th'
      elsif index == :footer
        config.tr.class = 'footer-tr'
      else  # normal row
        config.tr.class = index.odd? ? 'odd' : 'even'
        config.tr.class += ' lowstock' if object.stock < 10
      end
    end if presenter.type?(:html)

    column :brand
    column :stock, footer: 'Footer text'
  end
end

This example shows how to use presenter.table_config to set the configuration for (in this case) the table and theadtags. The block you use with table_config is called once, when the table is being rendered. A configuration object is passed in that allows you to set any HTML attribute of the seven supported tags.

Note that the configuration object already contains the pre-existing configuration information (coming from either the presenter initialisation and/or from the global configuration), so you can refine the configuration in the block instead of having to re-specify it in full. This means you can easily add CSS classes without knowledge of previously existing configuration:

presenter.table_config do |config|
  config.table.class += ' my-table-class'
end if presenter.type?(:html)

Per row configuration can be specified with presenter.row_config. The block used with this method is called once for each row being rendered, and receives three parameters: the configuration object (identical as with table_config), an index value, and the object containing the data being rendered in this row. The index value is equal to the row number of the row being rendered (starting at zero), except for the header and footer rows, in which case it is equal to :header and :footer, respectively. object is the current object being rendered (nil for the header and footer rows). As above, the passed in configuration object already contains the configuration which is in effect before calling the block.

Per cell configuration

As shown above, you can specify per column configuration using a hash:

class CarTable < DiningTable::Table
  def define
    column :number_of_doors, html: { td: { class: 'center' }, th: { class: 'center' } }
  end
end

For each column, the per column configuration is merged with the row configuration (see presenter.row_config above) before cells from the column are rendered.

Sometimes, you might want to specify the configuration per cell, for instance to add a CSS class for cells with a certain content. This is possible by supplying a lamba or proc instead of a hash:

class CarTable < DiningTable::Table
  def define
    number_of_doors_options = ->( config, index, object ) do
      config.td.class = 'center'
      config.td.class += ' five_doors' if object && object.number_of_doors == 5
    end
    column :number_of_doors, html: number_of_doors_options
  end
end

The arguments provided to the lambda or proc are the same as in the case of presenter.row_config.

CSV

dining-table can also generate csv files instead of HTML tables. In order to do that, specify the presenter when instantiating the table object. You could do the following in a Rails controller action, for instance:

def export_csv
  collection = Car.order(:brand)
  csv = CarTable.new( collection, nil, presenter: DiningTable::Presenters::CSVPresenter.new ).render
  send_data( csv, filename: 'export.csv', content_type: "text/csv" )
end

The CSV Presenter uses the CSV class from the Ruby standard library. Options passed in through the :csv key will be passed on to CSV.new:

csv = CarTable.new( collection, nil, presenter: DiningTable::Presenters::CSVPresenter.new( csv: { col_sep: ';' } ) ).render

CSV options can also be set as defaults, see Configuration

It can often be useful to use the same table class with both html and csv presenters. Usually, you don't want the action column in your csv file. You can easilly omit it when the presenter is not HTML:

class CarTable < DiningTable::Table
  def define
    column :brand
    actions header: I18n.t('shared.action') do |object|
      action { |object| h.link_to( I18n.t('shared.show'), h.car_path(object) ) }
      action { |object| h.link_to( I18n.t('shared.edit'), h.edit_car_path(object) ) }
    end if presenter.type?(:html)
  end
end

Note that you also have access to the presenter inside column blocks, so if necessary you can adapt a column's content accordingly:

class CarTable < DiningTable::Table
  def define
    column :brand do |object|
      if presenter.type?(:html)
        h.link_to( object.brand, h.car_path(object) )
      else
        object.brand  # no link for csv and xlsx
      end
    end
  end
end

Excel (xlsx)

The Excel presenter depends on xlsxtream or axlsx. Note that dining-table doesn't require either xlsxtream or axlsx, you have to add one of them to your Gemfile yourself if you want to use the Excel presenter.

In order to use the Excel presenter, pass it in as a presenter and provide a xlsxtream or axlsx worksheet:

collection = Car.order(:brand)
# sheet is the xlsxtream or axlsx worksheet in which the table will be rendered
CarTable.new( collection, nil, presenter: DiningTable::Presenters::ExcelPresenter.new( sheet ) ).render

Custom column classes

You can write your own column classes and use them for specific columns. For instance, the following column class will format a date using I18n:

class DateColumn < DiningTable::Columns::Column
  def value(object)
    val = super
    I18n.l( val ) if val
  end
end

A column class has to be derived from DiningTable::Columns::Column and implement the value method. The object passed in is the object in the collection for which the current line is being rendered. If you don't have too many custom column classes, an easy place to put the code is in an initializer (e.g. config/initializers/dining-table.rb).

You can use custom column classes as follows:

class CarTable < DiningTable::Table
  def define
    column :fabrication_date, class: DateColumn
  end
end

Configuration

You can set default options for the different presenters in an initializer (e.g. config/initializers/dining-table.rb):

DiningTable.configure do |config|
  config.html_presenter.default_options = { tags: { table: { class: 'table table-bordered' }, 
                                                    thead: { class: 'header' } },
                                            wrap: { tag: :div, class: 'table-responsive' } }
  config.csv_presenter.default_options  = { csv: { col_sep: ';' } }
end

Copyright

Copyright (c) 2018 Michaël Van Damme. See LICENSE.txt for further details.

About

Easily output tabular data, be it in HTML, CSV or XLSX. Create clean table classes instead of messing with views to create nice tables.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages