Angular Dropdown Menu Close On Click Outside
Angular Dropdown Menu Close On Click Outside

In this post I want to show you how to create click outside directive inside Angular. And if you are wondering why do you need such directive this is because every single time when we want to close a modal, tooltip or dropdown menu we must implement click outside.

This is why let's create together reusable click outside directive for your next project.

Creating dropdown component

I already generated an empty Angular project. And I want to implement click outside directive with a real usage. This is why we will create a dropdown component with custom click outside directive.

Let's first add markup for our dropdown.

<!-- app.component.html -->

<div class="menu-container">
  <div class="menu-toggler">Toggle menu</div>
  <div class="menu">
    <div class="menu-item">News</div>
    <div class="menu-item">Contacts</div>
    <div class="menu-item">About</div>
  </div>
</div>

We added markup with container, button to toggle our menu and menu itself which will be shown.

Now let's add some styles.

# src/styles.css

.menu-container {
  width: 200px;
  background: #04aa6d;
  font-size: 20px;
}

.menu-toggler {
  padding: 10px;
}

.menu {
  background: #333;
  color: #fff;
  padding: 10px;
}

.menu-item {
  cursor: pointer;
  padding: 5px 0;
}

.menu-item:hover {
  text-decoration: underline;
}

Here is how it looks in browser.

Dropdown markup

Now let's add some basic behaviour to close dropdown on click.

<div class="menu-container">
  <div class="menu-toggler" (click)="toggleMenu()">Toggle menu</div>
  <div class="menu" *ngIf="isMenuOpened">
    <div class="menu-item">News</div>
    <div class="menu-item">Contacts</div>
    <div class="menu-item">About</div>
  </div>
</div>
export class AppComponent {
  isMenuOpened: boolean = false;

  toggleMenu(): void {
    this.isMenuOpened = !this.isMenuOpened;
  }
}

We successfully implemented our click behaviour in our dropdown. But we are still missing click outside feature which is exactly the point of this video.

It's time to create our click outside directive.

// src/app/clickOutside.ts

@Directive({
  selector: '[clickOutside]',
})
export class ClickOutsideDirective {
  @Output() clickOutside = new EventEmitter<void>();

  constructor(
    private element: ElementRef,
    @Inject(DOCUMENT) private document: Document
  ) {}
}

Here we created out directive which has an output which will be fired when we are clicking outside. In our constructor we injected element and document references to get access to direct DOM elements of them later.

// src/app/clickOutside.ts

export class ClickOutsideDirective implements AfterViewInit, OnDestroy {
  @Output() clickOutside = new EventEmitter<void>();

  documentClickSubscription: Subscription | undefined;

  constructor(
    private element: ElementRef,
    @Inject(DOCUMENT) private document: Document
  ) {}

  ngAfterViewInit(): void {
    this.documentClickSubscription = fromEvent(this.document, 'click')
      .pipe(
        filter((event) => {
          return !this.isInside(event.target as HTMLElement);
        })
      )
      .subscribe(() => {
        this.clickOutside.emit();
      });
  }

  ngOnDestroy(): void {
    this.documentClickSubscription?.unsubscribe();
  }

  isInside(elementToCheck: HTMLElement): boolean {
    return (
      elementToCheck === this.element.nativeElement ||
      this.element.nativeElement.contains(elementToCheck)
    );
  }
}

Here we did a lot of stuff.

  1. We used ngAfterViewInit we wait until our DOM element will be rendered in DOM
  2. We created a stream with fromEvent which we bind to document. It will be triggered every single time when we click on document
  3. We filter our clicking with isInside function where we check if we clicked inside current element or not
  4. If we clicked outside we emit out output to notify our parent about clicking outside

Important moment here is to unsubscribe from out stream when our element is destroyed. In other case we will have a subscription which will hang there forever.

Now it's time to register our directive.

@NgModule({
  declarations: [AppComponent, ClickOutsideDirective],
  ...
})
export class AppModule {}

And we can use it now inside our dropdown component.

<div class="menu-container clickOutside (clickOutside)="clickedOutside()">
  <div class="menu-toggler" (click)="toggleMenu()">Toggle menu</div>
  <div class="menu" *ngIf="isMenuOpened">
    <div class="menu-item">News</div>
    <div class="menu-item">Contacts</div>
    <div class="menu-item">About</div>
  </div>
</div>

Here we used clickOutside directive and created clickOutside event which should close our dropdown.


export class AppComponent {
  isMenuOpened: boolean = false;

  toggleMenu(): void {
    this.isMenuOpened = !this.isMenuOpened;
  }

  clickedOutside(): void {
    this.isMenuOpened = false;
  }
}

As you can see in browser, we can close our dropdown now with click outside. And this directive is fully reusable in any project. And if you are interested how we can implement nested comments in Angular make sure to check this post also.

📚 Source code of what we've done