How can I close a dropdown on click outside?

Clement picture Clement · Mar 1, 2016 · Viewed 165.1k times · Source

I would like to close my login menu dropdown when the user click anywhere outside of that dropdown, and I'd like to do that with Angular2 and with the Angular2 "approach"...

I have implemented a solution, but I really do not feel confident with it. I think there must be an easiest way to achieve the same result, so if you have any ideas ... let's discuss :) !

Here is my implementation:

The dropdown component:

This is the component for my dropdown:

  • Every time this component it set to visible, (For example: when the user click on a button to display it) it subscribe to a "global" rxjs subject userMenu stored within the SubjectsService.
  • And every time it is hidden, it unsubscribe to this subject.
  • Every click anywhere within the template of this component trigger the onClick() method, which just stop event bubbling to the top (and the application component)

Here is the code

export class UserMenuComponent {

    _isVisible: boolean = false;
    _subscriptions: Subscription<any> = null;

    constructor(public subjects: SubjectsService) {
    }

    onClick(event) {
        event.stopPropagation();
    }

    set isVisible(v) {
        if( v ){
            setTimeout( () => {
this._subscriptions =  this.subjects.userMenu.subscribe((e) => {
                       this.isVisible = false;
                       })
            }, 0);
        } else {
            this._subscriptions.unsubscribe();
        }
        this._isVisible = v;
    }

    get isVisible() {
        return this._isVisible;
    }
}

The application component:

On the other hand, there is the application component (which is a parent of the dropdown component):

  • This component catch every click event and emit on the same rxjs Subject (userMenu)

Here is the code:

export class AppComponent {

    constructor( public subjects: SubjectsService) {
        document.addEventListener('click', () => this.onClick());
    }
    onClick( ) {
        this.subjects.userMenu.next({});
    }
}

What bother me:

  1. I do not feel really comfortable with the idea of having a global Subject that act as the connector between those components.
  2. The setTimeout: This is needed because here is what happen otherwise if the user click on the button that show the dropdown:
    • The user click on the button (which is not a part of the dropdown component) to show the dropdown.
    • The dropdown is displayed and it immediately subscribe to the userMenu subject.
    • The click event bubble up to the app component and gets caught
    • The application component emit an event on the userMenu subject
    • The dropdown component catch this action on userMenu and hide the dropdown.
    • At the end the dropdown is never displayed.

This set timeout delay the subscription to the end of the current JavaScript code turn which solve the problem, but in a very in elegant way in my opinion.

If you know cleaner, better, smarter, faster or stronger solutions, please let me know :) !

Answer

Sasxa picture Sasxa · Mar 1, 2016

You can use (document:click) event:

@Component({
  host: {
    '(document:click)': 'onClick($event)',
  },
})
class SomeComponent() {
  constructor(private _eref: ElementRef) { }

  onClick(event) {
   if (!this._eref.nativeElement.contains(event.target)) // or some similar check
     doSomething();
  }
}

Another approach is to create custom event as a directive. Check out these posts by Ben Nadel: