module Datagrid::Columns
Defines a column to be used for displaying data in a Datagrid
.
class UserGrid < ApplicationGrid scope do User.order("users.created_at desc").joins(:group) end column(:name) column(:group, order: "groups.name") do self.group.name end column(:active, header: "Activated") do |user| !user.disabled end end
Each column is used to generate data for the grid.
To create a grid displaying all users:
grid = UserGrid.new grid.header # => ["Group", "Name", "Disabled"] grid.rows # => [ # ["Steve", "Spammers", true], # ["John", "Spoilers", true], # ["Berry", "Good people", false] # ] grid.data # => Header & Rows grid.data_hash # => [ # { name: "Steve", group: "Spammers", active: true }, # { name: "John", group: "Spoilers", active: true }, # { name: "Berry", group: "Good people", active: false }, # ] }
## Column
Value
The value of a column can be defined by passing a block to ‘Datagrid.column`.
### Basic Column
Value
If no block is provided, the column value is generated automatically by sending the column name method to the model.
column(:name) # => asset.name
Using instance_eval
:
column(:completed) { completed? }
Using the asset as an argument:
column(:completed) { |asset| asset.completed? }
### Advanced Column
Value
You can also pass the Datagrid
object itself to define more complex column values.
Using filters with columns:
filter(:category) do |value| where("category LIKE '%#{value}%'") end column(:exactly_matches_category) do |asset, grid| asset.category == grid.category end
Combining columns:
column(:total_sales) do |merchant| merchant.purchases.sum(:subtotal) end column(:number_of_sales) do |merchant| merchant.purchases.count end column(:average_order_value) do |_, _, row| row.total_sales / row.number_of_sales end
## Using Database Expressions
Columns
can use database expressions to directly manipulate data in the database.
column(:count_of_users, 'count(user_id)') column(:uppercase_name, 'upper(name)')
## HTML Columns
Columns
can have different formats for HTML and non-HTML representations.
column(:name) do |asset| format(asset.name) do |value| content_tag(:strong, value) end end
## Column
Value Cache
Enables grid-level caching for column values.
self.cached = true
## Ordering
Columns
can specify SQL ordering expressions using the ‘:order` and `:order_desc` options.
Basic ordering:
column(:group_name, order: "groups.name") { self.group.name }
In example above descending order is automatically inherited from ascending order specified. When such default is not enough, specify both options together:
column( :priority, # models with null priority are always on bottom # no matter if sorting ascending or descending order: "priority is not null desc, priority", order_desc: "priority is not null desc, priority desc" )
Disable order like this:
column(:title, order: false)
Order by joined table Allows to join specified table only when order is enabled for performance:
column(:profile_updated_at, order: proc { |scope| scope.joins(:profile).order("profiles.updated_at") }) do |model| model.profile.updated_at.to_date end
Order by a calculated value
column( :duration_request, order: "(requests.finished_at - requests.accepted_at)" ) do |model| Time.at(model.finished_at - model.accepted_at).strftime("%H:%M:%S") end
## Default Column
Options
Default options for all columns in a grid can be set using ‘default_column_options`.
self.default_column_options = { order: false }
It can also accept a proc with the column instance as an argument:
self.default_column_options = ->(column) { { order: column.name == :id } }
## Columns
Visibility
Columns
can be dynamically shown or hidden based on the grid’s ‘column_names` accessor.
grid.column_names = [:id, :name]
## Dynamic Columns
Columns
can be defined dynamically on a grid instance or based on data.
Adding a dynamic column:
grid.column(:extra_data) do |model| model.extra_data end
## Localization
Column
headers can be localized using the ‘:header` option or through i18n files.
column(:active, header: Proc.new { I18n.t("activated") })
## Preloading Associations
Preload database associations for better performance.
Automatic association preloading:
column(:group) do |user| user.group.name end
Custom preloading:
column(:account_name, preload: { |s| s.includes(:account) })
## Decorator
A decorator or presenter class can be used around each object in the ‘scope`.
decorate { UserPresenter } column(:created_at) do |presenter| presenter.user.created_at end
Public Class Methods
Source
# File lib/datagrid/columns.rb, line 238 def self.included(base) base.extend ClassMethods base.class_eval do include Datagrid::Core class_attribute :default_column_options, instance_writer: false, default: {} class_attribute :batch_size, default: 1000 class_attribute :columns_array, default: [] class_attribute :cached, default: false class_attribute :decorator, instance_writer: false end end
@visibility private @param [Object] base
Source
# File lib/datagrid/columns.rb, line 579 def initialize(*) self.columns_array = self.class.columns_array.clone super end
@!visibility private
Datagrid::Core::new
Public Instance Methods
Source
# File lib/datagrid/columns.rb, line 399 def assets append_column_preload( driver.append_column_queries( super, columns.select(&:query), ), ) end
@!visibility private
Datagrid::Core#assets
Source
# File lib/datagrid/columns.rb, line 601 def available_columns columns_array.select do |column| column.enabled?(self) end end
@return [Array<Datagrid::Columns::Column>] all columns
that are possible to be displayed for the current grid object
@example
class MyGrid filter(:search) {|scope, value| scope.full_text_search(value)} column(:id) column(:name, mandatory: true) column(:search_match, if: proc {|grid| grid.search.present? }) do |model, grid| search_match_line(model.searchable_content, grid.search) end end grid = MyGrid.new grid.columns # => [ #<Column:name> ] grid.available_columns # => [ #<Column:id>, #<Column:name> ] grid.search = "keyword" grid.available_columns # => [ #<Column:id>, #<Column:name>, #<Column:search_match> ]
Source
# File lib/datagrid/columns.rb, line 574 def column(name, query = nil, **options, &block) self.class.define_column(columns_array, name, query, **options, &block) end
Defines a column at instance level @see Datagrid::Columns::ClassMethods#column
Source
# File lib/datagrid/columns.rb, line 526 def column_by_name(name) self.class.find_column_by_name(columns_array, name) end
Finds a column by name @param name [String, Symbol] column name to be found @return [Datagrid::Columns::Column, nil]
Source
# File lib/datagrid/columns.rb, line 500 def columns(*column_names, data: false, html: false) self.class.filter_columns( columns_array, *column_names, data: data, html: html, ).select do |column| column.enabled?(self) end end
@param column_names [Array<Symbol, String>] @param [Boolean] data return only data columns @param [Boolean] html return only HTML columns @return [Array<Datagrid::Columns::Column>] all columns selected in grid instance @example
MyGrid.new.columns # => all defined columns grid = MyGrid.new(column_names: [:id, :name]) grid.columns # => id and name columns grid.columns(:id, :category) # => id and category column
Source
# File lib/datagrid/columns.rb, line 447 def data(*column_names) rows(*column_names).unshift(header(*column_names)) end
@param column_names [Array<String, Symbol>] list of column names
if you want to limit data only to specified columns.
@return [Array<Array<Object>>] data for each row in datagrid assets with header.
Source
# File lib/datagrid/columns.rb, line 512 def data_columns(*column_names, html: false) columns(*column_names, html: html, data: true) end
@param column_names [Array<String, Symbol>] list of column names
if you want to limit data only to specified columns
@param [Boolean] html return only HTML columns @return [Array<Datagrid::Columns::Column>] columns that can be represented in plain data(non-html) way
Source
# File lib/datagrid/columns.rb, line 464 def data_hash map_with_batches do |asset| hash_for(asset) end end
@return [Array<Hash{Symbol => Object}>] an array of hashes representing the rows in the filtered datagrid relation
@example
class MyGrid scope { Model } column(:id) column(:name) end Model.create!(name: "One") Model.create!(name: "Two") MyGrid.new.data_hash # => [{name: "One"}, {name: "Two"}]
Source
# File lib/datagrid/columns.rb, line 568 def data_row(asset) ::Datagrid::Columns::DataRow.new(self, asset) end
@param [Object] asset one of the assets from grid scope @return [Datagrid::Columns::DataRow] an object representing a grid row. @example
class MyGrid scope { User } column(:id) column(:name) column(:number_of_purchases) do |user| user.purchases.count end end row = MyGrid.new.data_row(User.last) row.id # => user.id row.number_of_purchases # => user.purchases.count
Source
# File lib/datagrid/columns.rb, line 610 def data_value(column_name, asset) column = column_by_name(column_name) cache(column, asset, :data_value) do raise "no data value for #{column.name} column" unless column.data? result = generic_value(column, asset) result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_data : result end end
@param [String,Symbol] column_name column name @param [Object] asset one of the assets from grid scope @return [Object] a cell data value for given column name and asset
Source
# File lib/datagrid/columns.rb, line 638 def decorate(model) self.class.decorate(model) end
@param [Object] model one of the assets from grid scope @return [Object] a decorated version of given model if decorator is specified or the model otherwise.
Source
# File lib/datagrid/columns.rb, line 544 def format(value, &block) if block self.class.format(value, &block) else # don't override Object#format method super end end
Gives ability to have a different formatting for CSV and HTML column value. @example Formating using Rails view helpers
column(:name) do |model| format(model.name) do |value| tag.strong(value) end end
@example Formatting using a separated view template
column(:company) do |model| format(model.company.name) do render partial: "companies/company_with_logo", locals: {company: model.company } end end
@return [Datagrid::Columns::Column::ResponseFormat] Format object
Source
# File lib/datagrid/columns.rb, line 643 def generic_value(column, model) cache(column, model, :generic_value) do presenter = decorate(model) unless column.enabled?(self) raise Datagrid::ColumnUnavailableError, "Column #{column.name} disabled for #{inspect}" end if column.data_block.arity >= 1 Datagrid::Utils.apply_args(presenter, self, data_row(model), &column.data_block) else presenter.instance_eval(&column.data_block) end end end
@!visibility private
Source
# File lib/datagrid/columns.rb, line 427 def hash_for(asset) result = {} data_columns.each do |column| result[column.name] = data_value(column, asset) end result end
@param asset [Object] asset from datagrid scope @return [Hash] A mapping where keys are column names and
values are column values for the given asset
Source
# File lib/datagrid/columns.rb, line 410 def header(*column_names) data_columns(*column_names).map(&:header) end
@param column_names [Array<String, Symbol>] list of column names
if you want to limit data only to specified columns
@return [Array<String>] human readable column names. See also “Localization” section
Source
# File lib/datagrid/columns.rb, line 519 def html_columns(*column_names, data: false) columns(*column_names, data: data, html: true) end
@param column_names [Array<String>] list of column names if you want to limit data only to specified columns @param [Boolean] data return only data columns @return all columns that can be represented in HTML table
Source
# File lib/datagrid/columns.rb, line 624 def html_value(column_name, context, asset) column = column_by_name(column_name) cache(column, asset, :html_value) do if column.html? && column.html_block value_from_html_block(context, asset, column) else result = generic_value(column, asset) result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_html(context) : result end end end
@param [String,Symbol] column_name column name @param [Object] asset one of the assets from grid scope @param [ActionView::Base] context view context object @return [Object] a cell HTML value for given column name and asset and view context
Source
# File lib/datagrid/columns.rb, line 659 def reset super @cache = {} end
@!visibility private
Datagrid::Core#reset
Source
# File lib/datagrid/columns.rb, line 418 def row_for(asset, *column_names) data_columns(*column_names).map do |column| data_value(column, asset) end end
@param asset [Object] asset from datagrid scope @param column_names [Array<String, Symbol>] list of column names
if you want to limit data only to specified columns
@return [Array<Object>] column values for given asset
Source
# File lib/datagrid/columns.rb, line 438 def rows(*column_names) map_with_batches do |asset| row_for(asset, *column_names) end end
@param column_names [Array<String,Symbol>] list of column names
if you want to limit data only to specified columns
@return [Array<Array<Object>>] with data for each row in datagrid assets without header
Source
# File lib/datagrid/columns.rb, line 478 def to_csv(*column_names, **options) require "csv" CSV.generate( headers: header(*column_names), write_headers: true, **options, ) do |csv| each_with_batches do |asset| csv << row_for(asset, *column_names) end end end
@param column_names [Array<String,Symbol>] @param options [Hash] CSV generation options @return [String] a CSV representation of the data in the grid
@example
grid.to_csv grid.to_csv(:id, :name) grid.to_csv(col_sep: ';')
Protected Instance Methods
Source
# File lib/datagrid/columns.rb, line 666 def append_column_preload(relation) columns.inject(relation) do |current, column| column.append_preload(current) end end
Source
# File lib/datagrid/columns.rb, line 672 def cache(column, asset, type) @cache ||= {} unless cached? @cache.clear return yield end key = cache_key(asset) raise(Datagrid::CacheKeyError, "Datagrid Cache key is #{key.inspect} for #{asset.inspect}.") unless key @cache[column.name] ||= {} @cache[column.name][key] ||= {} @cache[column.name][key][type] ||= yield end
Source
# File lib/datagrid/columns.rb, line 686 def cache_key(asset) if cached.respond_to?(:call) cached.call(asset) else driver.default_cache_key(asset) end rescue NotImplementedError raise Datagrid::ConfigurationError, <<~MSG #{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}. MSG end
Source
# File lib/datagrid/columns.rb, line 707 def each_with_batches(&block) if batch_size&.positive? driver.batch_each(assets, batch_size, &block) else assets.each(&block) end end
Source
# File lib/datagrid/columns.rb, line 699 def map_with_batches(&block) result = [] each_with_batches do |asset| result << block.call(asset) end result end
Source
# File lib/datagrid/columns.rb, line 715 def value_from_html_block(context, asset, column) args = [] remaining_arity = column.html_block.arity remaining_arity = 1 if remaining_arity.negative? asset = decorate(asset) if column.data? args << data_value(column, asset) remaining_arity -= 1 end args << asset if remaining_arity.positive? args << self if remaining_arity > 1 context.instance_exec(*args, &column.html_block) end