Skip to content
Bogdan Gusiev edited this page Jan 7, 2024 · 63 revisions

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

Column value can be defined by passing a block to Datagrid.column method.

Basic column value

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? }

Advanced column value

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

Using database expression

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.

HTML Columns

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)

Column Value cache (>= 1.3.0)

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.

Ordering

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

Default column options

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 }   

Columns visibility

Instance API

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 from column_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] ]

If and Unless options

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

Dynamic Columns

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

Frontend

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.

Localization

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.

Styling

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.

Preload (eager loading) of associations

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.

Decorator

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