Stimulus & Tailwind dropdown

Edit
equivalent Web Development
Public
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>

Payment successful

Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore.