Stimulus & Tailwind dropdown

Edit
equivalent
Public
stimulus
Tailwind



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

// 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


// 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