TableSortable
TableSortable adds multi-column, server-side filtering, sorting and pagination to the tableSorter jQuery plugin, so you don't have to worry about interpreting the query parameters, combining multiple queries, columns to sort by, or figuring out how to send the correct page back to the client.
It is a Rails backend complementation to the frontend tableSorter.js.
The Problem
The jQuery tableSorter plugin is an excellent tool for filtering and sorting tables. Often, when dealing with lots of rows, we may want to split the table into multiple pages. tableSorter.js has a nifty widget for that, which requires using mottie's fork of tableSorter.
Usually this is a scenario where we don't want to send our entire set of records to the frontend, which consequently means that the frontend no longer knows the entire set of records to filter and sort through, which eventually requires our server to handle all that stuff as well as the pagination of the results.
The Solution: TableSortable
TableSortable will handle all the backend filtering, sorting and pagination for you.
NOTICE: This gem is in very early stages of development, and is not yet fully documented. Any input will be more than welcome.
Installation
Add this line to your application's Gemfile:
gem 'table_sortable'
Then run bundle install
and you're ready to start
You should also probably be using jquery-tablesorter.
For information regarding integration or tableSorter.js into your Rails project,
please see the jQuery tablesorter plugin for Rails page.
Usage
First, we need to setup our controller. For this example this will be a users controller.
Let's include TableSortable::Controller
so that we can use its methods.
#controllers/users_controller.rb
class UsersController < ApplicationController
include TableSortable::Controller
Next, let's define our columns.
#controllers/users_controller.rb
class UsersController < ApplicationController
include TableSortable::Controller
define_colunns :first_name, :last_name, :email, :created_at
That's just the basic setup of columns. For more configuration options, please see advanced configuration.
Now we need to make sure our index
action filters, sorts and paginates the records.
We can do that using the filter_and_sort
method.
#controllers/users_controller.rb
def index
@users = filter_and_sort(User.all)
respond_to do |format|
format.html {}
format.json {render layout: false}
end
end
Let's write the index
view. We can use TableSortable's view helpers to render our table.
<!-- views/users/index.html.erb -->
<div id="usersPager">
<%= table_sortable_pager %>
</div>
<table id="usersTable" data-query="<%= users_path %>">
<thead>
<tr>
<%= table_sortable_headers %>
</tr>
</thead>
<tbody>
</tbody>
</table>
Notice how index.html
doesn't render the actual rows. They will later be polled via ajax by tableSorter.js.
Let's create index.json.jbuilder
to send the users' info back to the frontend.
# views/users/index.json.jbuilder
# Since we only send out the current page of users,
# we must also send the total number of records.
# We'll use TableSortable's total_count method for that.
json.total @users.total_count
json.rows @users.map{|user| render partial: 'user_row.html.erb', locals: {user: user}}.join
json.pager_output 'Users {startRow} to {endRow} of {totalRows}'
We should also create the _user_row.html partial. In it, we may also use TableSortable's helpers.
<!-- views/users/_user_row.html.erb -->
<tr>
<%= table_sortable_columns user %>
</tr>
Now that we are done configuring the backend - let's continue to frontend. Here's a simple tableSorter.js configuration example:
var table = $('#usersTable');
table.tablesorter({
widgets: ['filter', 'pager'],
widgetOptions: {
// show 10 records at a time
pager_size: 10,
// Poll our users_index_path, which we receive from the table's data-query attribute.
pager_ajaxUrl: table.data('query') + '?pagesize={size}&page={page}&{filterList:fcol}&{sortList:scol}',
// Parse the incoming result
pager_ajaxProcessing: function (data) {
if (data && data.hasOwnProperty('rows')) {
// Update the pager output
this.pager_output = data.pager_output;
// return total records, the rows HTML data,
// and the headers information.
return [data.total, $(data.rows)];
}
}
}
});
That's it! The results fetched from the server are now filtered, sorted and paginated by TableSortable.
For full documentation of the usage of tableSorter.js please go here for the very popular fork by mottie, or here for the original version of the plugin.
Of course there are many more configuration options that make TableSortable flexible and adaptable. For those, please see advanced configuration.
Advanced Configuration
TableSortable has several advanced configuration settings, which allow for a more fine-grained control over its behaviour.
- define_column
- define_column_order
- define_column_offset
define_column
TableSortable lets you define the columns one by one with many custom attributes, using the define_column
method.
#controllers/users_controller.rb
class UsersController < ApplicationController
include TableSortable::Controller
define_column :full_name,
value: -> (user) {"#{user.first_name} #{user.last_name}"}
define_column :email
end
Syntax: define_column column_name, [arguments]
-
column_name
(required)
A symbol representing the column_name. Can be anything, but should usually be the same as the column name. -
arguments
(optional)-
value: (symbol|proc)
default: same as column name
Accepts either a symbol representing a method that the record responds to, like an ActiveRecord attribute, or a proc the returns the record's value. for example:define_column :full_name, value: -> (user) {"#{user.first_name} #{user.last_name}"}
-
content: (symbol|proc)
default: same as value
Works the same way asvalue
, but allows specifying a different value to be displayed in the table cells. The difference betweenvalue
andcontent
is that the former determines the value by which the scope will be filtered and sorted, while the latter only affects the displayed cell contents.define_column :full_name, value: -> (user) {"#{user.first_name} #{user.last_name}"}, content: -> (user) {"#{user.last_name}, #{user.first_name}"}
-
label: (string)
default: 'Titleized' version of the column name. Supports I18n internationalization. see translateion_key.
Allows specifying a string to be used as the column label, displayed at the table header. -
placeholder: (string|false)
default: same as label
Allows specifying a string to be used as a placeholder for the column's filter input field. You may also specifyfalse
, in which case no placeholder will be displayed. -
filter: (proc|false)
default: free case-insensitive text search on the column's value
By default, the column will be filtered according to the column's value. However, you may specify a proc to perform the search on that column. The proc itself can contain either ActiveRecord operations (eg.where
) or array operations (eg.select
), and will be passed the current filter query when run.If no filtering is to be performed on that column you can set it todefine_column :full_name, filter: ->(query) {where('LOWER(CONCAT(first_name," ", last_name)) LIKE (?)', "%#{query.downcase}%")}
false
. When using TableSortable's view helpers, this also means that no filter input will be shown on that column. -
filter_method: (:array|:active_record)
default: automatically detects method based on the record given
Determines whether the default filter function relies on an ActiveRecordwhere
method or an Arrayselect
method.
When no method and no filters are supplied, TableSortable will check if the column_name corresponds to a database column of the record. If it does, it will use an ActiveRecordwhere
method, otherwise it will perform an arrayselect
operation.
Only applies when nofilter
option has been specified.-
:array
(default)
Filter using theselect
method. While being slower, it selects based on the column's value, whatever it might be, and so fits every scenario. -
:active_record
Filter using thewhere
method. While being faster, it only applies to cases where the column name matches the database column name.
-
-
filter_initial_value: (string)
default: nil
You may specify an initial filter query for each column. -
sort: (proc|false)
default: sorting based on the column's value
By default, the record set will be sorted according to the selected column's value. However, you may specify a proc to perform the sorting when this column is selected as the sort base. The proc itself can contain either ActiveRecord operations (eg.order
) or array operations (eg.sort
), and will be passed the current sort order as a symbol (:asc
or:desc
) when run.If no sorting is to be performed based on that column you can set it todefine_column :full_name, sort: -> (sort_order) { sort{ |a,b| (sort_order == :asc ? a : b) <=> (sort_order == :asc ? b : a) } }
false
. When using TableSortable's view helpers, this also means that no sorting handle will be shown on the client side on that column as well. -
sort_method: (:array|:active_record)
default: :array
Determines whether the default sort function relies on an ActiveRecordorder
method or an Arraysort
method.
When no method and no filters are supplied, TableSortable will check if the column_name corresponds to a database column of the record. If it does, it will use an ActiveRecordorder
method, otherwise it will perform an arraysort
operation.
Only applies when nosort
option has been specified.-
:array
(default)
Sort using thesort
method. While being slower, it sorts based on the column's value, whatever it might be, and so fits every scenario. -
:active_record
Sort using theorder
method. While being faster, it only applies to cases where the column name matches the database column name.
-
-
template: (string)
default: same as column_name
Allows setting a custom template name to look for when rendering the header or column contents. For more information see view helpers.
-
Dynamic Column Definitions
If the column definitions themselves need to be dynamic (eg. if you're using
dynamic fields in your model) you can also use define column
in a before action
callback like this
#controllers/users_controller.rb
before_action :define_columns
private
def define_columns
@user.custom_fields.each do |cf|
define_column cf,
value: -> (user) {user.get_custom_field(cf)}
end
end
define_column_order
Columns are displayed in the order they are defined using define_method
. If you wish, you may override this order by specifying your own using the define_column_order
method.
It also allows hiding columns that you need to include in the filtering and sorting process, eg. when you want to have an external search box filtering based on a column that you don't necessarily want to display.
Syntax: define_column_order column1, [column2], [column3] ...
#controllers/users_controller.rb
#define a full_name column, allowing to create a custom search box that operates on the 4th column
define_columns :first_name, :last_name, :email, :full_name
define_column_order :last_name, :first_name, :email
define_column_offset
Sometimes you wish to manually add columns in the views before adding the defined TableSortable columns, eg. to have a checkbox next to each row.
<!-- views/users/index.html.erb -->
<table id="usersTable" data-query="<%= users_path %>">
<thead>
<tr></tr> <!-- EXTRA COLUMN -->
<tr>
<%= table_sortable_headers %>
</tr>
</thead>
<tbody>
</tbody>
</table>
<!-- views/users/_user_row.html.erb -->
<tr> <%= check_box_tag :checked %> </tr> <!-- EXTRA COLUMN -->
<tr>
<%= table_sortable_columns user %>
</tr>
Since the column numbers that appear in tableSorter.js' ajax request will represent their actual number, you should let TableSortable know that the columns you defined are offset.
So, in the above case you should define an offset of 1.
Syntax: define_column_offset offset
define_column_offset 1
You may define it either using the define_column_offset
method shown above,
or using the all inclusive define_columns
method, as an offset: <integer>
argument.
define_column :first_name, :last_name, :email, offset: 1
View Helpers
TableSortable offers several view helpers, to help maintain the connection between the view layer and the controller layer, and make your code DRYer and less error prone.
- table_sortable_headers
- table_sortable_columns
- table_sortable_pager
table_sortable_headers
Syntax: table_sortable_headers [html_attributes]
Renders the table header columns, each one inside a <th> tag.
Notice that it does not wrap a <tr> tag around the headers, allowing you to add columns before of after TableSortable's headers.
It also renders data attributes that let tableSorter.js know the columns behaviour:
-
data-filter="false"
if the column'sfilter
attribute is set tofalse
. -
data-sorter="false"
if the column'ssort
attribute is set tofalse
. -
data-placeholder="?"
if a placeholder has been defined (defaults to be the same as the column's label). -
data-value="?"
if an initial filter value has been defined.
You may also specify any html attribute, as well as additional data attributes to be added to each <th> element.
<tr> <%= table_sortable_headers class: 'text-center' %> </tr>
If you wish to render your own version of a specific header (eg. to add an icon),
you may create a partial inside a table_sortable
folder nested inside the controller's views folder. The partial should be named according to the following naming scheme:
_<template>_header.html
, with template
corresponding to the column's template attribute.
Notice that in this case you need to manually specify the different data attributes that correspond to the behaviour you wish this column to have.
The view will be supplied with two locals:
-
label
: the column's label -
column
: the TableSortable::Column object -
index
: the column's index (useful for manually setting thedata-col
attribute)
Here is an example of a full name header partial, which includes a font-awesome icon:
<!-- views/users/table_sortable/_full_name_header.html.erb -->
<th data-placeholder="<%= column.placeholder %>">
<i class="fa fa-user" aria-hidden="true"></i>
<%= label %>
</th>
By using the template attribute you may render several columns using the same template.
Notice that whether using slim or erb, you must include .html before the extension.
table_sortable_columns
Syntax: table_sortable_columns record, [html_attributes]
Renders the table columns for a specific record
, each one inside a <td> tag.
Notice that it does not wrap a <tr> tag around the headers, allowing you to add columns before of after TableSortable's columns.
It also renders data attributes that let tableSorter.js know the columns behaviour:
-
data-text="?"
if the column'svalue
attribute is different than the column'scontent
attribute, the value will be set in thedata-text
attribute, and the content will be displayed inside the <td> element.
You may also specify any html attribute, as well as additional data attributes to be added to each <td> element.
<tr> <%= table_sortable_columns @user, class: 'text-info' %> </tr>
If you wish to render your own version of a specific column (eg. to include a link inside of it),
you may create a partial inside a table_sortable
folder nested inside the controller's views folder. The partial should be named according to the following naming scheme:
_<template>_column.html
, with template
corresponding to the column's template attribute.
Notice that in this case you need to manually specify the different data attributes that correspond to the behaviour you wish this column to have.
The view will be supplied with four locals:
-
record
: the source ActiveRecord object -
column
: the TableSortable::Column object
Here is an example of a full name column partial, in which the name links to the edit_user_path:
<!-- views/users/table_sortable/_full_name_column.html.erb -->
<td>
<%= link_to content, edit_user_path(source) %>
</td>
By using the template attribute you may render several columns using the same template.
Notice that whether using slim, haml or erb, you must include .html before the extension.
table_sortable_pager
documentation coming soon...
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/odedd/table_sortable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the TableSortable project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.