Skip to content

Instantly share code, notes, and snippets.

@tmichel
Last active December 29, 2017 11:23
Show Gist options
  • Save tmichel/e2623ba25a35bdb7516a600b2ef5bca4 to your computer and use it in GitHub Desktop.
Save tmichel/e2623ba25a35bdb7516a600b2ef5bca4 to your computer and use it in GitHub Desktop.
Very simple table_for and attribute_table_for for Rails
# Drop this into app/helpers and then in your views:
#
# <%= table_for(User.all) do |t| %>
# <% t.column :username %>
# <% t.column :email do |user| %>
# <%= mail_to user.email %>
# <% end %>
# <% t.actions :show, :edit %>
# <% end %>
#
# Assumptions about your Rails app:
#
# * You are using ActiveRecord, table_for only works with ActiveRecord relations
# * You are using cancancan for authorization
# * You are using Kaminari for pagination
#
module TableHelper
def table_for(relation, options={}, &block)
TableBuilder.new(relation, options, self, &block).render
end
def paginated_table_for(relation, options={}, &block)
page_key = options.delete(:page_key) { :page }
per_key = options.delete(:per_key) { :per }
paging_options = options.delete(:paging) { Hash.new }
relation = relation.page(params[page_key]).per(params[per_key])
table_for(relation, options, &block) + paginate(relation, paging_options)
end
def attribute_table_for(object, options={}, &block)
AttributeTableBuilder.new(object, options, self, &block).render
end
class TableBuilder
attr_reader :relation, :view_context, :options, :columns
delegate :content_tag, :content_tag_for, :link_to, :button_to, :t, :can?, to: :view_context
def initialize(relation, options, view_context)
@relation = relation
@view_context = view_context
@options = options
@columns = []
yield self if block_given?
end
# Add a column to the table
#
# @param attribute [Symbol] optional attribute for the column
# @param options [Hash] options for the column
# @option options [String] :header the header for the column
# @option options [String] :class css class for the column
def column(*args, &block)
options = args.extract_options!
attribute = args.first
if attribute
options[:header] ||= relation.model.human_attribute_name(attribute)
end
Column.new(attribute, options, block).tap do |column|
columns << column
end
end
# Add an column with action links
#
# @param actions [Array] list of actions, possible values: :show, :edit, :destroy
# @param options [Hash] options for the column, same as for #column
# @see #column
def actions(*args)
options = args.extract_options!
actions = args.empty? ? [:show, :edit] : args
options[:class] = ["actions", options[:class]].compact.join(" ")
column(options) do |object|
actions.reduce("".html_safe) do |cell, action|
scope = options[:scope]
scope = scope.respond_to?(:call) ? scope.call(object) : scope
cell << render_single_action(action, object, scope)
end
end
end
def render
header = content_tag(:thead) do
content_tag(:tr, render_row(:th) { |column| column.header })
end
body = relation.empty? ? render_no_results : render_body
table = content_tag(:table, header + body, html_options)
if responsive?
content_tag(:div, table, class: "table-responsive")
else
table
end
end
def render_row(tag)
columns.reduce("".html_safe) do |row, column|
row << content_tag(tag, yield(column), class: column.css_class)
end
end
def render_no_results
defaults = [
".table.#{relation.model_name.i18n_key}.no_results".to_sym,
".table.no_results".to_sym,
"No results"
]
text = t(defaults.shift, default: defaults)
content_tag(:tbody) do
content_tag(:tr) do
content_tag(:td, text, colspan: columns.size)
end
end
end
def render_body
content_tag(:tbody) do
content_tag_for(:tr, relation) do |item|
render_row(:td) do |column|
column.content_for(item, view_context)
end
end
end
end
def render_single_action(action, object, scope)
target = [scope, object].compact
case
when action == :show && can?(:show, object)
link_to t(".table.actions.show.text", default: "View"), target, class: "action show_link"
when action == :edit && can?(:edit, object)
link_to t(".table.actions.edit.text", default: "Edit"), target.dup.unshift(:edit), class: "action edit_link"
when action == :destroy && can?(:destroy, object)
button_to t(".table.actions.destroy.text", default: "Delete"), target,
method: :delete, class: "action destroy_link", form_class: "action_links destroy",
data: {
confirm: t(".table.actions.destroy.confirm", model_name: object.model_name.human.downcase,
default: "Are you sure to delete this #{object.model_name.human.downcase}?")
}
end
end
def html_options
{ class: Rails.configuration.x.table_helper.css_class }.merge(options.fetch(:html, {}))
end
def responsive?
Rails.configuration.x.table_helper.responsive
end
end
class Column
attr_reader :attribute, :options, :block
def initialize(attribute, options, block)
@attribute = attribute
@options = options
@block = block
end
def header
options[:header]
end
def content_for(object, view_context)
if block
view_context.capture(object, &block)
else
object.public_send(attribute)
end
end
def css_class
classes = %w[column]
classes << "column_#{attribute}" if attribute
classes << options[:class]
classes.compact.join(" ")
end
end
class AttributeTableBuilder
attr_reader :object, :options, :view_context, :rows
delegate :content_tag, to: :view_context
def initialize(object, options, view_context)
@object = object
@options = options
@view_context = view_context
@rows = []
yield self if block_given?
end
def row(attribute, options={}, &block)
Row.new(object, attribute, options, block).tap { |row| rows << row }
end
def render
content_tag(:table, html_options) do
content_tag(:tbody) do
rows.reduce("".html_safe) { |body, row| body << render_row(row) }
end
end
end
def render_row(row)
css_class = ["row", "row_#{row.attribute}"].join(" ")
content_tag(:tr, class: row.css_class) do
content_tag(:th, row.header) + content_tag(:td, row.content(view_context))
end
end
def html_options
{ class: Rails.configuration.x.table_helper.css_class }.merge(options.fetch(:html, {}))
end
end
class Row
attr_reader :object, :attribute, :options, :block
def initialize(object, attribute, options, block)
@object = object
@attribute = attribute
@options = options
@block = block
end
def header
options[:header] || object.class.human_attribute_name(attribute)
end
def content(view_context)
case
when block && block.arity == 0
view_context.capture(&block)
when block
view_context.capture(object.public_send(attribute), &block)
else
object.public_send(attribute)
end
end
def css_class
[
"row",
"row_#{attribute}",
options[:class]
].compact.flatten.join(" ")
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment