StimulusJS Tabs
Previously I did a post covering lazy-loading tabs with Turbo Frames
That’s a great appraoch, but not always will you want a separate route-controller-view for each tab.
Sometimes JS tabs are just enough.
Now, I’m not a JS god, but I present you with my minimalistic approach to handling tabs with StimulusJS.
How it works:
- click a button -> unhide the related tab
- click another button -> hide previous tab, open related tab
- click current active button -> hide this tab (all) tabs
- optionally, a select tab can be open by default
HOWTO:
- generate a blank stimulus controller:
rails g stimulus tabs
install stimulus-use
to add the “click outside to close tab(s) behaviour:
bin/importmap pin stimulus-use
- the stimulus controller:
// app/javascript/controllers/tabs_controller.js
import { Controller } from "@hotwired/stimulus"
import { useClickOutside } from "stimulus-use";
// Connects to data-controller="tabs"
export default class extends Controller {
static targets = ["btn", "tab"]
static values = { defaultTab: String }
connect() {
this.tabTargets.map(x => x.hidden = true) // hide all tabs by default
// OPEN DEFAULT TAB
try {
let selectedBtn = this.btnTargets.find(element => element.id === this.defaultTabValue)
let selectedTab = this.tabTargets.find(element => element.id === this.defaultTabValue)
selectedTab.hidden = false
selectedBtn.classList.add("active")
} catch { }
useClickOutside(this)
}
select(event) {
// find tab with same id as clicked btn
let selectedTab = this.tabTargets.find(element => element.id === event.currentTarget.id)
if (selectedTab.hidden) {
// CLOSE CURRENT TAB
this.tabTargets.map(x => x.hidden = true) // hide all tabs
this.btnTargets.map(x => x.classList.remove("active")) // deactive all btns
selectedTab.hidden = false // show current tab
event.currentTarget.classList.add("active") // active current btn
} else {
// OPEN CURRENT TAB
this.tabTargets.map(x => x.hidden = true) // hide all tabs
this.btnTargets.map(x => x.classList.remove("active")) // deactive all btns
selectedTab.hidden = true // hide current tab
event.currentTarget.classList.remove("active") // deactive current btn
}
}
clickOutside() {
this.tabTargets.forEach(x => x.classList.add("hidden")); // hide all tabs
this.btnTargets.forEach(x => x.classList.remove("active")); // deactivate all btns
}
}
- add a CSS
.active
class:
/* app/assets/stylesheets/application.css */
.active {
color: blue;
}
- HTML required for the stimulus controller to work:
-
data-tabs-default-tab-value="two"
- optional default open tab - each
button
must have atarget="btn"
andaction="click->tabs#select"
- each
tab
must have atarget="tab"
- each
button
-tab
combination must have the sameid
<div data-controller="tabs" data-tabs-default-tab-value="two">
<button type="button" id="one" data-tabs-target="btn" data-action="click->tabs#select">UK</button>
<button type="button" id="two" data-tabs-target="btn" data-action="click->tabs#select">France</button>
<button type="button" id="abc" data-tabs-target="btn" data-action="click->tabs#select">Ukraine</button>
<div data-tabs-target="tab" id="one">
London, Glasgow
</div>
<div data-tabs-target="tab" id="two">
Paris, Lyon
</div>
<div data-tabs-target="tab" id="abc">
Kyiv, Lviv
</div>
</div>
If you know a better solution or if you can improve this controller please comment below.
That’s it!
Did you like this article? Did it save you some time?