Stimulus & Tailwind dropdown
stimulus
Tailwind
This note is now deprecated.
Tailwind 4.1 offers Custom element Vanilla JS functionality that works out of the box for Rail and you don't need to figure out this with Stimulus anymore
more: https://tailwindcss.com/blog/vanilla-js-support-for-tailwind-plus
------------------------------------------------------
Solution 1 (best one sofar)
<div class="">
<!-- Profile dropdown -->
<div class="relative ml-3" data-controller="dropdown">
<div>
<button type="button"
data-action="click->dropdown#toggle click@window->dropdown#hide"
data-dropdown-target="button"
class="relative flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
id="user-menu-button"
aria-expanded="false"
aria-haspopup="true">
<span class="absolute -inset-1.5"></span>
<span class="sr-only">Open user menu</span>
<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
</button>
</div>
<div
class="hidden absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
data-dropdown-target="menu"
aria-orientation="vertical"
aria-labelledby="user-menu-button"
tabindex="-1">
<!-- Active: "bg-gray-100", Not Active: "" -->
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-0">Your Profile</a>
<a href="#" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-1">Settings</a>
<a href="/sign_out" class="block px-4 py-2 text-sm text-gray-700" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</a>
</div>
</div>
</div>
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu", "button"]
toggle() {
if(this.menuTarget.classList.contains('hidden')) {
this.menuTarget.classList.remove('hidden')
} else {
this.menuTarget.classList.add('hidden')
}
}
hide(event) {
const buttonClicked = this.buttonTarget.contains(event.target)
if (!buttonClicked) {
this.menuTarget.classList.add('hidden')
}
}
}
Solution 2 - Button overlay hiding dropdown
source https://www.youtube.com/watch?v=TQFW3AtrDw4
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu", "hideButton"]
toggleMenu(event) {
if(this.menuTarget.classList.contains('hidden')) {
this.menuTarget.classList.remove('hidden')
this.hideButtonTarget.classList.remove('hidden')
} else {
this.hideMenu(event)
}
}
hideMenu(event) {
this.menuTarget.classList.add('hidden')
this.hideButtonTarget.classList.add('hidden')
}
}
<%# app/views/application/_menu_component.html.erb %>
<div class="relative" data-controller="dropdown">
<button
class="border-gray-200 rounded-full p-1 bg-white opacity-80"
data-action="click->dropdown#toggleMenu">
<span class="sr-only">Open options</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M10 3a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM10 8.5a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM11.5 15.5a1.5 1.5 0 10-3 0 1.5 1.5 0 003 0z" />
</svg>
</button>
<button
data-action="click->dropdown#hideMenu"
data-dropdown-target="hideButton"
class="hidden z-40 fixed left-0 right-0 top-0 bottom-0 h-full w-fulls bg-black opacity-50 cursor-default"></button>
<div data-dropdown-target="menu" class="hidden z-50 bg-white rounded-lg w-48 shadow-lg absolute right-0">
<%= yield %>
</div>
</div>
Solution 3 - with effects
source https://dev.to/mmccall10/tailwind-enter-leave-transition-effects-with-stimulus-js-5hl7
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
import {enter, leave} from 'el-transition';
// source https://dev.to/mmccall10/tailwind-enter-leave-transition-effects-with-stimulus-js-5hl7
export default class extends Controller {
static targets = ["menu", "button"]
// call the enter and leave functions
toggleMenu() {
if(this.menuTarget.classList.contains('hidden')) {
enter(this.menuTarget)
} else {
leave(this.menuTarget)
}
}
hideMenu(event) {
const buttonClicked = this.buttonTarget.contains(event.target)
if (!buttonClicked) {
leave(this.menuTarget)
}
}
}
<%# app/views/application/_menu_component.html.erb %>
<div
data-controller="dropdown"
class="relative inline-block text-left">
<div>
<button
id="<%= dom_id(record, "menu_btn") %>"
type="button"
data-dropdown-target="button"
data-action="click->dropdown#toggleMenu click@window->dropdown#hideMenu"
class="flex items-center rounded-full bg-gray-100 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-100 "
aria-expanded="true"
aria-haspopup="true">
<span class="sr-only">Open options</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M10 3a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM10 8.5a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM11.5 15.5a1.5 1.5 0 10-3 0 1.5 1.5 0 003 0z" />
</svg>
</button>
</div>
<div class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
data-dropdown-target="menu"
data-transition-enter="transition ease-out duration-100"
data-transition-enter-start="transform opacity-0 scale-95"
data-transition-enter-end="transform opacity-100 scale-100"
data-transition-leave="transition ease-in duration-75"
data-transition-leave-start="transform opacity-100 scale-100"
data-transition-leave-end="transform opacity-0 scale-95">
<div class="rounded-md bg-white shadow-xs">
<div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
<%= yield if block_given? %>
</div>
</div>
</div>
</div>
$ bin/import-map pin el-transition
Archived solution form Ahow project
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu", "button"]
toggle() {
this.menuTarget.classList.toggle("hidden")
}
hide(event) {
const buttonClicked = this.buttonTarget.contains(event.target)
if (!buttonClicked) {
this.menuTarget.classList.add('hidden')
}
}
}
<%
link_css = "block px-4 py-2 text-sm text-gray-700"
active_link_css = "bg-gray-100 text-gray-900"
%>
<div class="relative inline-block text-left" data-controller="dropdown">
<div>
<button type="button"
class="inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-50 hover:bg-gray-50"
id="menu-button"
data-action="click->dropdown#toggle click@window->dropdown#hide"
data-dropdown-target="button"
aria-expanded="true"
aria-haspopup="true">
<%= truncate current_space.title %>
<svg class="-mr-1 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
<path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
</svg>
</button>
</div>
<div data-dropdown-target="menu"
class="hidden absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
tabindex="-1">
<div class="py-1" role="none">
<% current_user.spaces.each do |space| %>
<%= link_to space.title, space_path(space), class: link_css, role: "menuitem" %>
<% end %>
</div>
</div>
</div>