¿Cómo puedo cerrar un menú desplegable al hacer clic afuera?


144

Me gustaría cerrar mi menú desplegable de inicio de sesión cuando el usuario haga clic en cualquier lugar fuera de ese menú desplegable, y me gustaría hacerlo con Angular2 y con el "enfoque" de Angular2 ...

He implementado una solución, pero realmente no me siento seguro con ella. Creo que debe haber una manera más fácil de lograr el mismo resultado, así que si tienes alguna idea ... ¡discutamos :)!

Aquí está mi implementación:

El componente desplegable:

Este es el componente para mi menú desplegable:

  • Cada vez que este componente se establece en visible, (por ejemplo: cuando el usuario hace clic en un botón para mostrarlo) se suscribe a un sujeto de usuario "global" rxjs almacenado en el servicio de sujetos .
  • Y cada vez que está oculto, se da de baja de este tema.
  • Cada clic en cualquier lugar dentro de la plantilla de este componente activa el método onClick () , que simplemente detiene el burbujeo de eventos en la parte superior (y el componente de la aplicación)

Aqui esta el codigo

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;
    }
}

El componente de la aplicación:

Por otro lado, está el componente de la aplicación (que es un padre del componente desplegable):

  • Este componente captura cada evento de clic y emite en el mismo Asunto rxjs ( userMenu )

Aquí está el código:

export class AppComponent {

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

Lo que me molesta:

  1. No me siento realmente cómodo con la idea de tener un Sujeto global que actúe como conector entre esos componentes.
  2. El setTimeout : Esto es necesario porque esto es lo que sucede lo contrario, si el usuario pulsa en el botón que muestra el menú desplegable:
    • El usuario hace clic en el botón (que no es parte del componente desplegable) para mostrar el menú desplegable.
    • Se muestra el menú desplegable e inmediatamente se suscribe al asunto UserMenu .
    • El evento de clic aparece en el componente de la aplicación y queda atrapado
    • El componente de la aplicación emite un evento sobre el tema userMenu
    • El componente desplegable captura esta acción en userMenu y oculta el menú desplegable.
    • Al final, el menú desplegable nunca se muestra.

Este tiempo de espera establecido retrasa la suscripción al final del código JavaScript actual que resuelve el problema, pero en una forma muy elegante en mi opinión.

Si conoce soluciones más limpias, mejores, más inteligentes, más rápidas o más fuertes, ¡hágamelo saber :)!


Respuestas:


245

Puedes usar el (document:click)evento:

@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();
  }
}

Otro enfoque es crear un evento personalizado como directiva. Echa un vistazo a estas publicaciones de Ben Nadel:


1
@Sasxa gracias, y estuvo de acuerdo. Pensé que si hubiera un documento API no obsoleto, habría aparecido en la búsqueda que me llevó aquí.
danludwig

44
Si event.target es un elemento que se agregó dinámicamente a través de un enlace [innerHTML], el elemento nativo de elementRef no lo contendrá.
Patrick Graham el

8
El único inconveniente de esta técnica es que ahora tiene un detector de eventos de clic en su aplicación que se activa cada vez que hace clic.
codeepic

37
Según la guía de estilo oficial de Angular 2, debe usar en @HostListener('document:click', ['$event'])lugar de la hostpropiedad en el Componentdecorador.
Michał Miszczyszyn

15
o simplemente puede usar rxjs para eso, Observable.fromEvent(document, 'click').subscribe(event => {your code here})por lo que siempre puede suscribirse solo cuando necesita escuchar, por ejemplo, abrió el menú desplegable, y cuando lo cierra se da de baja
Blind Despair

43

MÉTODO ELEGANTE

Encontré esta clickOutdirectiva: https://github.com/chliebel/angular2-click-outside . Lo compruebo y funciona bien (solo copio clickOutside.directive.tsa mi proyecto). U puede usarlo de esta manera:

<div (clickOutside)="close($event)"></div>

