-
Notifications
You must be signed in to change notification settings - Fork 117
Columns
Defines column that will be used to display data.
Example:
class UserGrid
include Datagrid
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 will be used to generate data. In order to create grid that display 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
Column value can be defined by passing a block to Datagrid.column method.
If no block given column it is generated automatically by sending column name method to model.
column(:name) # => asset.name
The block could have no arguments(instance_eval for each asset will be used).
column(:completed) { completed? }
If you don't like instance_eval you can use asset as first argument:
column(:completed) { |asset| asset.completed? }
For the most complicated columns you can also pass datagrid object itself:
filter(:category) do |value|
where("category LIKE '%#{value}%'")
end
column(:exactly_matches_category) do |asset, grid|
asset.category == grid.category
end
Another advanced use case:
class MerchantsGrid
scope { Merchant }
# Dummy filter is not automatically applied to scope
filter(:period, :date, range: true, dummy: true)
column(:number_of_purchases) do |merchant, grid|
merchant.purchases.where(created_at: grid.period).count
end
end
Sometimes you can even combine previously defined columns into new ones:
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
This allows to determine how a column should be selected. Right now it only supports ActiveRecord. If specified, it will add the string to the select. This is useful for data aggregation or to make transformations of the data directly in the database.
column(:count_of_users, 'count(user_id)')
column(:uppercase_name, 'upper(name)')
Note, that you should never specify the AS part, since it's added automatically.
Sometimes you might need a different formatting for column value in CSV and HTML. In this case you can use the following construction:
column(:name) do |asset|
format(asset.name) do |value|
content_tag(:strong, value)
end
end
Now when you render an HTML table you will see <strong>NAME</strong>
While in CSV (or any other non-HTML representation) it won't be wrapped with <strong>
tag.
You can specify if given column should only appear in html view (via the :html option) :
column(:completed, html: true) do |asset|
asset.completed? ? image_tag("green.gif") : image_tag("red.gif")
end
# or do it in partial
column(:actions, html: true) do |asset|
render partial: "admin/assets/actions", object: asset
end
# if you want to hide a column only from html view and have it only in csv export.
column(:id, html: false)
You can enable grid level cache for column values:
self.cached = true
In this way column values will be cached based on models primary keys You can define other cache key when model don't have primary key:
self.cached = proc {|model| model.identifier }
It is also helpful when aggregation queries are made.
Each column supports the following options that is used to specify SQL to sort data by the given column:
-
:order - an order SQL that should be used to sort by this column.
Default: report column name if there is database column with this name. Passing
false
will disable the order for this column. - :order_desc - descending order expression from this column. Default: "#{order} desc".
# Basic use case
column(:group_name, order: "groups.name") { self.group.name }
# Advanced use case
column(
:priority,
# suppose that models with null priority will be always on bottom
order: "priority is not null desc, priority",
order_desc: "priority is not null desc, priority desc"
)
# Disable order
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
In order to specify order, the following attributes are used for Datagrid instance:
- :order - column name to sort with as Symbol. Default: nil.
- :descending - if true descending suffix is added to specified order. Default: false.
UserGrid.new(order: :group, descending: true).assets # => assets ordered by :group column descending
Tip1: To combine :order
optional arguments with the standard filtering parameter, you must specify all the parameters as either a single Hash or (Ruby-2 style) multiple optional arguments with no main argument. Example:
UserGrid.new(order: :group, **(params.fetch(:users_grid, {}).permit!)).assets
Tip2: :order
optional parameter does not accept complicated ordering with a direct SQL statement. If you want to display a datagrid table in a complicated order with a SQL-string when no other sorting options are specified (such as, in fresh viewing of the page), here is an example of accomplishing it in a Controller (tested in Rails-7.0). Here, the condition is: in fresh viewing only, (1) the pinned record with a specific ID should come first like a Twitter timeline, (2) the newest should follow in the order of :created_at
, (3) these order conditions are disabled in any subsequent (user-specified) ordering.
# method index or something in users_controller.rb
grid_params = params.fetch(:users_grid, {}).permit!
@grid = UsersGrid.new(grid_params) do |scope|
if grid_params[:order].blank? # Only when no other orderings are specified
sql = "CASE users.id WHEN #{YourPinnedId} THEN 0 ELSE 1 END, created_at DESC"
scope = scope.order(Arel.sql(sql))
end
scope.page(params[:page])
end
You can specify default options for entire grid by using default_column_options
accessor methods.
They still can be overwritten at column level.
# Disable default order
self.default_column_options = { order: false }
# Makes entire report HTML
self.default_column_options = { html: true }
You are able to show only specific columns in certain context.
The column_names
instance accessor can be used for that:
grid = UsersGrid.new
grid.data # => [["Id", "Name" "Disabled"], [1, "Allan", true], [2, "Bogdan", false]]
grid.column_names = [:id, :name]
grid.data # => [["Id", "Name], [1, "Allan"], [2, "Bogdan"]]
grid.column_names = nil # Reset to default
grid.data # => [["Id", "Name" "Disabled"], [1, "Allan", true], [2, "Bogdan", false]]
There is several column options that helps to control column names filter content
-
:mandatory
- makes column impossible to disable. Hides it fromcolumn_names_filter
selection
When you specify at least one mandatory
column in a grid the column visibility mechanism become different:
- only mandatory columns are displayed by default
- non-mandatory columns need to be explicitly enabled by
column_names
attribute
Example:
class Grid
include Datagrid
scope { User }
column(:id, mandatory: true)
column(:name, mandatory: true)
column(:category)
[:posts, :comments].each do |association|
column(:"number_of_#{association}") do |model|
model.public_send(association).count
end
end
end
grid = Grid.new
grid.data # => [["Id", "Name"], [1, "Bogdan Gusiev"], [2, "Dominic Coryell"]]
grid.column_names = ["category", "number_of_posts"]
grid.data # => [ ["Id", "Name", "Category", "Number of posts],
# [1, "Bogdan Gusiev", "developer", 5],
# [2, "Dominic Coryell", "manager", 3] ]
You can specify :if
and :unless
options to a column, to determine if the column should be shown or not. If a symbol is given, it will call that method on the grid, if a proc is given, it will be called with the grid as an argument.
Example:
Conditional display of columns based on user login:
class MyGrid
scope { }
attr_accessor(:current_user)
column(:secret_data, if: -> { current_user.admin? })
end
g = MyGrid.new(params[:my_grid].merge(current_user: current_user)
g.data
Complex relationship between column display and selected filters:
column(:name, if: :show_name?)
#equivalent:
column(:name, unless: proc {|grid| !grid.show_name? })
# More realistic
filter(:category) do |value|
where("category like '%#{value}%'")
end
column(:exactly_match_category, if: proc {|grid| grid.category.present?}) do |model, grid|
model.category == grid.category
end
In some cases you cannot define columns at class level. So you can define columns on instance level
grid = MyGrid.new
grid.column(:extra_data) do |model|
model.extra_data
end
In this example extra_data
column will not be defined for any other MyGrid
instance in a project. Only for current instance stored in grid
local variable.
It can be used for:
- User-based customizations
- Dynamic columns based data stored in the DB or from third party API
Same behaviour can be achieved by using dynamic
inside of grid class. More live example:
class CampaignsGrid
scope { Campaign }
filter(:account_id, :integer)
def account
Account.find(account_id)
end
dynamic do
account.sales_categories.each do |category|
column(:"sales_in_#{category.name}") do |campaign|
campaign.sales.where(category_id: category.id).sum(:subtotal)
end
end
end
end
Column selection can be available as select[multiple]
or several input[type=checkbox]
in datagrid form.
Use column_names_filter
to reach that behavior.
column_names_filter
accepts same options as :enum
filter.
column_names_filter(header: "Column", checkboxes: true)
In this case column names select
will only contain counter columns. id name category
columns will be always present.
You can manually specify which columns should be selectable by end user:
column_names_filter(select: [:metric_one, :metric_two, :metric_three])
In this way you can hide columns from end user.
Column header can be specified with :header option:
column(:active, header: "Activated")
By default it is generated from column name. Also you can use localization file if you have multilanguage application.
Example: In order to localize column :name in SimpleReport use the key datagrid.simple_report.columns.name
Tip: For internationalization (i18n), it is safer to give a callable object (typically Proc
) to the :header
optional argument like: header: Proc.new{I18n.t("activated")}
. Otherwise, the caching mechanism may mess it up. See also Localization for Filters.
column
accepts a class
optional argument, taking either String or an Array of Strings. It is added to the HTML class
attribute of both <th>
and (every) <td>
of the generated table.
column(:active, class: ["my-css1", "my-css2"])
generates an HTML like
<th class="active my-css1 my-css2">Active
<div class="order"> [...] </div></th>
<!-- -->
<td class="active my-css1 my-css2">.....</td>
where the (String-converted) first argument of column
is used as the default HTML class attribute.
Datagrid tries to be intelligent on preloading:
User.belongs_to :group
class MyGrid
include Datagrid
scope { User }
column(:group) do |user|
user.group.name
end
end
In this case, Datagrid
will automatically preload group
association of a User
because it matches the column name.
You may also specify custom preloading as an option :preload
:
# Custom association name
column(:coupon_codes, preload: :coupons) do |purchase|
purchase.coupons.map(&:code).join(", ")
end
# Custom scope on preload
column(:account_name, preload: { |s| s.includes(:account) })
# Disable automatic preloading
column(:group, preload: false)
This is very useful strategy to specify preloading for each individual columns when columns visibility settings are used and they are not displayed all the time. It will tell datagrid to only preload specific column association when it is visible.
You may use a decorator/presenter/wrapper class around each object from the scope
.
Decorator can be defined like this:
decorate { |user| UserPresenter.new(user) }
# same as:
decorate { UserPresenter }
column(:created_at) do |presenter|
format(presenter.user.created_at) do
presenter.created_at # applies date format through presenter
end
end