April 15, 2019; in Ruby on Rails

Trivial Batch Actions in Ruby on Rails

Sometimes, you want to send a message to a bunch of objects. In this case, I want to mark a bunch of a customer’s “Location” records as “do not display”. But of course, sometimes the customer will want to have “do display”.


This is now ActiveAction.

Planning the design

I want to re-use as many Ruby on Rails components, conventions, and ideas as possible. Here’s my plan:

  • write a macro to store my batch actions so I can display them on a button
  • use simple_form
  • use as little javascript as possible

Writing the macro

I’m going to defer to using require_dependency because this code isn’t shared amongst other controllers and it helps keep things clean. I talked about how require_dependency works in a separate blog post.

In app/controllers/application_controller/batch_actions_dependency.rb:

class ApplicationController
  def self.batch_action(action_name, options = {})
    add_batch_action(action_name, options)
  def self.batch_actions
    @_batch_actions ||= []

  # define it on the instance so we can access it from our views
  helper_method :batch_actions
  def batch_actions

  def self.add_batch_action(action_name, options = {})
    options[:label] ||= action_name.to_s.titleize
    if action_name.is_a?(Symbol)
      action = OpenStruct.new(action: action_name, label: options[:label])
    elsif action_name.is_a?(Hash)
      action = OpenStruct.new(action: action_name, label: options[:label])
    self.batch_actions << action

And then drop it in my application_controller.rb:

require_dependenncy 'application_controller/batch_actions_dependency'

class ApplicationController < ActionController::Base; end

Integrating our DSL into the resource controller

In my controllers, I’m going to use separate methods for each type of batch action. This really does belong in its own controller but for this blog post we’ll be less mindful.

class CustomersController < ApplicationController
  before_action :find_customer, only: [:show, :mark_as_displayable, :mark_as_not_displayable]

  def show
    @customer = @customer.includes(:locations)

  batch_action :mark_as_displayable, label: "Display selected"
  def mark_as_displayable
    @customer.locations.where(id: customer_params[:location_ids]).update_all(display: true)
    # ...

  batch_action :mark_as_not_displayable, label: "Don't display selected"
  def mark_as_not_displayable
    @customer.locations.where(id: customer_params[:location_ids]).update_all(display: false)
    # ...


  def find_customer
    @customer = Customer.find(params[:id])

  def customer_params
    params.require(:customer).permit(location_ids: [])

But, of course, brownie points if you extract the relevant methods into BatchLocationsController. Again, for the purposes of this blog post we’ll keep things simple.

Adding the routes

And routes.rb:

resources :customers do
  member do
    patch :mark_as_not_displayable
    patch :mark_as_displayable

Writing our view

I’m use SimpleForm because it’s great. I’ll omit the usual submit button and instead submit the form with Javascript, only after altering the action the form submits to. I’ll include checkboxes in each table row:

<%= simple_form_for @customer do |f| %>
  <div class="card">
    <div class="card-header bg-light header-elements-inline">
      <div class="header-elements">
        <div class="btn-group">
          <button type="button" class="batch_actions btn btn-outline dropdown-toggle" data-toggle="dropdown" disabled="disabled">Batch actions</button>
          <div class="dropdown-menu dropdown-menu-right">
            <% batch_actions.each do |act| %>
                <a href="#" class="dropdown-item batch_action" data-action="<%= act.action %>"><%= act.label %></a>
            <% end %>
    <table class="table table-xs">
       <% @customer.locations.each do |location| %>
           <td><input type="checkbox" class="customer_location_ids collection_selection" name="customer[location_ids][]" value="<%= location.id %>" /></td>
           <th><%= location.name %></th>
           <th><%= location.display? %></th>
       <% end %>
<% end %>

This input checkbox could be a form builder method but because this is a blog post we’ll be explicit.


$('.collection_selection').on('change', function() {
    if($(this).is(':checked')) {
    } else {
    if($('.collection_selection:checked').length > 0) {
    } else {
        $('.batch_actions').attr('disabled', 'disabled');
$('.batch_action').on('click', function() {
    var form = $(this).closest('form');
    form.attr('action', form.attr('action') + "/" + $(this).attr('data-action'));

The idea here is to simply change what path the form posts to before submitting it. From there, we can handle the form like we would otherwise.

Obviously, this applies to an association, but the same idea applies for first-level objects as well. You’d want to change members do ... in the routes to collection do ... and the rest should take care of itself.