Ever needed a “bulk actions” on a list of resources in your Rails app? It is a very common pattern/feature for SaaS apps where users can add many resources (like in admins, CMS’, etc.).
Something like this:
This code is based on Rails Designer’s Bulk Actions Component. 👀
Let’s start with the HTML structure for our selectable list: create a list of items that can be selected with a Shift+Click:
<ul data-controller="select" class="list">
<% @posts.each do |post| %>
<li data-action="click->select#toggle" class="item">
<%= check_box_tag "post_ids[]", post.id, false, hidden: :hidden %>
<%= link_to post.title, post.href %>
</li>
<% end %>
</ul>
Notice each item has a hidden checkbox. You don’t need (or want!) to make the checkbox hidden, but based on your app and audience you can pull this off and keep a clean UI. The list is connected to a Stimulus controller named “select”, and each item has a click action that triggers the toggle
method in the controller.
Now let’s look at the Stimulus controller that handles just the Shift+Click logic:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
toggle(event) {
if (!event.shiftKey) return
event.preventDefault()
const checkbox = event.currentTarget.querySelector('input[type="checkbox"]')
checkbox.checked = !checkbox.checked
}
}
Easy! The controller only prevents the default behavior when the Shift key is pressed, allowing normal link clicks to work as expected. When you hold Shift and clicks on an item, the controller toggles the hidden checkbox state.
Now, let’s add the actions section that appears when items are selected:
<ul data-controller="select" class="list">
<% @posts.each do |post| %>
<li data-action="click->select#toggle" class="item">
<%= check_box_tag "post_ids[]", post.id, false, hidden: :hidden %>
<%= link_to post.title, post.href %>
</li>
<% end %>
+ <div data-select-target="actions" class="actions">
+ <%= button_to "Archive all", "#", class: "action" %>
+ <%= button_to "Rename all", "#", class: "action" %>
+ <%= button_to "Delete all", "#", class: "action" %>
+ </div>
</ul>
And the code that manages the visibility of the actions:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
+ static targets = ["actions"]
+ static values = { selectedItems: { type: Number, default: 0 } }
toggle(event) {
if (!event.shiftKey) return
event.preventDefault()
const checkbox = event.currentTarget.querySelector('input[type="checkbox"]')
checkbox.checked = !checkbox.checked
+ this.#updateSelectedItemsValue()
}
// private
+ selectedItemsValueChanged() {
+ this.actionsTarget.classList.toggle("visible", this.selectedItemsValue !== 0)
+ }
+ #updateSelectedItemsValue() {
+ this.selectedItemsValue = [...this.#checkboxes].filter(checkbox => checkbox.checked).length
+ }
+ get #checkboxes() {
+ return this.element.querySelectorAll('input[type="checkbox"]')
+ }
}
classList.toggle
accepts an optional argument: the controller usesclassList.toggle("visible", this.selectedItemsValue !== 0)
to conditionally add or remove thevisible
class based on whether any items are selected;filter
on arrays: use thefilter
method to count only the checked checkboxes. This creates a clean, functional approach to determining how many items are selected.
In the controller (eg. Posts::BulkController#update) you can work off the post_ids
array that is passed along.
And that is it. The basics to add bulk actions to your list of resources. Check out Rails Designer’s Components for a more comprehensive Bulk Actions Component that is ready to add to your (SaaS) app.