Skip to content

Instantly share code, notes, and snippets.

@jameskerr
Last active July 17, 2025 22:07
Show Gist options
  • Save jameskerr/9305bc7ba469d2be66fcef644a3c6b16 to your computer and use it in GitHub Desktop.
Save jameskerr/9305bc7ba469d2be66fcef644a3c6b16 to your computer and use it in GitHub Desktop.
Mutli Select Stimulus Controller
import Base from "controllers/base";
export default class extends Base {
static targets = ["checkbox", "toggler", "count", "menu", "hide"];
mount() {
this.on("turbo:morph", document, this.update);
this.on("click", this.togglerTarget, this.toggle);
}
update() {
const allBoxes = this.checkboxTargets;
const allCount = allBoxes.length;
const checkedBoxes = allBoxes.filter((box) => box.checked);
const checkedCount = checkedBoxes.length;
if (checkedCount == 0) {
this.closeMenu();
this.togglerTarget.checked = false;
this.selectAllButton.innerHTML = "Select All";
} else if (checkedCount == allCount) {
this.openMenu();
this.togglerTarget.checked = true;
this.selectAllButton.innerHTML = "Deselect All";
} else {
this.togglerTarget.checked = false;
this.selectAllButton.innerHTML = "Select All";
this.openMenu();
}
this.selectedCount = checkedCount;
}
selectAll() {
for (const checkbox of this.checkboxTargets) checkbox.checked = true;
this.update();
}
toggle() {
if (this.allChecked) this.clear();
else this.selectAll();
}
clear() {
for (const checkbox of this.checkboxTargets) checkbox.checked = false;
this.update();
}
get allChecked() {
return this.checkboxTargets.every((box) => box.checked);
}
get selectAllButton() {
return this.togglerTarget;
}
set selectedCount(count) {
this.countTarget.innerHTML = count;
}
openMenu() {
this.menuTarget.removeAttribute("hidden");
this.hideTargets.forEach((el) => {
el.setAttribute("hidden", true);
});
}
closeMenu() {
this.menuTarget.setAttribute("hidden", true);
this.hideTargets.forEach((el) => {
el.removeAttribute("hidden");
});
}
}
@jameskerr
Copy link
Author

Each checkbox gets the "checkbox" target.
The "select all" checkbox is the "toggler" target.
The "menu" target contains the bulk actions.
The "hide" targets are any other elements you want hidden when the menu is open.

I use this to replace the normal toolbar with the bulk menu.

I also like to setup my event listeners in my controller sometimes. That's what that "on" function is for. It's defined in the Base. The base controller looks like this.

import { Controller } from "@hotwired/stimulus";
import { on } from "lib/on";
import { call } from "lib/utils";

export default class Base extends Controller {
  connect() {
    this.on = on(this);
    call(this.mount?.bind(this), []);
  }

  disconnect() {
    this.on.destroy();
    call(this.unmount?.bind(this), []);
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment