WiceGrid

Version

3.0.2

Author

Yuri Leikind

Homepage

leikind.org/pages/wicegrid

Examples online

grid.leikind.org

News

leikind.org/wicegrid

Email

“Yuri Leikind” <yuri.leikind at gmail dot com>

Attention!

There are two major branches of WiceGrid.

THIS IS A VERSION OF THE PLUGIN FOR RAILS 3.0. The current branch (github.com/leikind/wice_grid) starts with version 3.0, works with Rails 3, and can only be used as a gem.

For Rails 2 WiceGrid only works as a Rails plugin. See WiceGrid version 0.6 (github.com/leikind/wice_grid/tree/master).

Intro

WiceGrid is a Rails grid plugin.

One of the goals of this plugin was to allow the programmer to define the contents of the cell by himself, just like one does when rendering a collection via a simple table (and this is what differentiates WiceGrid from various scaffolding solutions), but automate implementation of filters, ordering, paginations, CSV export, and so on. Ruby blocks provide an elegant means for this.

WiceGrid builds the call to the ActiveRecord layer for you and creates a table view with the results of the call including:

  • paging

  • sortable columns

  • filtering by multiple columns

  • CSV export

  • saving queries

All working nicely together. Filters are added automatically according to the type of the underlying DB column. Filtering by more than one column at the same time is possible. More than one such grid can appear on a page, and manipulations with one grid do not have any impact on the other.

WiceGrid does not take a collection as an input, it works directly with ActiveRecord (with the help of will_paginate[http://github.com/mislav/will_paginate/wikis]).

WiceGrid does not use AJAX calls to reload itself, instead simple GET requests are used for this, nevertheless, all other page parameters are respected and preserved. WiceGrid views do not contain forms so you can include it in your own forms.

There are two major branches of WiceGrid.

The current branch (github.com/leikind/wice_grid/tree/rails3) starts with version 3.0, works with Rails 3, and can only be used as a gem.

For Rails 2 WiceGrid only works as a Rails plugin. See WiceGrid version 0.6 (github.com/leikind/wice_grid/).

WiceGrid is known to work with MySQL and Postgres.

WiceGrid works with Prototype and jQuery

Examples

This tutorial is accompanied by a sample application with WiceGrid examples which you can browse online ( grid.leikind.org ), or just view the code ( github.com/leikind/wice_grid_examples ).

Requirements

Rails version 3.0 or newer.

will_paginate[http://github.com/mislav/will_paginate/wikis] version 3.0.pre2 or newer.

Prototype[http://www.prototypejs.org] version 1.5.1 or newer, or jQuery[http://jquery.com/].

How-To

Installation

Add the following to your Gemfile:

gem "wice_grid", '3.0.1'

and run the bundle command.

If your application is Prototype-based , run the following generator to copy all plugin assets:

rails g wice_grid:wice_grid_assets_prototype

For jQuery:

rails g wice_grid:wice_grid_assets_jquery

This will copy images, stylesheets, configuration file config/initializers/wice_grid_config.rb, and the correct version of javascript files.

Support for Prototype and jQuery

WiceGrid started as a plugin using the Prototype javascript framework. Support for jQuery was added for version 0.6. The plugin contains two versions of the main javascript file wice_grid.js, and depending on the generator run to install the assets (wice_grid_assets_prototype or wice_grid_assets_jquery) the correct file is copied into public/javascripts.

Some javascript code is also generated and injected into the HTML page. The value of constant Wice::Defaults::JS_FRAMEWORK in configuration file wice_grid_config.rb defines for which JS framework code should be generated. Generators wice_grid_assets_prototype and wice_grid_assets_jquery create a version of wice_grid_config.rb with the corresponding value of Wice::Defaults::JS_FRAMEWORK.

In the Prototype mode the plugin uses a forked version of Calendarview. It is bundled with the plugin and the generator task wice_grid_assets_prototype will install al necessary assets.

The jQuery version uses jQuery datepicker. Because this is part of the standard jQuery codebase, it is not bundled together with the plugin, and it is the responsibility of the programmer to include all necessary assets including localization files if the application is multilingual.

It is always possible to fall back to simple dropdown lists that the standard date/datetime Rails helpers use.

JQUERY DATEPICKER IS ONLY USED FOR DATE FILTERS AT THE MOMENT. DATETIME FILTERS FALL BACK TO THE DEFAULT RAILS DATE/DATETIME HELPERS.

Basics

The Prototype version of WiceGrid requires prototype.js and effects.js in order to function. The jQuery version of WiceGrid requires jquery.js, the Datepicker widget from jQuery UI, and the Highlight and Fade effects for the saved queries functionality.

Thus, make sure you require all necessary JS assets in your layout template.

To include WiceGrid javascript and stylesheet files to the page use helper include_wice_grid_assets :

<%= include_wice_grid_assets %>

The default behavior of include_wice_grid_assets is a bit magical in the sense that it produces no output if the page contains no grids, so don’t panic if you see no WiceGrid includes. If you prefer to always have WiceGrid stylesheet and javascript, use

<%= include_wice_grid_assets :load_on_demand => false %>

The simplest example of a WiceGrid for one simple DB table called ApplicationAccount is the following:

Controller:

@tasks_grid = initialize_grid(Task)

It is also possible to use an ActiveRecord::Relation instance as the first argument:

@tasks_grid = initialize_grid(Task.where(:active => true))

View:

<%= grid(@tasks_grid) do |g|

  g.column do |task|
    task.id
  end

  g.column  do |task|
    task.title
  end

  g.column do |task|
    task.description
  end

  g.column do |task|
    task.archived? ? 'Yes' : 'No'
  end

  g.column do |task|
    link_to('Edit', edit_task_path(task))
  end
end -%>

Code g.column do |task| ... end defines everything related to a column in the resulting view table including column names, sorting, filtering, the content of the column cells, etc. The return value of the block is the table cell content.

In the above view code five columns were defined, all without names, no sorting or filtering is available. Still, pagination becomes active if the number of all extracted records exceeds the default number of rows per page.

Column names are defined with parameter :column_name:

<%= grid(@tasks_grid) do |g|

  g.column :column_name => 'ID' do |task|
    task.id
  end

  g.column :column_name => 'Title'  do |task|
    task.title
  end

  g.column  :column_name => 'Description' do |task|
    task.description
  end

  g.column  :column_name => 'Archived' do |task|
    task.archived? ? 'Yes' : 'No'
  end

  g.column   do |task|
    link_to('Edit', edit_task_path(task))
  end
end -%>

To add filtering and ordering, declare to which column in the underlying database table(s) the view column corresponds using parameter :attribute_name :

<%= grid(@tasks_grid) do |g|

  g.column :column_name => 'ID', :attribute_name => 'id' do |task|
    task.id
  end

  g.column :column_name => 'Title', :attribute_name => 'title'  do |task|
    task.title
  end

  g.column  :column_name => 'Description', :attribute_name => 'description' do |task|
    task.description
  end

  g.column  :column_name => 'Archived', :attribute_name => 'archived' do |task|
    task.archived? ? 'Yes' : 'No'
  end

  g.column   do |task|
    link_to('Edit', edit_task_path(task))
  end
end -%>

This will add sorting links and filters for columns Username and Active. The plugin automatically creates filters according to the type of the database column. In the above example a text field will be created for column Title (title is a string), for column Archived a dropdown filter will be created with options ‘Yes’, ‘No’, and ‘–’, and for the integer ID two short text fields are added which can contain the numeric range (more than, less than).

It is important to remember that :attribute_name is the name of the database column, not a model attribute. Of course, all database columns have corresponding model attributes, but not all model attributes map to columns in the same table with the same name.

Read more about available filters in the documentation for the column method. Read the section about custom dropdown filters for more advanced filters.

For columns like

g.column :column_name => 'Title', :attribute_name => 'title'  do |task|
  task.title
end

where the block contains just a call to the same attribute declared by :attribute_name, the block can be omitted:

<%= grid(@tasks_grid) do |g|

  g.column :column_name => 'ID', :attribute_name => 'id'

  g.column :column_name => 'Title', :attribute_name => 'title'

  g.column  :column_name => 'Description', :attribute_name => 'description'

  g.column  :column_name => 'Archived', :attribute_name => 'archived' do |task|
    task.archived? ? 'Yes' : 'No'
  end

  g.column   do |task|
    link_to('Edit', edit_task_path(task))
  end
end -%>

In this case attribute_name will be used as the method name to send to the ActiveRecord instance.

If only sorting is needed, we can turn off filters using :no_filter :

g.column :column_name => 'ID', :attribute_name => 'id', :no_filter => true

If no ordering links are needed, use :allow_ordering:

g.column :column_name => 'Added', :attribute_name => 'created_at', :allow_ordering => false

It is important to understand that it is up to the developer to make sure that the value returned by a column block (the content of a cell) corresponds to the underlying database column specified by :attribute_name (and :model_class discussed below).

Rendering filter panel

The filter panel can be shown and hidden clicking the icon with binoculars.

The way the filter panel is shown after the page is loaded is controlled via parameter :show_filters of the grid helper. Possible values are:

  • :when_filtered - the filter is shown when the current table is the result of filtering

  • :always - show the filter always

  • :no - never show the filter

Example:

<%= grid(@tasks_grid, :show_filters => :always) do |g|
  ......
end -%>

Filter related icons (filter icon, reset icon, show/hide icon) are placed in the header of the last column if it doesn’t have any filter or a column name, otherwise an additional table column is added. To always place the icons in the additional column, set Wice::Defaults::REUSE_LAST_COLUMN_FOR_FILTER_ICONS to false in the configuration file.

Initial Ordering

Initializing the grid we can also define the column by which the record will be ordered on the first rendering of the grid, when the user has not set their ordering setting by clicking the column label, and the order direction:

@tasks_grid = initialize_grid(Task,
  :order => 'tasks.title',
  :order_direction => 'desc'
)

Records Per Page

The number of rows per page is set with :per_page:

@tasks_grid = initialize_grid(Task, :per_page => 40)

Conditions

The initialize_grid method supports a :conditions parameter which is passed on to the underlying ActiveRecord (via will_paginate), so it can be in any format processable by ActiveRecord:

@tasks_grid = initialize_grid(Task,
  :conditions => ["archived = false and estimated_time > ?", 100]
)

@tasks_grid = initialize_grid(Task,
  :include => :project,
  :conditions => {:archived => false, :project => {:active => true}}
)

A good example is substituting a common pattern like

@user_groups = @portal_application.user_groups

with WiceGrid code:

@user_groups_grid = initialize_grid(UserGroup, :conditions => ['portal_application_id = ?', @portal_application])

Alternatively, instead of a Class object as the first parameter, you can use ActiveRecord::Relation:

@tasks_grid = initialize_grid(Task.
  where(:archived => false, :projects => {:active => true}).
  joins(:project)
)

Please note that though all queries inside of WiceGrid are run without the default scope, if you use an ActiveRecord::Relation instance to initialize grid, it will already include the default scope. Thus you might consider using unscoped:

@tasks_grid = initialize_grid(Task.unscoped.
  where(:archived => false, :projects => {:active => true}).
  joins(:project)
)

Queries with join tables

WiceGrid also supports ActiveRecord’s :joins and :include.

@products_grid = initialize_grid(Product,
  :include => :category,
  :order => 'products.name',
  :per_page => 20)

Note that if we want to order initially by a column from a joined table we have to specify the table and the column name with the sql dot notation, that is, products.name

To show columns of joined tables in the view table, the ActiveRecord model class name has to be specified, that corresponds to the joined table:

<%= grid(@products_grid) do |g|
  g.column :column_name => 'Product Name', :attribute_name => 'name' do |product|  # primary table
    link_to(product.name, product_path(product))
  end

  g.column :column_name => 'Category', :attribute_name => 'name', :model_class => Category |product| # joined table
    product.category.name
  end
%>

Please note that the blockless definition of the column only works with columns from the main table and it won’t work with columns with :model_class

Joined associations referring to the same table

In case there are two joined associations both referring to the same table, ActiveRecord constructs a query where the second join provides an alias for the joined table. To enable WiceGrid to order and filter by columns belonging to different associations but originating from the same table, set :table_alias to this alias:

Model:

class Project < ActiveRecord::Base
  belongs_to :customer, :class_name => 'Company'
  belongs_to :supplier, :class_name => 'Company'
end

Controller:

@projects_grid = initialize_grid(Project, :include => [:customer, :supplier] )

View:

<%= grid(@projects_grid, :show_filters => :always) do |g|

  g.column :column_name => 'Project Name', :attribute_name => 'name'

  g.column  :column_name => 'Customer company', :model_class => 'Company', :attribute_name => 'name' do |task|
    task.customer.name if task.customer
  end

  g.column  :column_name => 'Supplier company', :model_class => 'Company', :attribute_name => 'name', :table_alias => ' suppliers_projects' do |task|
    task.supplier.name if task.supplier
  end

end -%>

More than one grid on a page

It is possible to use more that one grid on a page, each with its own state. To do so, you must specify the name of the grid in initialize_grid using parameter :name

The name serves as the base name for HTTP parameters, DOM IDs, etc, so it is important that all grids on a page have different names. The default name is ‘grid’.

The name can only contain alphanumeric characters.

@projects_grid = initialize_grid(Project, :name => 'g1' )
@tasks_grid = initialize_grid(Task, :name => 'g2')

Custom Ordering

It is possible to change the way results are ordered injecting a piece of SQL code, for example, use ORDER BY INET_ATON(ip_address) instead of ORDER BY ip_address.

To do so, provide parameter :custom_order in the initialization of the grid with a hash where keys are fully qualified names of database columns, and values the required chunks of SQL to use in the ORDER BY clause.

For example:

@hosts_grid = initialize_grid(Host,
  :custom_order => {
    'hosts.ip_address' => 'INET_ATON(hosts.ip_address)'
  })

It is possible to use the ‘?’ character instead of the name of the column in the hash value:

@hosts_grid = initialize_grid(Host,
  :custom_order => {
    'hosts.ip_address' => 'INET_ATON( ? )'
  })

Values can also be Proc objects. The parameter supplied to such a Proc object is the name of the column:

@hosts_grid = initialize_grid(Host,
  :custom_order => {
    'hosts.ip_address' => lambda{|f| "INET_ATON( #{f} )"}
  })

Custom dropdown filters

It is possible to construct custom dropdown filters. Depending on the value of column parameter:custom_filter different modes are available:

Array of two-element arrays or a hash

An array of two-element arrays or a hash are semantically identical ways of creating a custom filter.

Every first item of the two-element array is used for the label of the select option while the second element is the value of the select option. In case of a hash the keys become the labels of the generated dropdown list, while the values will be values of options of the dropdown list:

g.column :column_name => 'Status', :attribute_name => 'status',
         :custom_filter => {'Development' => 'development', 'Testing' => 'testing', 'Production' => 'production'}

g.column :column_name => 'Status', :attribute_name => 'status',
        :custom_filter => [['Development', 'development'], ['Testing', 'testing'], ['Production', 'production']]

It is also possible to submit a array of strings or numbers, in this case every item will be used both as the value of the select option and as its label:

g.column :column_name => 'Status', :attribute_name => 'status',
         :custom_filter => ['development', 'testing', 'production']

:auto

:auto - a powerful option which populates the dropdown list with all unique values of the column specified by :attribute_name and :model_class.

g.column :column_name => 'Status', :attribute_name => 'status',
         :custom_filter => ['development', 'testing', 'production']

Note that in the above example all names of all possible categories will appear even if they don’t appear in the current resultset. To only show those which do appear in the resutset, use an array of symbol messages (see section ‘An array of symbols’).

Custom filters and associations (joined tables)

In most cases custom fields are required for one-to-many and many-to-many associations.

To correctly build a filter condition foreign keys have to be matched.

For example, if there is a column:

g.column :column_name => 'Project Name', :attribute_name => 'name', :model_class => 'Project' do |task|
  task.project.name if task.project
end

adding :custom_filter like this:

g.column :column_name => 'Project Name', :attribute_name => 'name', :model_class => 'Project',
         :custom_filter => Project.find(:all).map{|pr| [pr.name, pr.name]} do |task|
  task.project.name if task.project
end

is bad style and can fail, because the resulting condition will compare the name of the project, projects.name to a string, and in some databases it is possible that different records (projects in our example) have the same name.

To use filter with foreign keys, we have to change the declaration of the column from projects.name, to tasks.project_id, and build the dropdown with foreign keys as values:

g.column :column_name => 'Project Name', :attribute_name => 'tasks.project_id',
         :custom_filter => Project.find(:all).map{|pr| [pr.id, pr.name]} do |task|
  task.project.name if task.project
end

However, this will break the ordering of the column - the column will be ordered by the integer foreign key. To fix this, we can override the ordering using :custom_order:

@tasks_grid = initialize_grid(Task,
  :include => :project,
  :custom_order => {
    'tasks.project_id' => 'projects.name'
  }
)

Any other symbol (method name) or an array of symbols (method names)

For one symbol (different from :auto) the dropdown list is populated by all unique values returned by the method with this name sent to all ActiveRecord objects throughout all pages.

The conditions set up by the user are ignored, that is, the records used are all those found on all pages without any filters active.

For an array of symbols, the first method name is sent to the ActiveRecord object if it responds to this method, the second method name is sent to the returned value unless it is nil, and so on. In other words, a single symbol mode is the same as an array of symbols where the array contains just one element.

g.column :column_name => 'Version', :attribute_name => 'expected_version_id', :custom_filter => [:expected_version, :to_option] do |task|
  task.expected_version.name if task.expected_version
end

There are two important differences from :auto:

  1. The method does not have to be a field in the result set, it is just some value computed in the method after the database call and ActiveRecord instantiation.

  2. Filtering by any option of such a custom filter will bring a non-empty list, unlike with :auto.

This mode has one major drawback - this mode requires an additional query without offset and limit clauses to instantiate all ActiveRecord objects, and performance-wise it brings all the advantages of pagination to nothing. Thus, memory- and performance-wise this can be really bad for some queries and tables and should be used with care.

If the final method returns a atomic value like a string or an integer, it is used for both the value and the label of the select option element:

<option value="returned value">returned value</option>

However, if the retuned value is a two element array, the first element is used for the option label and the second - for the value.

Typically, a model method like the following:

def to_option
  [name, id]
end

together with

:custom_filter => :to_option

would do the trick:

<option value="id">name</option>

Alternatively, a hash with the single key-value pair can be used, where the key will be used for the label, and the key - for the value:

def to_option
  {name => id}
end

Special treatment of values ‘null’ and ‘not null’

Values ‘null’ and ‘not null’ in a generated custom filter are treated specially, as SQL null statement and not as strings. Value ‘null’ is transformed into SQL condition IS NULL, and ‘not null’ into IS NOT NULL

Thus, if in a filter defined by

:custom_filter => {'No' => 'null', 'Yes' => 'not null', '1' => 1, '2' => '2', '3' => '3'}

values ‘1’, ‘2’ and ‘No’ are selected (in a multi-select mode), this will result in the following SQL:

( table.field IN ( '1', '2' ) OR table.field IS NULL )

Multiple selection

By default it is possible for any dropdown list to switch between single and multiple selection modes. To only allow single selection use :allow_multiple_selection:

g.column :column_name => 'Expected in version', :attribute_name => 'expected_version_id',
       :custom_filter => [:expected_version, :to_option], :allow_multiple_selection => false do |task|
  ...
end

Defaults

Default values like can be changed in config/initializers/wice_grid_config.rb, as well grid labels and paths to some images.

Submit/Reset buttons

View helper submit_grid_javascript returns javascript which applies current filters. View helper reset_grid_javascript returns javascript which resets the grid, clearing the state of filters. This allows to create your own Submit and Reset buttons anywhere on the page with the help of button_to_function:

<%= button_to_function "Submit", submit_grid_javascript(@grid) %>
<%= button_to_function "Reset",  reset_grid_javascript(@grid) %>

To complement this feature there are two parameters in the grid helper :hide_submit_button and :hide_reset_button which hide default buttons in the grid if set to true.

Auto-reloading filters

It is possible to configure a grid to reload itself once a filter has been changed. It works with all filter types including the JS calendar, the only exception is the standard Rails date/datetime filters.

Use option :auto_reload in the column definiton:

<%= grid(@tasks_grid, :show_filters => :always, :hide_submit_button => true) do |g|

  # String
  g.column :column_name => 'Title', :attribute_name => 'title', :auto_reload => true

  # Boolean
  g.column :column_name => 'Archived', :attribute_name => 'archived', :auto_reload => true

  # Custom (dropdown)
  g.column :column_name => 'Status', :attribute_name => 'status_id', :custom_filter => Status.to_dropdown, :auto_reload => true  do |task|
    task.status.name if task.status
  end

  # Datetime
  g.column :column_name => 'Added', :attribute_name => 'created_at', :auto_reload => true, :helper_style => :calendar do |task|
    task.created_at.to_s(:short)
  end

end -%>

To make this behavior default change constant AUTO_RELOAD in the configuration file.

Styling the grid

Predefined css classes used by the grid

td tags are assigned two styles automatically - sorted if the column is the one by which the grid is ordered, and active_filter if the column’s filter is on.

Odd and even tr tags are assigned styles odd and even, correspondingly.

For other classes see wice_grid.css. Feel free to customize it according to your needs.

Adding classes and styles

The grid helper accepts parameter :table_html_attrs which is a hash of HTML attributes for the table tag.

Another grid parameter is header_tr_html_attrs which is a hash of HTML attributes to be added to the first tr tag (or two first tr‘s if the filter row is present).

:td_html_attrs is a parameter for the column method setting HTML attributes of td tags for a certain column.

Adding classes and styles dynamically

WiceGrid offers ways to dynamically add classes and styles to TR and TD based on the current ActiveRecord instance.

For <TD>, let the column return an array where the first item is the usual string output whole the second is a hash of HTML attributes to be added for the <td> tag of the current cell:

g.column  do |portal_application|
  css_class = portal_application.public? ? 'public' : 'private'
  [portal_application.name, {:class => css_class}]
end

For adding classes/styles to <TR> use special clause row_attributes , similar to column, only returning a hash:

<%= grid(@versions_grid) do |g|
  g.row_attributes do |version|
    if version.in_production?
      {:style => 'background-color: rgb(255, 255, 204);'}
    end
  end

  g.column{|version| ... }
  g.column{|version| ... }
end  -%>

Naturally, there can be only one row_attributes definition for a WiceGrid instance.

Various classes do not overwrite each other, instead, they are concatenated.

WiceGrid icons are in directory public/images/icons/grid/.

Adding rows to the grid

It is possible to add your own handcrafted HTML after and/or before each grid row. This works similar to row_attributes, by adding blocks after_row, before_row, and last_row:

<%= grid(@tasks_grid) do |g|
  g.before_row do |task|
    if task.active?
      "<tr><td colspan=\"10\">Custom line for #{t.name}</td></tr>"  # this would add a row
                                                                    # before every active task row
    else
      nil
    end
  end

  g.last_row do          # This row will always be added to the bottom of the grid
    content_tag(:tr,
      content_tag(:td,
        'Last row',
      :colspan => 10),
     :class => 'last_row')
  end

  .......
end %>

It is up for the developer to return the correct HTML code, or return nil if no row is needed for this record. Naturally, there is only one before_row definition and one after_row definition for a WiceGrid instance.

A real life example might be some enterprisy tables inside a table.

Rendering a grid without records

If the grid does not contain any records to show, it is possible show some alternative view instead of an empty grid. Bear in mind that if the user sets up the filters in such a way that the selection of records is empty, this will still render the grid and it will be possible to reset the grid clicking on the Reset button. Thus, this only works if the initial number of records is 0.

<%= grid(@grid) do |g|

  g.blank_slate  do
    "There are no records"
  end

  g.column  do |product|
     ...
  end
 end  -%>

There are two alternative ways to do the same, submitting a string to blank_slate:

g.blank_slate "some text to be rendered"

Or a partial:

g.blank_slate :partial => "partial_name"

Localization

Versions of WiceGrid newer than version 0.5 support localization via Rails I18n, however, without breaking compatibility with pre-I18n versions of Rails (2.1.0 and older).

Running

./script/generate wice_grid_assets

will copy file wice_grid.yml to config/locales.

The current locale is taken from I18n.locale. The locale also propagates to the javascript calendar widget (see Calendar.messagebundle in calendarview.js to add languages).

If the messages are not found, or I18n is missing, WiceGrid will fall back to the hardcoded messages in config/initializers/wice_grid_config.rb.

Currently supported languages are English, Dutch, French, and Russian.

ERB mode

The view helper can function in two different modes. These are defined by its erb_mode parameter. By default (:erb_mode => false) the view helper is a simple helper surrounded by <%= and %>, like in all examples above.

The second mode (:erb_mode => true) is called ERB mode and it allows to embed any ERB content inside blocks, which is basically the style of the form_for helper, only form_for takes one block, while inside the grid block there are other method calls taking blocks as parameters:

<% grid(@countries_grid, :erb_mode => true) do |g| %>

  <% g.column :column_name => 'Name', :attribute_name => 'name' do |country| %>
    <b>Name: <%= link_to(country.name, country_path(country)) %></b>
  <% end %>

  <% g.column :column_name => 'Numeric Code', :attribute_name => 'numeric_code' do |country| %>
    <i>Numeric Code: <%= country.numeric_code %></i>
  <% end %>

<% end -%>

This mode can be usable if you like to have much HTML code inside cells.

Please remember that in this mode the helper opens with <% instead of <%=, similar to form_for.

The default value for :show_filters can be changed in config/initializers/wice_grid_config.rb.

Action Column

It is easy to add a column with checkboxes for each record. This is useful for actions with multiple records, for example, deleting selected records. Please note that action_column only creates the checkboxes and the ‘Select All’ and ‘Deselect All’ buttons, and the form itself as well as processing the parameters should be taken care of by the application code.

<%= grid(@tasks_grid, :show_filters => :always) do |g|

  ...

  g.action_column

  ...

end -%>

By default the name of the HTTP parameter follows pattern "#{grid_name}[#{param_name}][]", thus params[grid_name][param_name] will contain an array of object IDs.

See the documentation for more details.

Integration of the grid with other forms on page

Imagine that the user should be able to change the behavior of the grid using some other control on the page, and not a grid filter.

For example, on a page showing tasks, change between ‘Show active tasks’ to ‘Show archived tasks’ using a dropdown box. WiceGrid allows to keep the status of the grid with all the filtering and sorting using helper dump_filter_parameters_as_hidden_fields which takes a grid object and dumps all current sorting and filtering parameters as hidden fields. Just include dump_filter_parameters_as_hidden_fields(@grid) inside your form, and the newly rendered grid will keep ordering and filtering.

<% form_tag('', :method => :get) do %>
  <%= dump_filter_parameters_as_hidden_fields(@tasks_grid) %>
  <%= select_tag 'archived',
     options_for_select([['View active tasks', 0], ['View archived tasks', 1]], @archived ? 1 : 0),
    :onchange => 'this.form.submit()' %>
<% end -%>

Javascript Calendar for Date and DateTime Filters.

Standard Rails Date and DateTime helpers are a set of dropdown lists, and while this is practical, displaying two Date or especially DateTime helpers takes too much space on a page and is in general confusing.

To solve this, WiceGrid includes a second variant of Date/DateTime filters based on a Javascript calendar.

In the Prototype mode the plugin uses a forked version of Calendarview. It is bundled with the plugin and the generator task wice_grid_assets_prototype will install al necessary assets.

The jQuery version uses jQuery datepicker. Because this is part of the standard jQuery codebase, it is not bundled together with the plugin, and it is the responsibility of the programmer to include all necessary assets including localization files if the application is multilingual.

It is always possible to fall back to simple dropdown lists that the standard date/datetime Rails helpers use.

JQUERY DATEPICKER IS ONLY USED FOR DATE FILTERS AT THE MOMENT. DATETIME FILTERS FALL BACK TO THE DEFAULT RAILS DATE/DATETIME HELPERS.

Calendar based helpers are enabled by default, but it’s possible to change it in config/initializers/wice_grid_config.rb, variable HELPER_STYLE.

The flavor of the date filter can also be changed on per-column basis:

g.column :column_name => 'Due Date', :attribute_name => 'due_date', :helper_style => :calendar do |task|
  task.due_date.to_s(:short) if task.due_date
end

g.column :column_name => 'Updated', :attribute_name => 'updated_at', :helper_style => :standard do |task|
  task.created_at.to_s(:short)
end

Constants DATE_FORMAT and DATETIME_FORMAT define the format of dates the user will see, as well as the format of the string sent in a HTTP parameter.

You can change the constants in config/initializers/wice_grid_config.rb. Doing so, make sure that lamdbas defined in DATETIME_PARSER and DATE_PARSER return valid DateTime and Date objects. The format by default is %Y-%m-%d for the Date and the date part of DateTime, and Time.zone.parse and Date.parse handle it. Make sure it stays so.

jQuery datepicker uses a different format flavor, therefore there is an additional constant DATE_FORMAT_JQUERY. While DATE_FORMAT_JQUERY is fed to datepicker, DATE_FORMAT is still used for presenting initial date values in filters, so make sure that DATE_FORMAT_JQUERY and DATE_FORMAT result in an identical date representation.

Show All Records

It is possible to switch to the All Records mode clicking on link “show all” in the bottom right corner. This functionality should be used with care. To turn this mode off for all grid instances, change constant ALLOW_SHOWING_ALL_QUERIES in config/initializers/wice_grid_config.rb to false. To do so for a specific grid, use initializer parameter :allow_showing_all_records.

Configuration constant START_SHOWING_WARNING_FROM sets the threshold number of all records after which clicking on the link results in a javascript confirmation dialog.

CSV Export

It is possible to export the data displayed on a grid to a CSV file. The dumped data is the current resultset with all the current filters and sorting applied, only without the pagination constraint (i.e. all pages).

To enable CSV export add parameters enable_export_to_csv and csv_file_name to the initialization of the grid:

@projects_grid = initialize_grid(Project,
  :include => [:customer, :supplier],
  :name => 'g2',
  :enable_export_to_csv => true,
  :csv_file_name => 'projects'
)

csv_file_name is the name of the downloaded file. This parameter is optional, if it is missing, the name of the grid is used instead. The export icon will appear at the bottom right corner of the grid.

Next, each grid view helper should be placed in a partial of its own, requiring it from the master template for the usual flow. There must be no HTML or ERB code in this partial except for the grid helper.

By convention the name of such a partial follows the following pattern:

_GRID_NAME_grid.html.erb

In other words, a grid named tasks is expected to be found in a template called _tasks_grid.html.erb (remember that the default name of grids is ‘grid’.)

Next, method export_grid_if_requested should be added to the end of each action containing grids with enabled CSV export.

export_grid_if_requested intercepts CSV export requests and evaluates the partial with the required grid helper.

The naming convention for grid partials can be easily overridden by supplying a hash parameter to export_grid_if_requested where each key is the name of a grid, and the value is the name of the template (like it is specified for render, i.e. without ‘_’ and extensions):

export_grid_if_requested('g1' => 'tasks_grid', 'g2' => 'projects_grid')

If the request is not a CSV export request, export_grid_if_requested does nothing and returns false, if it is a CSV export request, the method returns true.

If the action has no explicit render call, it’s OK to just place export_grid_if_requested as the last line of the action:

def index

  @tasks_grid = initialize_grid(Task,
    :name => 'g1',
    :enable_export_to_csv => true,
    :csv_file_name => 'tasks'
  )

  @projects_grid = initialize_grid(Project,
    :name => 'g2',
    :enable_export_to_csv => true,
    :csv_file_name => 'projects'
  )

  export_grid_if_requested
end

Otherwise, to avoid double rendering, use the return value of the method to conditionally call your render :

def index

  ...........

  export_grid_if_requested || render(:action => 'my_template')
end

It’s also possible to supply a block which will be called if no CSV export is requested:

def index

  ...........

  export_grid_if_requested do
     render(:action => 'my_template')
  end
end

If a column has to be excluded from the CSV export, set column parameter in_csv to false:

g.column  :in_csv => false do |task|
  link_to('Edit', edit_task_path(task))
end

If a column must appear both in HTML and CSV, but with different output, duplicate the column and use parameters in_csv and in_html to include one of them to html output only, the other to CSV only:

# html version
g.column :column_name => 'Title', :attribute_name => 'title', :in_csv => false do |task|
  link_to('Edit', edit_task_path(task.title))
end
# plain text version
g.column :column_name => 'Title', :in_html => false do |task|
  task.title
end

The default field separator in generated CSV is a comma, but it’s possible to override this submitting a string to the :enable_export_to_csv parameter:

@products_grid = initialize_grid(Product,
  :enable_export_to_csv => ';',
  :csv_file_name => 'products')

Detached Filters

It is possible to detach filters and place them anywhere on the page. To do so, use parameter :detach_with_id for a column whose filter needs to be detached, with an arbitrary string or a symbol value which will be used later to identify the filter. As soon as there is one column with :detach_with_id, the behavior of the grid helper changes - it becomes an initializer of the grid and doesn’t output any HTML code. To render the grid, use grid for the second time without the block. To render a detached output filter, use helper grid_filter(grid_object, detached_filter_key):

<%= grid(@tasks_grid, :show_filters => :always) do |g|

g.column :column_name => 'Title', :attribute_name => 'title', :detach_with_id => :title_filter do |task|
  link_to('Edit', edit_task_path(task.title))
end

g.column  :column_name => 'Archived', :attribute_name => 'archived', :detach_with_id => :archived_filter do |task|
  task.archived? ? 'Yes' : 'No'
end

g.column :column_name => 'Added', :attribute_name => 'created_at', :detach_with_id => :created_at_filter do |task|
  task.created_at.to_s(:short)
end

end -%>

<% # rendering filter with key :title_filter %>
<%= grid_filter @tasks_grid, :title_filter  %>

<% # rendering filter with key :archived_filter %>
<%= grid_filter @tasks_grid, :archived_filter  %>

<% # rendering filter with key :created_at_filter %>
<%= grid_filter @tasks_grid, :created_at_filter  %>

<% # Rendering the grid body %>
<%= grid(@tasks_grid) %>

It is important that the grid initializer goes first, the order of grid_filter and the second call to grid is of no importance.

Using custom submit and reset buttons together with :hide_submit_button => true and :hide_reset_button => true allows to completely get rid of the default filter row and the default icons (see section ‘Submit/Reset Buttons’).

For CSV export will continue functioning, just make sure the first call to grid is still in the template of its own and is inside of <%= %>, because when CSV is requested, the first grid works in the old fashioned way producing CSV formatted output.

This feature also works with :erb_mode => true.

If a column was declared with :detach_with_id, but never output with grid_filter, filtering the grid in development mode will result in an warning javascript message and the missing filter will be ignored. There is no such message in production.

grid parameter :show_filter must not be set to :no for detached filters to work.

How Does It Work? (For the interested)

When there is at least one column with :detach_with_id, the generated HTML code is stored in a buffer, code for detached filters is stored in buffers of their own identified by the given IDs, and nothing is returned to the view. When the helper is called for the second time, the buffer outputs its content. In a similar fashion, the grid_filter helper outputs buffers for filters.

Compatibility with Rails Asset Caching

Helpers names_of_wice_grid_stylesheets and names_of_wice_grid_javascripts return names of stylesheet and javascript files and can be used with stylesheet_link_tag and javascript_include_tag with :cache => true. Using this trick you have to deal with the parameters correctly, mind that Prototype has to be loaded before WiceGrid javascripts:

<%= stylesheet_link_tag *(['core',  'modalbox'] + names_of_wice_grid_stylesheets + [ {:cache => true}]) %>
<%= javascript_include_tag *([:defaults] + names_of_wice_grid_javascripts + [ 'ui', 'swfobject', {:cache => true}]) %>

Access to Records From Outside The Grid

There are two ways you can access the records outside the grid - using methods of the WiceGrid object and using callbacks.

Accessing Records Via The WiceGrid Object

Method current_page_records returns exactly the same list of objects displayed on page:

<%= grid(@tasks_grid) do |g|
  ...
end -%>

<p>
  IDs of records on the current page:
  <%= @tasks_grid.current_page_records.map(&:id).to_sentence %>
</p>

Method all_pages_records returns a list of objects browsable through all pages with the current filters:

<%= grid(@tasks_grid) do |g|
  ...
end -%>

<p>
  IDs of all records:
  <%= @tasks_grid.all_pages_records.map(&:id).to_sentence %>
</p>

Mind that this helper results in an additional SQL query.

Because of the current implementation of WiceGrid these helpers work only after the declaration of the grid in the view. This is due to the lazy nature of WiceGrid - the actual call to the database is made during the execution of the grid helper, because to build the correct query columns declarations are required.

Accessing Records Via Callbacks

It is possible to set up callbacks which are executed from within the plugin just after the call to the database. The callbacks are called before rendering the grid cells, so the results of this processing can be used in the grid. There are 3 ways you can set up such callbacks:

Via a lambda object:

def index
  @tasks_grid = initialize_grid(Task,
    :with_paginated_resultset => lambda do |records|
      ...
    end
  )
end

Via a symbol which is the name of a controller method:

def index
  @tasks_grid = initialize_grid(Task,
    :with_paginated_resultset => :process_selection
  )
end

def process_selection(records)
  ...
end

Via a separate block:

def index
  @tasks_grid = initialize_grid(Task)

  @tasks_grid.with_paginated_resultset do |records|
    ...
  end
end

There are two callbacks:

  • :with_paginated_resultset - used to process records of the current page

  • :with_resultset - used to process all records browsable through all pages with the current filters

While the :with_paginated_resultset callback just receives the list of records, :with_resultset receives a lambda object which, when called, returns the list of all records:

def index
  @tasks_grid = initialize_grid(Task)

  @tasks_grid.with_resultset do |wrapper|
    all_records = wrapper.call
    ...
  end
end

This lazy nature exists for performance reasons. Reading all records leads to an additional call, and there can be cases when processing all records should be triggered only under certain circumstances:

def index
  @tasks_grid = initialize_grid(Task)

  @tasks_grid.with_resultset do |wrapper|
    if params[:process_all_records]
      all_records = wrapper.call
      ...
    end
  end
end

Icons

Icons used by the plugin are courtesy of Mark James, the creator of the SILK icon set - www.famfamfam.com/lab/icons/silk/.