# frozen_string_literal: true require "action_view" module Datagrid # # Datagrid Frontend Guide # # ## Description # # The easiest way to start with Datagrid frontend is by using the generator: # # ``` sh # rails generate datagrid:scaffold users # ``` # # This command builds the controller, view, route, and adds # [built-in CSS](https://github.com/bogdan/datagrid/blob/master/app/assets/stylesheets/datagrid.sass). # # Datagrid includes helpers and a form builder for easy frontend generation. # If you need a fully-featured custom GUI, create your templates manually # with the help of the {Datagrid::Columns} API. # # ## Controller and Routing # # Grids usually implement the `index` action of a Rails REST resource. Here's an example: # # resources :models, only: [:index] # # Use the `GET` method in the form, and the controller becomes straightforward: # # class ModelsController < ApplicationController # def index # @grid = ModelsGrid.new(params[:my_report]) do |scope| # scope.page(params[:page]) # See pagination section # end # end # end # # To apply additional scoping conditions, such as visibility based on the current user: # # ModelsGrid.new(params[:my_report]) do |scope| # scope.where(owner_id: current_user.id).page(params[:page]) # end # # To pass an object to a grid instance, define it as an accessible attribute: # # class ModelsGrid # attr_accessor :current_user # end # # Then pass it when initializing the grid: # # ModelsGrid.new(params[:models_grid].merge(current_user: current_user)) # # ## Form Builder # # ### Basic Method # # Use the built-in partial: # # = datagrid_form_with model: @grid, url: report_path, other_form_for_option: value # # {#datagrid_form_with} supports the same options as Rails `form_with`. # # ### Advanced Method # # You can use Rails built-in tools to create a form. # Additionally, Datagrid provides helpers to generate input/select elements for filters: # # ``` haml # - form_with model: UserGrid.new, method: :get, url: users_path do |f| # %div # = f.datagrid_label :name # = f.datagrid_filter :name # => # %div # = f.datagrid_label :category_id # = f.datagrid_filter :category_id # => # ``` # # For more flexibility, use Rails default helpers: # # %div # = f.label :name # = f.text_field :name # # See the localization section of {Datagrid::Filters}. # # ## Datagrid Table # # Use the helper to display a report: # # %div== Total #{@grid.assets.total} # = datagrid_table(@report) # = will_paginate @report.assets # # Options: # # - `:html` - Attributes for the `` tag. # - `:order` - Set to `false` to disable ordering controls (default: `true`). # - `:columns` - Specify an array of column names to display. # # ## Pagination # # Datagrid is abstracted from pagination but integrates seamlessly with tools like Kaminari, WillPaginate, or Pagy: # # # Kaminari # @grid = MyGrid.new(params[:grid]) do |scope| # scope.page(params[:page]).per(10) # end # # # WillPaginate # @grid = MyGrid.new(params[:grid]) do |scope| # scope.page(params[:page]).per_page(10) # end # # # Pagy # @grid = MyGrid.new(params[:grid]) # @pagy, @records = pagy(@grid.assets) # # Render the paginated collection: # # # WillPaginate or Kaminari # <%= datagrid_table(@grid, options) %> # # Pagy # <%= datagrid_table(@grid, @records, options) %> # # ## CSV Export # # Add CSV support to your controller: # # class UsersController < ApplicationController # def index # @grid = UsersGrid.new(params[:users_grid]) # respond_to do |f| # f.html { @grid.scope { |scope| scope.page(params[:page]) } } # f.csv do # send_data @grid.to_csv, type: "text/csv", disposition: 'inline', filename: "grid-#{Time.now.to_s}.csv" # end # end # end # end # # Add a button in your interface: # # link_to "Get CSV", url_for(format: 'csv', users_grid: params[:users_grid]) # # ## AJAX # # Datagrid supports asynchronous data loading. Add this to your controller: # # if request.xhr? # render json: {table: view_context.datagrid_table(@grid)} # end # # Modify the form for AJAX: # # = datagrid_form_with model: @grid, html: {class: 'js-datagrid-form'} # .js-datagrid-table # = datagrid_table @grid # .js-pagination # = paginate @grid.assets # :javascript # $('.js-datagrid-form').submit(function(event) { # event.preventDefault(); # $.get($(this).attr("action"), $(this).serialize(), function (data) { # $('.js-datagrid-table').html(data.table); # }); # }); # # ## Modifying Built-In Partials # # To customize Datagrid views: # # rake datagrid:copy_partials # # This creates files in `app/views/datagrid/`, which you can modify to suit your needs: # # app/views/datagrid/ # ├── _enum_checkboxes.html.erb # datagrid_filter for filter(name, :enum, checkboxes: true) # ├── _form.html.erb # datagrid_form_with # ├── _head.html.erb # datagrid_header # ├── _range_filter.html.erb # datagrid_filter for filter(name, type, range: true) # ├── _row.html.erb # datagrid_rows/datagrid_rows # └── _table.html.erb # datagrid_table # # ## Custom Options # # You can add custom options to Datagrid columns and filters and implement their support on the frontend. # For example, you might want to add a `description` option to a column that appears as a tooltip on mouseover. # # column( # :aov, header: 'AOV', # description: 'Average order value: sum of orders subtotal divided by their count' # ) do |category| # category.orders.sum(:subtotal) / category.orders.count # end # # The `:description` option is not built into Datagrid, but you can implement it # by adding the following to partial `app/views/datagrid/_header.html.erb`: # # <% if column.options[:description] %> # # # # <% end %> # # This modification allows the `:description` tooltip to work with your chosen UI and JavaScript library. # The same technique can be applied to filters by calling `filter.options` in corresponding partials. # # ## Highlight Rows # # To add custom HTML classes to each row for styling, modify the `_row.html.erb` partial: # # ``` diff # - # +"> # <% grid.html_columns(*options[:columns]).each do |column| %> # # <% end %> # ``` # # This allows you to define a custom `row_class` method in your grid class, like this: # # class IssuesGrid < ApplicationGrid # scope { Issue } # # def row_class(issue) # case issue.status # when "fixed" then "green" # when "rejected" then "red" # else "blue" # end # end # end # # ## Localization # # You can overwrite Datagrid’s custom localization keys at the application level. # See the localization keys here: # # https://github.com/bogdan/datagrid/blob/master/lib/datagrid/locale/en.yml module Helper # @param grid [Datagrid::Base] grid object # @param column [Datagrid::Columns::Column, String, Symbol] column name # @param model [Object] an object from grid scope # @return [Object] individual cell value from the given grid, column name and model # @example # def datagrid_value(grid, column, model) column = grid.column_by_name(column) if column.is_a?(String) || column.is_a?(Symbol) grid.html_value(column, self, model) end # @!visibility private def datagrid_format_value(grid, column, model) datagrid_value(grid, column, model) end # Renders html table with columns defined in grid class. # In the most common used you need to pass paginated collection # to datagrid table because datagrid do not have pagination compatibilities: # @param grid [Datagrid::Base] grid object # @param assets [Array] objects from grid scope # @param [Hash{Symbol => Object}] options HTML attributes to be passed to `
# <%= datagrid_value(grid, column, asset) %> #
` tag # @option options [Hash] html A hash of attributes for the `
` tag. # @option options [Boolean] order Whether to generate ordering controls. # If set to `false`, ordering controls are not generated. Default: `true`. # @option options [Array] columns An array of column names to display. # Use this when the same grid class is used in different contexts and requires different columns. # Default: all defined columns. # @option options [String] partials The path for partials lookup. Default: `'datagrid'`. # @return [String] table tag HTML markup # @example # assets = grid.assets.page(params[:page]) # datagrid_table(grid, assets, options) def datagrid_table(grid, assets = grid.assets, **options) _render_partial( "table", options[:partials], { grid: grid, options: options, assets: assets, }, ) end # Renders HTML table header for given grid instance using columns defined in it # # @option options [Boolean] order Whether to display ordering controls built into the header. # Default: `true`. # @option options [Array] columns An array of column names to display. # Use this when the same grid class is used in different contexts and requires different columns. # Default: all defined columns. # @option options [String] partials The path for partials lookup. # Default: `'datagrid'`. # @param grid [Datagrid::Base] grid object # @param [Object] opts (deprecated) pass keyword arguments instead # @param [Hash] options # @return [String] HTML table header tag markup def datagrid_header(grid, opts = :__unspecified__, **options) unless opts == :__unspecified__ Datagrid::Utils.warn_once("datagrid_header now requires ** operator when passing options.") options.reverse_merge!(opts) end options[:order] = true unless options.key?(:order) _render_partial("head", options[:partials], { grid: grid, options: options },) end # Renders HTML table rows using given grid definition using columns defined in it. # Allows to provide a custom layout for each for in place with a block # # @option options [Array] columns An array of column names to display. # Use this when the same grid class is used in different contexts and requires different columns. # Default: all defined columns. # @option options [String] partials The path for partials lookup. # Default: `'datagrid'`. # @return [String] # @example Generic table rows Layout # = datagrid_rows(grid) # @example Custom Layout # = datagrid_rows(grid) do |row| # %tr # %td= row.project_name # %td.project-status{class: row.status}= row.status # @param [Datagrid::Base] grid datagrid object # @param [Array] assets assets as per defined in grid scope def datagrid_rows(grid, assets = grid.assets, **options, &block) safe_join( assets.map do |asset| datagrid_row(grid, asset, **options, &block) end.to_a, ) end # @return [String] renders ordering controls for the given column name # @option options [String] partials The path for partials lookup. # Default: `'datagrid'`. # @param [Datagrid::Base] grid datagrid object # @param [Datagrid::Columns::Column] column # @deprecated Put necessary code inline inside datagrid/head partial. # See built-in partial for example. def datagrid_order_for(grid, column, options = {}) Datagrid::Utils.warn_once(<<~MSG) datagrid_order_for is deprecated. Put necessary code inline inside datagrid/head partial. See built-in partial for example. MSG _render_partial("order_for", options[:partials], { grid: grid, column: column },) end # Renders HTML for grid with all filters inputs and labels defined in it # @option options [String] partials Path for form partial lookup. # Default: `'datagrid'`, which uses `app/views/datagrid/` partials. # Example: `'datagrid_admin'` uses `app/views/datagrid_admin` partials. # @option options [Datagrid::Base] model a Datagrid object to be rendered. # @option options [Hash] All options supported by Rails `form_with` helper. # @param [Hash{Symbol => Object}] options # @return [String] form HTML tag markup def datagrid_form_with(**options) raise ArgumentError, "datagrid_form_with block argument is invalid. Use form_with instead." if block_given? grid = options[:model] raise ArgumentError, "Grid has no available filters" if grid&.filters&.empty? _render_partial("form", options[:partials], { grid: options[:model], options: options }) end # Renders HTML for grid with all filters inputs and labels defined in it # # Supported options: # # * :partials - Path for form partial lookup. # Default: 'datagrid'. # * All options supported by Rails form_with helper # @deprecated Use {#datagrid_form_with} instead. # @param grid [Datagrid::Base] grid object # @param [Hash] options # @return [String] form HTML tag markup def datagrid_form_for(grid, options = {}) Datagrid::Utils.warn_once("datagrid_form_for is deprecated if favor of datagrid_form_with.") _render_partial( "form", options[:partials], grid: grid, options: { method: :get, as: grid.param_name, local: true, **options, }, ) end # Provides access to datagrid columns data. # Used in case you want to build html table completelly manually # @param grid [Datagrid::Base] grid object # @param asset [Object] object from grid scope # @param block [Proc] block with Datagrid::Helper::HtmlRow as an argument returning a HTML markup as a String # @param [Hash{Symbol => Object}] options # @return [Datagrid::Helper::HtmlRow, String] captured HTML markup if block given otherwise row object # @example Render default layout for row # <%= datagrid_row(grid, user, columns: [:first_name, :last_name, :actions]) %> # @example Rendering custom layout for `first_name` and `last_name` columns # <%= datagrid_row(grid, user) do |row| %> # # # # # <% end %> # @example Rendering custom layout passing a block # <% row = datagrid_row(grid, user) %> # First Name: <%= row.first_name %> # Last Name: <%= row.last_name %> def datagrid_row(grid, asset, **options, &block) Datagrid::Helper::HtmlRow.new(self, grid, asset, options).tap do |row| return capture(row, &block) if block_given? end end # Generates an ascending or descending order url for the given column # @param grid [Datagrid::Base] grid object # @param column [Datagrid::Columns::Column, String, Symbol] column name # @param descending [Boolean] order direction, descending if true, otherwise ascending. # @return [String] order layout HTML markup def datagrid_order_path(grid, column, descending) column = grid.column_by_name(column) query = request&.query_parameters || {} ActionDispatch::Http::URL.path_for( path: request&.path || "/", params: query.merge(grid.query_params(order: column.name, descending: descending)), ) end # @!visibility private def datagrid_column_classes(grid, column) Datagrid::Utils.warn_once(<<~MSG) datagrid_column_classes is deprecated. Assign necessary classes manually. Correspond to default datagrid/rows partial for example.) MSG column = grid.column_by_name(column) order_class = if grid.ordered_by?(column) ["ordered", grid.descending ? "desc" : "asc"] end class_names(column.name, order_class, column.options[:class], column.tag_options[:class]) end protected def _render_partial(partial_name, partials_path, locals = {}) render({ partial: File.join(partials_path || "datagrid", partial_name), locals: locals, }) end # Represents a datagrid row that provides access to column values for the given asset # @example # row = datagrid_row(grid, user) # row.class # => Datagrid::Helper::HtmlRow # row.first_name # => "Bogdan" # row.grid # => Datagrid::Base object # row.asset # => User object # row.each do |value| # puts value # end class HtmlRow include Enumerable attr_reader :grid, :asset, :options # @!visibility private def initialize(renderer, grid, asset, options) @renderer = renderer @grid = grid @asset = asset @options = options end # @return [Object] a column value for given column name def get(column) @renderer.datagrid_value(@grid, column, @asset) end # Iterates over all column values that are available in the row # param block [Proc] column value iterator def each(&block) (@options[:columns] || @grid.html_columns).each do |column| block.call(get(column)) end end # @return [String] HTML row format def to_s @renderer.send(:_render_partial, "row", options[:partials], { grid: grid, options: options, asset: asset, },) end protected def method_missing(method, *args, &blk) if (column = @grid.column_by_name(method)) get(column) else super end end def respond_to_missing?(method, include_private = false) !!@grid.column_by_name(method) || super end end end end
<%= row.first_name %><%= row.last_name %>