StimulusRailsDatatables

A comprehensive Rails gem that provides DataTables integration with server-side processing, advanced filtering, and Stimulus controllers for modern Rails applications.

Features

  • Server-side DataTables: Full integration with ajax-datatables-rails
  • Advanced Filtering: Dynamic filters with dependent selects and date ranges
  • Stimulus Controllers: Modern JavaScript integration using Hotwire Stimulus
  • Bootstrap 5 Support: Beautiful, responsive tables out of the box
  • LocalStorage State: Filter and table state persistence
  • Flexible API: Easy-to-use Ruby helpers and JavaScript API

Installation

Add this line to your application's Gemfile:

gem 'stimulus_rails_datatables'

And then execute:

$ bundle install
$ rails generate stimulus_rails_datatables:install

This will create:

  • config/initializers/stimulus_rails_datatables.rb
  • app/javascript/datatables_config.js

Setup JavaScript Controllers

In app/javascript/controllers/index.js:

import DatatableController from 'stimulus_rails_datatables/datatables_controller'
import FilterController from 'stimulus_rails_datatables/filter_controller'

application.register('datatable', DatatableController)
application.register('filter', FilterController)

In app/javascript/application.js:

import 'datatables_config'

Usage

Helpers

Basic DataTable

<%= datatable_for 'users-table', source: users_path do |dt| %>
  <% dt.column :id, title: 'ID' %>
  <% dt.column :name, title: 'Name' %>
  <% dt.column :email, title: 'Email' %>
  <% dt.column :created_at, title: 'Created', orderable: false %>
<% end %>

With Filters

<%= filter_for 'users-table' do |f| %>
  <%= f.status do |opts| %>
    <%= opts.option '', 'All Statuses' %>
    <%= opts.option 'active', 'Active' %>
    <%= opts.option 'inactive', 'Inactive' %>
  <% end %>

# If set_value matches, it will be mark as selected
# When set_value is present, localStorage restoration is skipped
# so the default is not overridden on page reload.
  <%= f.role(
    remote: {
      url: roles_path,
      label: 'name',
      value: 'id',
      placeholder: 'Select Role',
      set_value: 1 
    }
  ) %>

  <%= f.duration do |opts| %>
    <%= opts.option '', 'All Time' %>
    <%= opts.option 'today', 'Today' %>
    <%= opts.option 'this_week', 'This Week' %>
    <%= opts.option 'custom', 'Custom Range' %>
  <% end %>
<% end %>

Dependent Location Filters

Use dependent remote selects when one filter should load its options from the value of another filter. The built-in location helper creates three filters named province_id, city_id, and barangay_id.

<%= filter_for 'locations-table' do |f| %>
  <%= f.location(
    province_url: provinces_path(format: :json),
    city_url: cities_path(province_id: '{province_id}', format: :json),
    barangay_url: barangays_path(city_id: '{city_id}', format: :json)
  ) %>
<% end %>

<%= datatable_for 'locations-table', source: locations_path(format: :json) do |dt| %>
  <% dt.column :name, title: 'Name' %>
  <% dt.column :province_name, title: 'Province' %>
  <% dt.column :city_name, title: 'City' %>
  <% dt.column :barangay_name, title: 'Barangay' %>
<% end %>

The {province_id} and {city_id} placeholders are replaced in the browser before fetching the next select's options:

  • changing province fetches cities_path(... province_id: selected_province_id)
  • changing city fetches barangays_path(... city_id: selected_city_id)
  • changing any filter reloads the datatable with params such as filters[province_id]=1&filters[city_id]=2

Your JSON endpoints should return an array using the keys configured by the helper: location_id for the option value and name for the label.

class ProvincesController < ApplicationController
  def index
    render json: Province.select(:location_id, :name)
  end
end

class CitiesController < ApplicationController
  def index
    cities = City.where(province_id: params[:province_id])
    render json: cities.select(:location_id, :name)
  end
end

class BarangaysController < ApplicationController
  def index
    barangays = Barangay.where(city_id: params[:city_id])
    render json: barangays.select(:location_id, :name)
  end
