Active Sort Order

Gem Version CI Status RubyGems Downloads

The "easy-peasy" dynamic sorting pattern for ActiveRecord that your Rails apps deserve. Useful for Rails controllers with large data, pagination, etc.

Features:

Installation

gem 'active_sort_order'

Then add include ActiveSortOrder to your ApplicationRecord or individual models.

class ApplicationRecord < ActiveRecord::Base
  include ActiveSortOrder
end

Dynamic Sorting

This gem defines one scope on your models: sort_order

This method uses ActiveRecord's reorder under the hood, so any previously defined order will be removed upon calling sort_order

In the below examples we are within a controller and are using the params as our variables:

# app/controllers/posts_controller.rb

case params[:sort]
when "number_str"
  sort_col_sql = "CAST(posts.number_str AS int)"
when "user"
  ### To sort on multiple fields pass in an Array
  sort_col_sql = ["users.first_name", "users.last_name"] 
else
  sort_col_sql = params[:sort]
end

### Output combined sort order (if present) and secondary / base sort order
Post.all.sort_order(sort_col_sql, params[:direction], base_sort_order: "lower(number) ASC, lower(code) ASC")

### Output combined sort order (if present) AND applies the classes base_sort_order (if defined)
Post.all.sort_order(sort_col_sql, params[:direction]) 

Sorting on multiple columns

Method Definition:

sort_order(sort_col_sql = nil, sort_direction_sql = nil, base_sort_order: true)

Options:

  • sort_col_sql is a SQL String of the column name
    • Feel free to use any SQL manipulation on the column name
    • There is no built-in SQL string validation so be sure to handle your sanitization in your project before passing to this method. See Safely Handling Input
    • If blank value provided it will skip the dynamic sort and just apply the base_sort_order
  • sort_direction_sql is a String of the SQL ORDER BY direction
    • The SQL String is automatically validated within the few allowable SQL ORDER BY directions.
    • If nil or "blank string" provided it will fallback to "ASC"
  • base_sort_order is a String of the SQL base ordering
    • If not provided or true it will use the classes base_sort_order method (if defined)
    • If nil or false is provided it will skip the classes base_sort_order

Base Sort Order

To maintain consistency when sorting its always a good idea to have a secondary or base sort order for when duplicates of the main sort column are found or no sort is provided.

For this you can define a base_sort_order class method to your models.

This will be utilized on the sort_order method when not providing a direct :base_sort_order argument.

class Post < ActiveRecord::Base
  include ActiveSortOrder

  def self.base_sort_order
    "lower(#{table_name}.name) ASC, lower(#{table_name}.code) ASC" # for example
  end

end

The default behaviours of this are shown below.

### Applies the classes base_sort_order (if defined)
Post.all.sort_order

### Override the classes base_sort_order
Post.all.sort_order(base_sort_order: "lower(number) DESC")
# OR
Post.all.sort_order(sort_col_sql, params[:direction], base_sort_order: "lower(number) DESC")

### Skip the classes base_sort_order by providing false, nil will still use classes base_sort_order
Post.all.sort_order(sort_col_sql, params[:direction], base_sort_order: false) 

Safely Handling Input

When accepting params or any custom input for column names it is wise to safely map the field name/alias to the correct SQL string rather than directly sending in the params.

Here is an example on how to handle this within your controller:

if params[:sort].present?
  case params[:sort]
  when "author_name"
    sort_col_sql = "authors.name"
  when "a_or_b"
    sort_col_sql = "COALESCE(posts.field_a, posts.field_b)"
  when "price"
    sort_col_sql = "CAST(REPLACE(posts.price, '$', ',', '') AS int)"
  else
    raise "Invalid Sort Column Given: #{params[:sort]}"
  end
end

Post.all.sort_order(sort_col_sql, params[:direction])

Additional Customizations

This gem is just one concern with one scope. I encourage you to read the code for this library to understand how it works within your project so that you are capable of customizing the functionality later. You can always copy the code directly into your project for deeper project-specific customizations.

Helper / View Examples

We do not provide built in helpers or view templates because this is a major restriction to applications. Instead we provide a simple copy-and-pasteable starter template for the sort link:

### app/helpers/application_helper.rb

module ApplicationHelper

  def sort_link(column, title = nil, opts = {})
    column = column.to_s

    if title && title.is_a?(Hash)
      opts = title
      title = opts[:title]
    end

    title ||= column.titleize

    if opts[:disabled]
      return title
    else
      if params[:direction].present? && params[:sort].present?
        direction = (column == params[:sort] && params[:direction] == "asc") ? "desc" : "asc"
      else
        direction = "asc"
      end

      return link_to(title, params.to_unsafe_h.merge(sort: column, direction: direction))
    end
  end

end

Then use the link helper within your views like:

<th>
  <%= sort_link :name %>
</th>

<th>
  <%= sort_link "companies.name", "Company Name" %>
</th>

<th>
  <%= sort_link "companies.name", "Company Name", disabled: !@sort_enabled %>
</th>

Credits

Created & Maintained by Weston Ganger - @westonganger