Last active
December 29, 2017 11:23
-
-
Save tmichel/e2623ba25a35bdb7516a600b2ef5bca4 to your computer and use it in GitHub Desktop.
Very simple table_for and attribute_table_for for Rails
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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