¿Dónde closeestá su función que se llamará cuando el usuario haga clic fuera de div. Es una forma muy elegante de manejar el problema descrito en cuestión.

Si usa la directiva anterior para cerrar la ventana emergente, recuerde primero agregar event.stopPropagation()al controlador de eventos de clic de botón que abre la ventana emergente.

PRIMA:

A continuación, copie el código de directiva original del archivo clickOutside.directive.ts(en caso de que el enlace deje de funcionar en el futuro), el autor es Christian Liebel :


2
@Vega Mi recomendación sería utilizar la Directiva en un elemento con * ngSi, en el caso de los menús desplegables, esto puede ser algo como<div class="wrap" *ngIf="isOpened" (clickOutside)="...// this should set this.isOpen=false"
Gabriel Balsa Cantú

19

Lo he hecho de esta manera.

Se agregó un detector de eventos en el documento clicky en ese controlador verificó si mi containercontiene event.target, si no, oculte el menú desplegable.

Se vería así.

@Component({})
class SomeComponent {
    @ViewChild('container') container;
    @ViewChild('dropdown') dropdown;

    constructor() {
        document.addEventListener('click', this.offClickHandler.bind(this)); // bind on doc
    }

    offClickHandler(event:any) {
        if (!this.container.nativeElement.contains(event.target)) { // check click origin
            this.dropdown.nativeElement.style.display = "none";
        }
    }
}

Hola. ¿Es necesario el enlace (esto)?
Drenai

1
@Brian Puede o no ser necesario, pero definitivamente no lo sería si envolviera la this.offClickHandlerfunción de flecha.
Lansana Camara

17

Creo que Sasxa aceptó que la respuesta funciona para la mayoría de las personas. Sin embargo, tuve una situación en la que el contenido del Elemento, que debería escuchar eventos fuera de clic, cambió dinámicamente. Por lo tanto, Elements nativeElement no contenía event.target, cuando se creó dinámicamente. Podría resolver esto con la siguiente directiva

@Directive({
  selector: '[myOffClick]'
})
export class MyOffClickDirective {

  @Output() offClick = new EventEmitter();

  constructor(private _elementRef: ElementRef) {
  }

  @HostListener('document:click', ['$event.path'])
  public onGlobalClick(targetElementPath: Array<any>) {
    let elementRefInPath = targetElementPath.find(e => e === this._elementRef.nativeElement);
    if (!elementRefInPath) {
      this.offClick.emit(null);
    }
  }
}

En lugar de verificar si elementRef contiene event.target, verifico si elementRef está en la ruta (ruta DOM al destino) del evento. De esa manera es posible manejar Elementos creados dinámicamente.


Gracias - esto funciona mejor cuando los componentes secundarios están presentes
MAhsan

Esto fue de mucha ayuda para mí. No estoy seguro de por qué el clic fuera del componente no se detectó con otras respuestas.
JavaQuest

13

Si está haciendo esto en iOS, use el touchstartevento también:

A partir de Angular 4, la HostListenerdecoración es la forma preferida de hacer esto

import { Component, OnInit, HostListener, ElementRef } from '@angular/core';
...
@Component({...})
export class MyComponent implement OnInit {

  constructor(private eRef: ElementRef){}

  @HostListener('document:click', ['$event'])
  @HostListener('document:touchstart', ['$event'])
  handleOutsideClick(event) {
    // Some kind of logic to exclude clicks in Component.
    // This example is borrowed Kamil's answer
    if (!this.eRef.nativeElement.contains(event.target) {
      doSomethingCool();
    }
  }

}

10

Hemos estado trabajando en un problema similar en el trabajo hoy, tratando de descubrir cómo hacer que un div desplegable desaparezca cuando se desactiva. La nuestra es ligeramente diferente a la pregunta del póster inicial porque no queríamos alejarnos de un componente o directiva diferente , sino simplemente fuera del div particular.

Terminamos resolviéndolo usando el controlador de eventos (window: mouseup).

Pasos:
1.) Le dimos a todo el menú desplegable div un nombre de clase único.

