Simple dropdown with tailwindcss and stimulus

rails

stimulus

tailwindcss

Dec, 2020

Simple dropdown with tailwindcss and stimulus

I'm surprised how easy it is to create UI components with tailwindcss and stimulus.

1. Install or upgrade tailwindcss (v2.0)

Tailwindcss v2.0 depends on PostCSS 8. Currently, Webpack only supports PostCSS 7 so we will have to install a compatibility build:

yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

Tailwind assures that we're not missing any features but once Webpack adds support to PostCSS 8 we can re-install Tailwind and its peer dependencies using the latest tag:

yarn uninstall tailwindcss @tailwindcss/postcss7-compat
yarn install tailwindcss@latest postcss@latest autoprefixer@latest

2. Install stimulus in your rails app (if you haven't already)

bundle exec rails webpacker:install:stimulus

This will create two files, an index.js that loads all stimulus controllers and an example of a controller under home_controller.js.

app/javascript/controllers/index.js:

// Load all the controllers within this directory and all subdirectories.   
// Controller files must be named *_controller.js.                          

import { Application } from "stimulus"                                      
import { definitionsFromContext } from "stimulus/webpack-helpers"           

const application = Application.start()                                     
const context = require.context("controllers", true, /_controller\.js$/)    
application.load(definitionsFromContext(context))

app/javascript/controllers/home_controller.js

 // Visit The Stimulus Handbook for more details                         
 // <https://stimulusjs.org/handbook/introduction>                         
 //                                                                      
 // This example controller works with specially annotated HTML like:    
 //                                                                      
 // <div data-controller="hello">                                        
 //   <h1 data-target="hello.output"></h1>                               
 // </div>                                                               

 import { Controller } from "stimulus"                                   

 export default class extends Controller {                               
   static targets = [ "output" ]                                         

   connect() {                                                           
     this.outputTarget.textContent = 'Hello, Stimulus!'                  
   }                                                                     
 }

I'm going to rename home_controller.js to menu_controller.js and use it as a specific controller that will handle all navigation functionalities. For now, I will only add the dropdown toggle logic but in the future, I might add more (e.g. dark mode functionality).

That easy, ready to go!

3. Add the button and dropdown HTML

In my case, I'm going to add this button to my navbar.

Tailwindcss has great free components we can use so I'm going to copy the code of the simple dropdown available here.

I only want to show this button on small screens so I changed the button's svg to a hamburger - that I copied and adjusted from css tricks - and I also added a tailwindcss lg:hidden class to hide the button on large screens.

The default state of the dropdown should be hidden but at this point, it is always showing. We can add tailwind's hidden class to solve that. Still, if you click on the button nothing happens yet. The dropdown does not show because we need javascript for that. This is where stimulus comes in.

At this point, this is the component's HTML:

<%# Adding lg:hidden here to hide the button on large screens %> 
<div class="relative inline-block pt-10 text-left lg:hidden">   

   <%# Hamburger button %>                                                                                                                        
   <div>                                                                                                                                                                        
     <button type="button" class="inline-flex justify-center w-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500" 
       id="options-menu" aria-haspopup="true" aria-expanded="true">                                                                                                                 
       <svg viewBox="0 0 100 80" width="60" height="60">                                                                                                                            
           <rect width="100" height="20" rx="8"></rect>                                                                                                                             
           <rect y="30" width="100" height="20" rx="8"></rect>                                                                                                                      
           <rect y="60" width="100" height="20" rx="8"></rect>                                                                                                                      
       </svg>                                                                                                                                                                       
     </button>                                                                                                                                                                      
   </div>                                                                                                                                                                           

   <%# Dropdown menu with a default hidden class %>                                                                                                                                                                        
   <div class="hidden absolute right-0 w-56 mt-2 bg-white shadow-lg origin-top-right rounded-md ring-1 ring-black ring-opacity-5">                                                         
     <div class="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">                                                                                      
       <a href="/about" class="block px-4 py-2 text-4xl text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">About</a>                                              
       <a href="/posts" class="block px-4 py-2 text-4xl text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">Posts</a>                                              
     </div>                                                                                                                                                                         
   </div>                                                                                                                                                                           
 </div>

4. Add the toggle functionality

First, we need to connect the menu controller to the component. Stimulus does that using a data-controller attribute that accepts the name of the controller as a value:

This should be placed in the component's opening div.

<div data-controller="menu" class="relative inline-block pt-10 text-center lg:hidden">

Now, I will have to make the dropdown element available to the controller and for that stimulus uses a data-[identifier]-target attribute, where the identifier is the name of the controller.

Note: If you are using stimulus 1.0 the attribute is data-target and not data-[identifier]-target. Version 2.0 still accepts data-target but be aware that this will be deprecated.

I want to target the dropdown element so I'll add this attribute to its opening div and I'll give it a toggleable value (as in something that can be toggled).

<div data-menu-target="toggleable" class="absolute right-0 hidden mt-2 bg-white shadow-lg w-96 origin-top-right rounded-md ring-1 ring-black ring-opacity-5">

For this to work, this toggleable target will need to be registered in the controller:

import { Controller } from "stimulus"                  

 export default class extends Controller {              
   static targets = [ "toggleable" ]                    

   // toggle function will be here                                                    
 }

Now that we have the target, we need to add a click event to the hamburger button. This is the event that will trigger the toggle function that in turn will use the registered target to change the CSS.

Stimulus uses a data-action attribute that accepts an event type followed by the function we want to trigger. So, on the button element we will add:

 <div data-action="click->menu#toggle">   

Have a look at the final HTML where I've commented above the elements where I placed these three attributes.

<%# data-controller with the name of the controller that will listen to the events on this element %>                                                                              
  <div data-controller="menu" class="relative inline-block pt-10 text-center lg:hidden">                                                                                             

<%# data-action with the event type that will trigger a toggle function on the menu controller %>                                                                                  
    <div data-action="click->menu#toggle">                                                                                                                                           
      <button type="button" class="inline-flex justify-center w-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500" 
        id="options-menu" aria-haspopup="true" aria-expanded="true">                                                                                                                 
        <svg viewBox="0 0 100 80" width="60" height="60">                                                                                                                            
            <rect width="100" height="20" rx="8"></rect>                                                                                                                             
            <rect y="30" width="100" height="20" rx="8"></rect>                                                                                                                      
            <rect y="60" width="100" height="20" rx="8"></rect>                                                                                                                      
        </svg>                                                                                                                                                                       
      </button>                                                                                                                                                                      
    </div>                                                                                                                                                                           

  <%# data-target - tells the controller which elements it should target %>                                                                                                                         
    <div data-menu-target="toggleable" class="absolute right-0 hidden mt-2 bg-white shadow-lg w-96 origin-top-right rounded-md ring-1 ring-black ring-opacity-5">                    
      <div class="py-6" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">                                                                                      
        <a href="/about" class="block px-4 py-5 text-5xl text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">About</a>                                              
        <a href="/posts" class="block px-4 py-5 text-5xl text-gray-700 hover:bg-gray-100 hover:text-gray-900" role="menuitem">Posts</a>                                              
      </div>                                                                                                                                                                         
    </div>                                                                                                                                                                           
  </div>

We've added the toggle function to the HTML and now we need to go to the controller to define it and add its logic.

The first thing you can do is just log something to the console to make sure that the previous HTML setup is working.

import { Controller } from "stimulus"       

 export default class extends Controller {   
   static targets = [ "toggleable" ]         

   toggle() {
        console.log('it works')
     }        
 }

Or you can also log the target element. If it's working properly you should see the dropdown HTML on your browser's console.

import { Controller } from "stimulus"       

 export default class extends Controller {   
   static targets = [ "toggleable" ]         

   toggle() {
        console.log(this.toggleableTarget)
     }        
 }

The final step will be to make menu#toggle call the javascript classList toggle function that will add/remove the hidden class to/from the toggleable target (our dropdown element).

import { Controller } from "stimulus"       

 export default class extends Controller {   
   static targets = [ "toggleable" ]         

   toggle() {
         this.toggleableTarget.classList.toggle('hidden')
     }        
 }

That's it! Check how it toggles! 🎉