dining-table
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 theh
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 thead
tags. 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
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.