2.) En el menú desplegable interno (la única parte en la que queríamos que los clics NO cerraran el menú), agregamos un controlador de eventos (ventana: mouseup) y pasamos el evento $.

NOTA: No se pudo hacer con un controlador de "clic" típico porque esto entraba en conflicto con el controlador de clic principal.

3.) En nuestro controlador, creamos el método al que queríamos que se nos llamara en el evento click out, y usamos event.closest ( docs here ) para averiguar si el punto cliqueado está dentro de nuestro div de clase objetivo.

 autoCloseForDropdownCars(event) {
        var target = event.target;
        if (!target.closest(".DropdownCars")) { 
            // do whatever you want here
        }
    }
 <div class="DropdownCars">
   <span (click)="toggleDropdown(dropdownTypes.Cars)" class="searchBarPlaceholder">Cars</span>
   <div class="criteriaDropdown" (window:mouseup)="autoCloseForDropdownCars($event)" *ngIf="isDropdownShown(dropdownTypes.Cars)">
   </div>
</div>


"window: mouseup" debe usarse dentro del decorador de host.
Shivam

@ Shivam-- No estoy seguro de lo que quieres decir con "debería usarse dentro del decorador de host". ¿Podrías explicar más? ¡Gracias!
Paige Bolduc

Quiero decir que en lugar de usar el objeto "ventana" directamente, debe usar la propiedad "host" del decorador de componentes / decorador "HostListener" del componente. Esa es una práctica estándar mientras se trabaja con objetos "ventana" o "documento" en angular 2.
Shivam

2
Solo tenga en cuenta la compatibilidad del navegador, .closest()no es compatible con IE / Edge a partir de hoy ( caniuse )
superjos

5

Puede crear un elemento hermano en el menú desplegable que cubra toda la pantalla que sería invisible y esté allí solo para capturar eventos de clic. Entonces podría detectar clics en ese elemento y cerrar el menú desplegable cuando se hace clic. Digamos que ese elemento es de serigrafía de clase, aquí hay algo de estilo para ello:

.silkscreen {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1;
}

El índice z debe ser lo suficientemente alto como para colocarlo por encima de todo, excepto su menú desplegable. En este caso, mi menú desplegable sería b z-index 2.

Las otras respuestas funcionaron en algunos casos para mí, excepto que a veces mi menú desplegable se cerraba cuando interactuaba con elementos dentro de él y no quería eso. Agregué dinámicamente elementos que no estaban contenidos en mi componente, de acuerdo con el objetivo del evento, como esperaba. En lugar de resolver ese desastre, pensé que lo intentaría a la manera de serigrafía.


5

