Datagrid v1 was released on Sep 19 2013 - more than 10 years ago. A lot of changes in best practices and available technology had happened during this period. It caused the library to be designed without support of those technologies or their implementation to be suboptimal because of backward compatibility. Version 2 addresses all that evolution.
List of things introduces:
- Ruby endless ranges for range filters.
- Use Hash instead of Array for multiparameter attributes.
- Remove
column[url]
option. - Inherit
Datagrid::Base
instead ofinclude Datagrid
. ApplicationGrid
is recommended base class instead ofBaseGrid
.
- Use
form_with
instead ofform_for
. - Deprecated
datagrid_order_for
- Modern modular CSS classes.
- HTML5 input types: number, date, datetime-local.
- Native Rails Engines:
- while supported, the library was not initially designed for it.
- HTML5 data attributes
- Use
column[tag_options]
option instead ofcolumn[class]
. - Consistent
label[for]
andinput[id]
for range filters. - Updated app/views/datagrid/enum_checkboxes
- Introduced
datagrid.filters.range.separator
localization - Remove SASS dependency
- Replace
rake datagrid:copy_partials
withrails g datagrid:views
column[url]
option was introduced before flexible data/html output layer for columns was established. Here is how the deprecated option can be migrated to modern setup:
Version 1:
column(:user, url: -> (user) => { user_profile_path(user) }) do
user.name
end
Version 2:
column(:user) do |user|
format(user.name) do |value|
link_to value, user_profile_path(user)
end
end
All deprecated columns can be found with a script.
Rails deprecates form_for in favor of form_with.
datagrid_form_for
is now deprecated if favor of datagrid_form_with
.
# V1
datagrid_form_for(@users_grid, url: users_path)
# V2
datagrid_form_with(model: @users_grid, url: users_path)
Version 2 built-in view datagrid/form
uses form_with
no matter of the which helper is used.
Beware of that.
datagrid_order_for
helper serves no purpose and should not be used directly.
The recommended way is to include your ordering code directly into datagrid/head
partial.
See default head partial for example.
You can implement datagrid_order_for
in ApplicationHelper
and copy datagrid/order_for into your project if you consider it useful:
module ApplicationHelper
def datagrid_order_for(grid, column)
render(partial: "datagrid/order_for", locals: { grid: grid, column: column })
end
end
include Datagrid
causes method name space to be clamsy.
Version 2 introduces a difference between the class
that needs to be inherited and high level namespace (just like most gems do):
class ApplicationGrid < Datagrid::Base
end
Ruby supports endless ranges now, so there is no need to present endless ranges as Hash or Array. But it introduces a breaking changes to range filters in Datagrid:
class UsersGrid < Datagrid::Base
filter(:id, :integer, range: true) do |value, scope|
# V1 value is [1, nil]
# V2 value is 1..nil
scope.where(id: value)
end
end
grid = UsersGrid.new
grid.id = [1, nil]
grid.id # V1: [1, nil]
# V2: (1..nil)
Version 2 makes an effort to make the transition as smooth as possible to you:
- Old Array format will be converted to new Range format
- Serialization/Deserialization of Range is held correctly
grid.id = 1..5
grid.id # => 1..5
grid.id = "1..5"
grid.id # => 1..5
grid.id = [nil, 5]
grid.id # => ..5
grid.id = nil..nil
grid id # => nil
grid.id = 3..7
# Simulate serialization/deserialization when interacting with
# jobs queue or database storage
grid = UsersGrid.new(ActiveSupport::JSON.load(grid.attributes.to_json))
grid.id # => 3..7
This very likely breaks all range: true
filters with custom block passed.
All such filters can be seen with this script (works only for V2):
Search all broken range filters
Built-in generated CSS classes renamed to match modern CSS naming conventions and avoid collisions with other libraries:
Old Name | New Name |
---|---|
filter | datagrid-filter |
from | datagrid-input-from |
to | datagrid-input-to |
noresults | datagrid-no-results |
datagrid | datagrid-table |
order | datagrid-order |
asc | datagrid-order-control-asc |
desc | datagrid-order-control-desc |
ordered.asc | datagrid-order-active-asc |
ordered.desc | datagrid-order-active-desc |
field | datagrid-dynamic-field |
operation | datagrid-dynamic-operation |
value | datagrid-dynamic-value |
separator | datagrid-range-separator |
checkboxes | datagrid-enum-checkboxes |
All classes are now explicitly assinged inside datagrid partials. Modify built-in partials if you want to change them.
Diff for built-in partials between V1 and V2 See a new built-in CSS file.
The difference in layout generation from v1 to v2.
datagrid_form_for(@grid)
Version 1 layout:
<form class="datagrid-form partial_default_grid" id="new_g"
action="/users" accept-charset="UTF-8" method="get">
<div class="datagrid-filter filter">
<label for="g_id">Id</label>
<input class="id integer_filter from" multiple type="text" name="g[id][]" />
<span class="separator integer"> - </span>
<input class="id integer_filter to" multiple type="text" name="g[id][]" />
</div>
<div class="datagrid-filter filter">
<label for="g_group_id">Group</label>
<label class="group_id enum_filter checkboxes" for="g_group_id_1">
<input id="g_group_id_1" type="checkbox" value="1" name="g[group_id][]" />1
</label>
<label class="group_id enum_filter checkboxes" for="g_group_id_2">
<input id="g_group_id_2" type="checkbox" value="2" name="g[group_id][]" />2
</label>
</div>
<div class="datagrid-actions">
<input type="submit" name="commit" value="Search"
class="datagrid-submit" data-disable-with="Search" />
<a class="datagrid-reset" href="/location">Reset</a>
</div>
</form>
Version 2 layout:
<form class="datagrid-form" action="/users" accept-charset="UTF-8" method="get">
<div class="datagrid-filter" data-filter="id" data-type="integer">
<label for="g_id">Id</label>
<input step="1" class="datagrid-range-from" name="g[id][from]" type="number" id="g_id" />
<span class="datagrid-range-separator"> - </span>
<input step="1" class="datagrid-range-to" name="g[id][to]" type="number" />
</div>
<div class="datagrid-filter" data-filter="group_id" data-type="enum">
<label>Group</label>
<div class="datagrid-enum-checkboxes">
<label for="g_group_id_1">
<input id="g_group_id_1" value="1" type="checkbox" name="g[group_id][]" />1
</label>
<label for="g_group_id_2">
<input id="g_group_id_2" value="2" type="checkbox" name="g[group_id][]" />2
</label>
</div>
</div>
<div class="datagrid-actions">
<input type="submit" name="commit" value="Search"
class="datagrid-submit" data-disable-with="Search" />
<a class="datagrid-reset" href="/location">Reset</a>
</div>
</form>
Version 1 generated <input type="text"/>
for most filter types.
Version 2 uses the appropriate input type for each filter type:
Type | HTML Input Element |
---|---|
string | <input type="text"/> |
boolean | <input type="checkbox"/> |
date | <input type="date"/> |
datetime | <input type="datetime-local"/> |
enum | <select> |
xboolean | <select> |
float | <input type="number" step="any"/> |
integer | <input type="number" step="1"/> |
The default behavior can be changed back by using input_options
:
filter(:created_at, :date, range: true, input_options: {type: 'text'})
filter(:salary, :integer, range: true, input_options: {type: 'text', step: nil})
You can disable HTML5 inputs with:
class ApplicationGrid < Datagrid::Base
def self.filter(name, type = :default, input_options: {}, **options)
if [:date, :datetime, :float, :integer].include?(type)
input_options[:type] ||= 'text'
end
super(name, type, input_options:, **options)
end
end
Additionally, textarea inputs are now supported this way:
# Rendered as <textarea/> tag:
filter(:text, :string, input_options: {type: 'textarea'})
Rails multiple input had been a problem #325.
Date From:
<input type="number" name="grid[members_count][]" value="1"/>
Date To:
<input type="number" name="grid[members_count][]" value="5"/>
Serialized to:
{grid: {members_count: ['1', '5']}}
V1 had used this convention for range: true
and dynamic
filter type.
Now, they are using the following convention instead:
Date From:
<input type="number" name="grid[members_count][from]" value="1"/>
Date To:
<input type="number" name="grid[members_count][to]" value="5"/>
Grid#members_count
will automatically typecast a hash
into appropriate Range
on assignment:
grid.members_count = {from: 1, to: 5}
grid.members_count # => 1..5
The old convention would still work to ensure smooth transition to new version:
grid.members_count = [3, 7]
grid.members_count # => 3..7
However, the f.datagrid_filter :members_count
will always generate from/to inputs instead:
<input value="3" type="number" step="1" name="grid[members_count][from]"/>
<span class="datagrid-range-separator"> - </span>
<input value="7" type="number" step="1" name="grid[members_count][to]"/>
It is more semantic and collision proof to use data-*
attributes
instead of classes for meta information from backend.
Therefor built-in partials now generate data attributes by default
instead of classes for column names.
- Filter name
input[class]
implemented as.datagrid-filter[data-filter]
. - Filter type
input[class]
implemented as.datagrid-filter[data-type]
. - Grid class
table[class]
removed due to:- security concerns from some users
- breaking CSS classes naming convention
- Column name
th[class], td[class]
implemented astd[data-column], th[data-column]
.
Note that the behavior change can be reverted by Modify built-in partials Version 2 makes it as easy as possible to override the defaults of the UI.
Version 1:
<div class="datagrid-filter filter">
<label for="form_for_grid_category">Category</label>
<input class="category default_filter" type="text"
name="form_for_grid[category]" id="form_for_grid_category" />
</div>
Version 2:
<div class="datagrid-filter" data-filter="category" data-type="string">
<label for="form_for_grid_category">Category</label>
<input type="text"
name="form_for_grid[category]" id="form_for_grid_category" />
</div>
Diff for built-in views between V1 and V2
Version 1:
<table class="datagrid users_grid">
<tr>
<th class="name">Name</th>
<th class="category">Category</th>
</tr>
<tr>
<td class="name">John</th>
<td class="category">Worker</th>
</tr>
<tr>
<td class="name">Mike</th>
<td class="category">Manager</th>
</tr>
</table>
Version 2:
<table class="datagrid-table">
<tr>
<th data-column="name">Name</th>
<th data-column="category">Category</th>
</tr>
<tr>
<td data-column="name">John</th>
<td data-column="category">Worker</th>
</tr>
<tr>
<td data-column="name">Mike</th>
<td data-column="category">Manager</th>
</tr>
</table>
If you still want to have an HTML class attached to a column use class
column option:
column(:name, class: 'short-column')
Renders:
<th class="short-column" data-column="name">Name</th>
...
<td class="short-column" data-column="name">John</td>
Modify built-in partials if you want to change this behavior completely.
column[class]
option is deprecated in favor of more flexible column[tag_options]
that allows to specify any th/td
html attribute.
Example migration:
# V1
column(:status, class: 'issue-status')
# V2
column(:status, tag_options: {class: 'issue-status'})
All deprecated columns can be found with a script.
W3 validator complains when
a label[for]
attribute doesn't correspond to any input[id]
in the same form.
Version 1 generated no id attribute for range filter inputs by default which resulted in a warning:
<label for="musics_grid_year">Year</label>
<input class="year integer_fiilter from" multiple name="musics_grid[year][]" type="text">
<span class="separator integer"> - </span>
<input class="year integer_filter to" multiple name="musics_grid[year][]" type="text">
Version 2 generates id attribute only for the first input, so that a click on label focuses the first input:
<label for="musics_grid_year">Year</label>
<input id="musics_grid_year" step="1" class="datagrid-range-from" name="musics_grid[year][from]" type="number">
<span class="datagrid-range-separator"> - </span>
<input step="1" class="datagrid-range-to" name="musics_grid[year][to]" type="number">
The behavior can be changed by modifying built-in view.
app/views/datagrid/enum_checkboxes
is now configured differently:
- Use
datagrid_filter_input
instead ofcheck_box
to ensurefilter
options behave consistently. - Use
choices
local variable instead ofelements
elements
variables contains values:value
,text
andchecked
.choices
has only first two values to ensurechecked
is determined automatically and consistently.
Diff for built-in partials between V1 and V2
Previously recommended base class BaseGrid
is incosistent
with Rails naming conventions.
It was renamed to ApplicationGrid
instead:
# app/grids/application_grid.rb
class ApplicationGrid < Datagrid::Base
def self.timestamp_column(name, *args, &block)
column(name, *args) do |model|
value = block ? block.call(model) : model.public_send(name)
value&.strftime("%Y-%m-%d")
end
end
end
# app/grids/users_grid.rb
class UsersGrid < ApplicationGrid
scope { User }
column(:name)
timestamp_column(:created_at)
end
A separator symbol between range filter inputs is now a part of localizations to avoid hard coding.
Add datagrid.filters.range.separator
to your localization file.
SASS is no longer a default choice when starting a Ruby on Rails project. Version 2 makes it more flexible by avoiding the dependency on any particular CSS framework.