end

Then apply the selected filters inside your datatable class:

def get_raw_records
  Location.all.then { |relation| apply_filters(relation) }
end

def apply_filters(relation)
  relation = relation.where(province_id: query_filters[:province_id]) if query_filters[:province_id].present?
  relation = relation.where(city_id: query_filters[:city_id]) if query_filters[:city_id].present?
  relation = relation.where(barangay_id: query_filters[:barangay_id]) if query_filters[:barangay_id].present?
  relation
end

To load the table already filtered from the page URL, use normal nested filter params:

/locations?filters[province_id]=1&filters[city_id]=2&filters[barangay_id]=3

Pass those params into the datatable source on the initial render:

<% location_filters = params[:filters]&.permit(:province_id, :city_id, :barangay_id) || {} %>

<%= datatable_for 'locations-table',
  source: locations_path(format: :json, filters: location_filters) do |dt| %>
  <% dt.column :name, title: 'Name' %>
  <% dt.column :province_name, title: 'Province' %>
  <% dt.column :city_name, title: 'City' %>
  <% dt.column :barangay_name, title: 'Barangay' %>
<% end %>

Backend DataTable Class

class UserDatatable < StimulusRailsDatatables::BaseDatatable
  def view_columns
    @view_columns ||= {
      id: { source: "User.id" },
      name: { source: "User.name" },
      email: { source: "User.email" },
      created_at: { source: "User.created_at" }
    }
  end

  def data
    records.map do |record|
      {
        id: record.id,
        name: record.name,
        email: record.email,
        created_at: record.created_at.strftime('%Y-%m-%d')
      }
    end
  end

  def get_raw_records
    User.all.then { |relation| apply_filters(relation) }
  end

  private

  def apply_filters(relation)
    relation = relation.where(status: query_filters[:status]) if query_filters[:status].present?
    relation = relation.where(role_id: query_filters[:role_id]) if query_filters[:role_id].present?
    relation
  end
end

JavaScript API

// Access via window.AppDataTable or import directly
import { AppDataTable } from 'stimulus_rails_datatables/app_datatable'

// Reload a specific datatable
AppDataTable.reload('#users-table')

// Load with new URL
AppDataTable.load('#users-table', '/users?status=active')

// Reload all datatables on page
AppDataTable.reloadAll()

Controller Setup

In your Rails controller, respond to JSON format for DataTables:

class UsersController < ApplicationController
  def index
    respond_to do |format|
      format.html
      format.json { render json: UsersDatatable.new(view_context) }
    end
  end
end

Configuration

Customizing DataTables Settings

Edit app/javascript/datatables_config.js to customize the DataTables appearance and behavior:

window.datatablesConfig = {
  // Language strings for DataTables UI
  language: {
    processing: '<div class="spinner-border"></div><div class="mt-2">Loading...</div>',
    lengthMenu: '_MENU_',
    search: `<div class="input-group">
              <span class="input-group-text"><i class="material-symbols-outlined">search</i></span>
              _INPUT_
              <span class="input-group-text">
                <kbd>ctrl + k</kbd>
              </span>
            </div>`,
    info: 'Showing _START_ to _END_ of _TOTAL_ entries',
    paginate: {
      first: 'First',
      last: 'Last',
      next: 'Next',
      previous: 'Previous'
    }
  },

  // Layout configuration
  layout: {
    topStart: 'pageLength',
    topEnd: 'search',
    bottomStart: 'info',
    bottomEnd: 'paging'
  },

  // Length menu options
  lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]]
}

Available Stimulus Controllers

The gem automatically registers the following Stimulus controllers:

  • datatable - Main DataTable controller with server-side processing
  • filter - Filter controller with state management and localStorage persistence

Dependencies

  • Rails >= 7.0
  • ajax-datatables-rails ~> 1.4
  • importmap-rails
  • @hotwired/stimulus
  • DataTables.net with Bootstrap 5 theme

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/denmarkmeralpis/stimulus_rails_datatables.

License

The gem is available as open source under the terms of the MIT License.