No hice ninguna solución. Acabo de adjuntar el documento: haga clic en mi función de alternar de la siguiente manera:

    @Directiva({
      selector: '[appDropDown]'
    })
    export class DropdownDirective implementa OnInit {

      @HostBinding ('class.open') isOpen: boolean;

      constructor (elemRef privado: ElementRef) {}

      ngOnInit (): void {
        this.isOpen = false;
      }

      @HostListener ('documento: clic', ['$ evento'])
      @HostListener ('document: touchstart', ['$ event'])
      alternar (evento) {
        if (this.elemRef.nativeElement.contains (event.target)) {
          this.isOpen =! this.isOpen;
        } más {
          this.isOpen = false;
      }
    }

Entonces, cuando estoy fuera de mi directiva, cierro el menú desplegable.


4
import { Component, HostListener } from '@angular/core';

@Component({
    selector: 'custom-dropdown',
    template: `
        <div class="custom-dropdown-container">
            Dropdown code here
        </div>
    `
})
export class CustomDropdownComponent {
    thisElementClicked: boolean = false;

    constructor() { }

    @HostListener('click', ['$event'])
    onLocalClick(event: Event) {
        this.thisElementClicked = true;
    }

    @HostListener('document:click', ['$event'])
    onClick(event: Event) {
        if (!this.thisElementClicked) {
            //click was outside the element, do stuff
        }
        this.thisElementClicked = false;
    }
}

DOWNNSIDES: - Escucha de eventos con dos clics para cada uno de estos componentes en la página. No use esto en componentes que están en la página cientos de veces.


No, solo lo he usado en el navegador de escritorio.
Alex Egli

3

Me gustaría complementar la respuesta de @Tony, ya que el evento no se elimina después del clic fuera del componente. Recibo completo:

  • Marque su elemento principal con #container

    @ViewChild('container') container;
    
    _dropstatus: boolean = false;
    get dropstatus() { return this._dropstatus; }
    set dropstatus(b: boolean) 
    {
        if (b) { document.addEventListener('click', this.offclickevent);}
        else { document.removeEventListener('click', this.offclickevent);}
        this._dropstatus = b;
    }
    offclickevent: any = ((evt:any) => { if (!this.container.nativeElement.contains(evt.target)) this.dropstatus= false; }).bind(this);
  • En el elemento en el que se puede hacer clic, use:

    (click)="dropstatus=true"

Ahora puede controlar su estado desplegable con la variable dropstatus y aplicar las clases adecuadas con [ngClass] ...


3

Puedes escribir la directiva:

@Directive({
  selector: '[clickOut]'
})
export class ClickOutDirective implements AfterViewInit {
  @Input() clickOut: boolean;

  @Output() clickOutEvent: EventEmitter<any> = new EventEmitter<any>();

  @HostListener('document:mousedown', ['$event']) onMouseDown(event: MouseEvent) {

       if (this.clickOut && 
         !event.path.includes(this._element.nativeElement))
       {
           this.clickOutEvent.emit();
       }
  } 


}

En su componente:

@Component({
  selector: 'app-root',
  template: `
    <h1 *ngIf="isVisible" 
      [clickOut]="true" 
      (clickOutEvent)="onToggle()"
    >{{title}}</h1>
`,
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  title = 'app works!';

  isVisible = false;

  onToggle() {
    this.isVisible = !this.isVisible;
  }
}

Esta directiva emite un evento cuando el elemento html está contenido en DOM y cuando la propiedad de entrada [clickOut] es 'true'. Escucha el evento mousedown para manejar el evento antes de que el elemento se elimine del DOM.

Y una nota: firefox no contiene la propiedad 'ruta' en caso de que pueda usar la función para crear la ruta:

const getEventPath = (event: Event): HTMLElement[] => {
  if (event['path']) {
    return event['path'];
  }
  if (event['composedPath']) {
    return event['composedPath']();
  }
  const path = [];
  let node = <HTMLElement>event.target;
  do {
    path.push(node);
  } while (node = node.parentElement);
  return path;
};

Por lo tanto, debe cambiar el controlador de eventos en la directiva: event.path debe reemplazarse getEventPath (event)

Este módulo puede ayudar. https://www.npmjs.com/package/ngx-clickout Contiene la misma lógica pero también maneja el evento esc en el elemento html de origen.


3

La respuesta correcta tiene un problema, si tiene un componente clicakble en su popover, el elemento ya no estará en el containmétodo y se cerrará, según @ JuHarm89 que creé el mío:

export class PopOverComponent implements AfterViewInit {
 private parentNode: any;

  constructor(
    private _element: ElementRef
  ) { }

  ngAfterViewInit(): void {
    this.parentNode = this._element.nativeElement.parentNode;
  }

  @HostListener('document:click', ['$event.path'])
  onClickOutside($event: Array<any>) {
    const elementRefInPath = $event.find(node => node === this.parentNode);
    if (!elementRefInPath) {
      this.closeEventEmmit.emit();
    }
  }
}

¡Gracias por la ayuda!


2

Una mejor versión para la gran solución @Tony:

@Component({})
class SomeComponent {
    @ViewChild('container') container;
    @ViewChild('dropdown') dropdown;

    constructor() {
        document.addEventListener('click', this.offClickHandler.bind(this)); // bind on doc
    }

    offClickHandler(event:any) {
        if (!this.container.nativeElement.contains(event.target)) { // check click origin

            this.dropdown.nativeElement.closest(".ourDropdown.open").classList.remove("open");

        }
    }
}

En un archivo css: // NO es necesario si usa el menú desplegable bootstrap.

.ourDropdown{
   display: none;
}
.ourDropdown.open{
   display: inherit;
}

2

Debería verificar si hace clic en la superposición modal en su lugar, mucho más fácil.

Su plantilla:

<div #modalOverlay (click)="clickOutside($event)" class="modal fade show" role="dialog" style="display: block;">
        <div class="modal-dialog" [ngClass]='size' role="document">
            <div class="modal-content" id="modal-content">
                <div class="close-modal" (click)="closeModal()"> <i class="fa fa-times" aria-hidden="true"></i></div>
                <ng-content></ng-content>
            </div>
        </div>
    </div>

Y el método:

  @ViewChild('modalOverlay') modalOverlay: ElementRef;

// ... your constructor and other method

      clickOutside(event: Event) {
    const target = event.target || event.srcElement;
    console.log('click', target);
    console.log("outside???", this.modalOverlay.nativeElement == event.target)
    // const isClickOutside = !this.modalBody.nativeElement.contains(event.target);
    // console.log("click outside ?", isClickOutside);
    if ("isClickOutside") {
      // this.closeModal();
    }


  }

2

He elaborado una directiva para abordar este problema similar y estoy usando Bootstrap. Pero en mi caso, en lugar de esperar el evento de clic fuera del elemento para cerrar el menú desplegable abierto actual, creo que es mejor si vigilamos el evento 'mouseleave' para cerrar automáticamente el menú.

Aquí está mi solución:

Directiva

import { Directive, HostListener, HostBinding } from '@angular/core';
@Directive({
  selector: '[appDropdown]'
})
export class DropdownDirective {

  @HostBinding('class.open') isOpen = false;

  @HostListener('click') toggleOpen() {
    this.isOpen = !this.isOpen;
  }

  @HostListener('mouseleave') closeDropdown() {
    this.isOpen = false;
  }

}

HTML

<ul class="nav navbar-nav navbar-right">
    <li class="dropdown" appDropdown>
      <a class="dropdown-toggle" data-toggle="dropdown">Test <span class="caret"></span>
      </a>
      <ul class="dropdown-menu">
          <li routerLinkActive="active"><a routerLink="/test1">Test1</a></li>
          <li routerLinkActive="active"><a routerLink="/test2/">Test2</a></li>
      </ul>
    </li>
</ul>

1

Si está utilizando Bootstrap, puede hacerlo directamente con bootstrap mediante menús desplegables (componente Bootstrap).

<div class="input-group">
    <div class="input-group-btn">
        <button aria-expanded="false" aria-haspopup="true" class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">
            Toggle Drop Down. <span class="fa fa-sort-alpha-asc"></span>
        </button>
        <ul class="dropdown-menu">
            <li>List 1</li>
            <li>List 2</li>
            <li>List 3</li>
        </ul>
    </div>
</div>

Ahora está bien poner (click)="clickButton()"cosas en el botón. http://getbootstrap.com/javascript/#dropdowns


1

También hice una pequeña solución por mi cuenta.

Creé un evento (dropdownOpen) que escucho en mi componente de elemento ng-select y llamo a una función que cerrará todos los demás SelectComponent abiertos aparte del SelectComponent abierto actualmente.

Modifiqué una función dentro del archivo select.ts como a continuación para emitir el evento:

private open():void {
    this.options = this.itemObjects
        .filter((option:SelectItem) => (this.multiple === false ||
        this.multiple === true && !this.active.find((o:SelectItem) => option.text === o.text)));

    if (this.options.length > 0) {
        this.behavior.first();
    }
    this.optionsOpened = true;
    this.dropdownOpened.emit(true);
}

En el HTML agregué un detector de eventos para (dropdownOpened) :

<ng-select #elem (dropdownOpened)="closeOtherElems(elem)"
    [multiple]="true"
    [items]="items"
    [disabled]="disabled"
    [isInputAllowed]="true"
    (data)="refreshValue($event)"
    (selected)="selected($event)"
    (removed)="removed($event)"
    placeholder="No city selected"></ng-select>

Esta es mi función de llamada en el desencadenante de eventos dentro del componente que tiene la etiqueta ng2-select:

@ViewChildren(SelectComponent) selectElem :QueryList<SelectComponent>;

public closeOtherElems(element){
    let a = this.selectElem.filter(function(el){
                return (el != element)
            });

    a.forEach(function(e:SelectComponent){
        e.closeDropdown();
    })
}

1

NOTA: Para aquellos que deseen utilizar trabajadores web y que necesiten evitar usar document y nativeElement, esto funcionará.

Respondí la misma pregunta aquí: /programming/47571144

Copiar / Pegar desde el enlace de arriba:

Tuve el mismo problema cuando estaba haciendo un menú desplegable y un diálogo de confirmación que quería descartarlos al hacer clic afuera.

Mi implementación final funciona perfectamente pero requiere algunas animaciones y estilos de css3.

NOTA : no he probado el siguiente código, puede haber algunos problemas de sintaxis que deben resolverse, ¡también los ajustes obvios para su propio proyecto!

Lo que hice:

Hice un div fijo separado con altura 100%, ancho 100% y transform: scale (0), esto es esencialmente el fondo, puedes peinarlo con background-color: rgba (0, 0, 0, 0.466); para hacer obvio que el menú está abierto y el fondo es hacer clic para cerrar. El menú obtiene un índice z más alto que todo lo demás, luego el div de fondo obtiene un índice z más bajo que el menú pero también más alto que todo lo demás. Luego, el fondo tiene un evento de clic que cierra el menú desplegable.

Aquí está con su código html.

<div class="dropdownbackground" [ngClass]="{showbackground: qtydropdownOpened}" (click)="qtydropdownOpened = !qtydropdownOpened"><div>
<div class="zindex" [class.open]="qtydropdownOpened">
  <button (click)="qtydropdownOpened = !qtydropdownOpened" type="button" 
         data-toggle="dropdown" aria-haspopup="true" [attr.aria-expanded]="qtydropdownOpened ? 'true': 'false' ">
   {{selectedqty}}<span class="caret margin-left-1x "></span>
 </button>
  <div class="dropdown-wrp dropdown-menu">
  <ul class="default-dropdown">
      <li *ngFor="let quantity of quantities">
       <a (click)="qtydropdownOpened = !qtydropdownOpened;setQuantity(quantity)">{{quantity  }}</a>
       </li>
   </ul>
  </div>
 </div>

Aquí está el css3 que necesita algunas animaciones simples.

/* make sure the menu/drop-down is in front of the background */
.zindex{
    z-index: 3;
}

/* make background fill the whole page but sit behind the drop-down, then
scale it to 0 so its essentially gone from the page */
.dropdownbackground{
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 2;
    transform: scale(0);
    opacity: 0;
    background-color: rgba(0, 0, 0, 0.466);
}

/* this is the class we add in the template when the drop down is opened
it has the animation rules set these how you like */
.showbackground{
    animation: showBackGround 0.4s 1 forwards; 

}

/* this animates the background to fill the page
if you don't want any thing visual you could use a transition instead */
@keyframes showBackGround {
    1%{
        transform: scale(1);
        opacity: 0;
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

Si no buscas nada visual, puedes usar una transición como esta

.dropdownbackground{
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 2;
    transform: scale(0);
    opacity: 0;
    transition all 0.1s;
}

.dropdownbackground.showbackground{
     transform: scale(1);
}

1

Encontré otra solución, inspirada en ejemplos con eventos de enfoque / desenfoque.

Por lo tanto, si desea lograr la misma funcionalidad sin adjuntar el escucha de documentos global, puede considerar como válido el siguiente ejemplo. Funciona también en Safari y Firefox en OSx, a pesar de que tienen otro manejo del evento de foco de botón: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus

Ejemplo de trabajo en stackbiz con angular 8: https://stackblitz.com/edit/angular-sv4tbi?file=src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts

Marcado HTML:

<div class="dropdown">
  <button class="btn btn-secondary dropdown-toggle" type="button" aria-haspopup="true" aria-expanded="false">Dropdown button</button>
  <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
    <a class="dropdown-item" href="#">Action</a>
    <a class="dropdown-item" href="#">Another action</a>
    <a class="dropdown-item" href="#">Something else here</a>
  </div>
</div>

La directiva se verá así:

import { Directive, HostBinding, ElementRef, OnDestroy, Renderer2 } from '@angular/core';

@Directive({
  selector: '.dropdown'
})
export class ToggleDropdownDirective {

  @HostBinding('class.show')
  public isOpen: boolean;

  private buttonMousedown: () => void;
  private buttonBlur: () => void;
  private navMousedown: () => void;
  private navClick: () => void;

  constructor(private element: ElementRef, private renderer: Renderer2) { }

  ngAfterViewInit() {
    const el = this.element.nativeElement;
    const btnElem = el.querySelector('.dropdown-toggle');
    const menuElem = el.querySelector('.dropdown-menu');

    this.buttonMousedown = this.renderer.listen(btnElem, 'mousedown', (evt) => {
      console.log('MOUSEDOWN BTN');
      this.isOpen = !this.isOpen;
      evt.preventDefault(); // prevents loose of focus (default behaviour) on some browsers
    });

    this.buttonMousedown = this.renderer.listen(btnElem, 'click', () => {
      console.log('CLICK BTN');
      // firefox OSx, Safari, Ie OSx, Mobile browsers.
      // Whether clicking on a <button> causes it to become focused varies by browser and OS.
      btnElem.focus();
    });

    // only for debug
    this.buttonMousedown = this.renderer.listen(btnElem, 'focus', () => {
      console.log('FOCUS BTN');
    });

    this.buttonBlur = this.renderer.listen(btnElem, 'blur', () => {
      console.log('BLUR BTN');
      this.isOpen = false;
    });

    this.navMousedown = this.renderer.listen(menuElem, 'mousedown', (evt) => {
      console.log('MOUSEDOWN MENU');
      evt.preventDefault(); // prevents nav element to get focus and button blur event to fire too early
    });
    this.navClick = this.renderer.listen(menuElem, 'click', () => {
      console.log('CLICK MENU');
      this.isOpen = false;
      btnElem.blur();
    });
  }

  ngOnDestroy() {
    this.buttonMousedown();
    this.buttonBlur();
    this.navMousedown();
    this.navClick();
  }
}

1

Puede usar mouseleaveen su vista así

Prueba con angular 8 y funciona perfectamente

<ul (mouseleave)="closeDropdown()"> </ul>

Esto cerrará el contenedor cuando el mouse se vaya, pero gracias por compartir de todos modos ya que no estaba al tanto de su existencia.
Ben Hayward

0

EL MÉTODO MÁS ELEGANTE: D

Hay una manera más fácil de hacer eso, no necesita directivas para eso.

"element-that-toggle-your-dropdown" debe ser una etiqueta de botón. Utilice cualquier método en el atributo (desenfoque). Eso es todo.

<button class="element-that-toggle-your-dropdown"
               (blur)="isDropdownOpen = false"
               (click)="isDropdownOpen = !isDropdownOpen">
</button>

Esto no funcionará si desea mantener el menú desplegable abierto al hacer clic, por ejemplo, un usuario podría perder hacer clic en un botón
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.