Recently, we were building a CMS system for a client, and had the requirement that admins should be able to set the order in which various records (photos, job adverts, team member pages) appeared on the frontend. Here’s how we did it.

We’re big fans of ActiveAdmin here at CookiesHQ – it allows to build admin backends quickly and simply, giving us more time to focus on customer-facing features.

ActiveAdmin does have the ability to reorder records by using the has_many form helper (using the jQueryUI Sortable widget), but not collections of records in the index view. Fortunately, ActiveAmin is very extensible, and we were able to add the functionality easily.

The following code is from a Rails 4.2 app, and assumes that your model has an integer column called order.

Backend

First up, we define a custom action for our client-side code to submit to:

collection_action :reorder, method: :patch do
  reorder_params = params.require(:items).map {|item| item.permit(:id, :order) }

  reorder_ids        = reorder_params.map {|item| item[:id] }
  reorder_attributes = reorder_params.map {|item| item.slice(:order) }

  resource_class.update(reorder_ids, reorder_attributes)

  render json: { status: "success" }
end

Some wrangling of the params is needed in order to transform it into a format suitable for update. We make the naive assumption in this case that the reordering worked, since we’re dealing with multiple records, but more robust code could wrap the update in a transaction and ensure that all the updates succeed, or otherwise rollback.

We also added some extra configuration to make reordering easier:

# ensures that the default ordering reflects our updates
config.sort_order = "order"
# reordering doesn't work across multiple pages,
# so try to put everything on one page
config.per_page   = 100

 # this filter isn't really needed
remove_filter :order

Markup

Rather than make the entire table row draggable (which proved to be problematic as it has several links within it, clicking on which was sometimes interpreted as a drag), we set up a special column as a “drag handle” for the row (using the font-awesome-rails gem).

We opted to define this in the controller, rather than a helper module, since helper reloading in development is currently an issue in ActiveAdmin.

controller do
  private

  def reorderable_column(dsl)
    # Don't allow reordering if filter(s) present
    # or records aren't sorted by `order`
    return if params[:q].present? || params[:order] != "order"

    dsl.column(sortable: false) do
      dsl.fa_icon :arrows, class: "js-reorder-handle"
    end
  end

  helper_method :reorderable_column
end

Since ActiveAdmin’s filtering can exclude records from the index table (and since our reordering affects the entire table at once), we disable reordering if a filter is present. Similarly, we also disable reordering if the table is ordered by a column other than order.

The reorderable_column method is then used like so:

index do
  reorderable_column(self)
  selectable_column
  column :title
  # etc...
end

Passing self is a bit ugly, but necessary in order to access the column method without monkey-patching ActiveAdmin.

Client-side

Since ActiveAdmin already comes with jQueryUI, using the Sortable widget seemed like an obvious choice. We thus put together a little CoffeeScript class that initialises the jQueryUI Sortable widget, and sets up an AJAX request to fire when the ordering is updated:

# admin/reorderable_table.js.coffee
class Admin.ReorderableTable
  constructor: (selector) ->
    $(selector).find("tbody").sortable
      items:  "tr"
      handle: ".js-reorder-handle" # from `reorderable_column` above
      update: @_sendPositions

  _calculatePositions: (sortable) ->
    # Sortable uses ids by default for serialisation
    for itemId, index in $(sortable).sortable("toArray")
      # ActiveAdmin sets the id to the form "underscored_classname_id"
      id:    itemId.split("_").pop()
      order: index + 1

  _sendPositions: (event) =>
    positions = items: @_calculatePositions(event.target)

    # `url` assumes that we're on the index page, with no extra params
    $.ajax
      url:         "#{window.location.pathname}/reorder"
      method:      "PATCH"
      dataType:    "json"
      contentType: "application/json"
      data:        JSON.stringify(positions)


# Initialise on page load
$ ->
  new Admin.ReorderableTable(".index_table")

This file then needs to be referenced from active_admin.js.coffee in your app/assets/javascript folder:

#= require active_admin/base
#= require ./admin/reorderable_table

Extract to a module

Now we have some functionality that can be wrapped up in a module and reused. Unfortunately, we can’t use concerns, since the object that you manipulate inside of ActiveAdmin.register isn’t a class, but an instance of ActiveAdmin::ResourceDSL. Thus, including a module would include our behaviour in all ActiveAdmin resources (which may or may not be what you want).

We instead settled on a convention for “concerns”, which consisted of modules with an .apply_to method, into which was passed the instance of ActiveAdmin::ResourceDSL. We then use instance_exec in order to call methods as if we were inside the ActiveAdmin.register block:

module ReorderableByAdmin
  def self.apply_to(dsl)
    dsl.instance_exec do
      # set config, define controller methods/actions etc.
    end
  end
end

Conclusion

The finished product:

Job adverts

We’ve since extended this approach to handle a grid layout for index pages, by subclassing the CoffeeScript class and customising the initialisation of the jQueryUI Sortable widget, but with the same backend.

Image from Wikimedia Commons