Search code examples
javascriptangulardrop-down-menudropdowntailwind-css

Angular & TailwindCSS - Dropdown menu


Apologies if this has been answered before, but I have been searching for a couple of days to find an answer and I have not come across a solution that makes sense for my setup (perhaps my setup is wrong, let find out).

I would like to create a dropdown menu using Angular with Tailwind CSS, My component html is as follows:

// recipe-details.component.html
<div>
  <div *ngIf="selectedRecipe;else noRecipe">
    <div class="flex items-center justify-between">
      <h3 class="text-title">{{ selectedRecipe.name }}</h3>

      <!-- The dropdown menu -->
      <div class="relative">

        <!-- The dropdown button -->
        <a href="#" class="bg-green-500 px-3 py-2 rounded-md shadow font-bold text-white flex items-center" appDropdown>Manage recipe
          <svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
            xmlns="http://www.w3.org/2000/svg">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
          </svg></a>

          <!-- Dropdown list of links -->
          <div class="origin-top-right absolute right-0 w-full rounded mt-2 shadow-lg border-gray-800">
            <div class="bg-white py-2">
              <a href="#" class="block px-4 py-2 font-bold text-sm text-gray-700 hover:bg-gray-100">Edit Recipe</a>
              <a href="#" class="block px-4 py-2 font-bold text-sm text-gray-700 hover:bg-gray-100">Delete Recipe</a>
            </div>
          </div>

      </div>

    </div>
    <p>{{ selectedRecipe.description }}</p>
  </div>

  <ng-template #noRecipe>
    <div class="">
      <div class="text-title ">Choose a recipe</div>
      <p>Please choose a recipe from the recipe list.</p>
    </div>
  </ng-template>
</div>

For the menu button I have created a directive with selector appDropdown:

//dropdown.directive.ts
import { Directive, EventEmitter, HostListener, Output } from '@angular/core';

@Directive({
  selector: '[appDropdown]'
})
export class DropdownDirective {
  @Output() isMenuOpen = new EventEmitter<boolean>();
  isOpen: boolean = false;

  constructor() { }

  @HostListener('click') toggleMenu() {
    this.isOpen = !this.isOpen;
    this.isMenuOpen.emit(this.isOpen);
    console.log(`Menu status changed. Menu is now ${this.isOpen ? 'Open' : 'Closed'}`)
  }

}

If my way of thinking so far makes sense, does it make further sense to handle the EventEmitter in another directive and place it on the dropdown list of links div. The directive would use a HostBinding to apply css to set the opacity of the div between opacity-0 and opacity-100 whether isOpen is true or false?

If this approach works, I am not sure how I would listen for the Event from the first directive within the second directive.

If I am completely off the mark, does anyone have a solution that would help me understand the workflow?

Thanks!


Solution

  • I guess writing my question down this way helped me come up with a solution. If anyone has a way to improve my code, feel free to post other ways it can be done :)

    My solution:

    //recipe-detail.component.html
    <div>
      <div *ngIf="selectedRecipe;else noRecipe">
        <div class="flex items-center justify-between">
          <h3 class="text-title">{{ selectedRecipe.name }}</h3>
          <div class="relative">
            <a href="#" class="bg-green-500 px-3 py-2 rounded-md shadow font-bold text-white flex items-center" (isMenuOpen)=toggleVisible($event) appDropdown>Manage recipe
              <svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"
                xmlns="http://www.w3.org/2000/svg">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
              </svg></a>
              <div class="origin-top-right absolute right-0 w-full rounded mt-2 shadow-lg border-gray-800" [ngClass]="visibilityClasses">
                <div class="bg-white py-2">
                  <a href="#" class="block px-4 py-2 font-bold text-sm text-gray-700 hover:bg-gray-100">Edit Recipe</a>
                  <a href="#" class="block px-4 py-2 font-bold text-sm text-gray-700 hover:bg-gray-100">Delete Recipe</a>
                </div>
              </div>
          </div>
        </div>
        <p>{{ selectedRecipe.description }}</p>
      </div>
    
      <ng-template #noRecipe>
        <div class="">
          <div class="text-title ">Choose a recipe</div>
          <p>Please choose a recipe from the recipe list.</p>
        </div>
      </ng-template>
    </div>
    
    //recipe-detail.component.ts
    import { Component, OnInit } from '@angular/core';
    import { Recipe } from 'src/app/models/models.i';
    import { RecipeService } from '../../../services/recipes/recipe.service'
    
    @Component({
      selector: 'app-recipe-detail',
      templateUrl: './recipe-detail.component.html',
      styleUrls: ['./recipe-detail.component.scss']
    })
    export class RecipeDetailComponent implements OnInit {
      selectedRecipe: Recipe;
      visibilityClasses: {};
      private isVisible: boolean = false;
    
      constructor(private recipeService: RecipeService) { }
    
      ngOnInit(): void {
        this.recipeService.currentRecipe.subscribe(recipe => this.selectedRecipe = recipe
        );
        this.setVisibilityClasses();
      }
    
      toggleVisible(isVisible: boolean): void {
        console.log(`is the menu open: ${isVisible ? 'Yes' : 'No'}`)
        this.isVisible = isVisible;
        this.setVisibilityClasses();
      }
    
      private setVisibilityClasses(): void {
        this.visibilityClasses = {
          'opacity-0': !this.isVisible,
          'opacity-100': this.isVisible
        };
      }
    }
    
    //dropdown.directive.ts
    import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
    
    @Directive({
      selector: '[appDropdown]'
    })
    export class DropdownDirective {
      @Output() isMenuOpen = new EventEmitter<boolean>();
      isOpen: boolean = false;
    
      constructor() { }
    
      @HostListener('click') toggleMenu() {
        this.isOpen = !this.isOpen;
        this.isMenuOpen.emit(this.isOpen);
        console.log(`Menu status changed. Menu is now ${this.isOpen ? 'Open' : 'Closed'}`)
      }
    }