Simple dropdown with tailwindcss and stimulus
Tags: rails, tailwindcss, ruby, stimulus
Published: 2020-12-20 10:28:38

### 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 on any features but once Webpack adds support to PostCSS 8 we can re-install Tailwind and its peer dependencies using the latest tag:

```bash
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)**

```bash
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`:

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

```javascript
 // 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](https://tailwindui.com/components/application-ui/elements/dropdowns).

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](https://css-tricks.com/snippets/svg/svg-hamburger-menu/) - 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: 

```erb
<%# 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`.

```html
<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).

```erb
<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:

```javascript
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:

```erb
 <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.

```erb
<%# 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 the controller to define it and add its logic.

First thing you can do is just log something to the console to make sure that the previous html setup is working.

```javascript
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.

```javascript
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).

```javascript
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! 🎉

As I'm exploring tailwindcss and stimulus I'll be posting about here, on my [website](www.ananunesdasilva.com) and on [twitter](https://twitter.com/anakbns). I also write about other rails and web development topics, so feel free to follow and DM me.