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




9 Answers

Puede usar el evento (document:click) :

@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 eventos personalizados como una directiva. Echa un vistazo a estas publicaciones de Ben Nadel:

vertical sin

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

Implementé una solución, pero realmente no me siento seguro con eso. Creo que debe haber una manera más fácil de lograr el mismo resultado, así que si tienes alguna idea ... vamos a discutir :)!

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 usuario de rx- rx sujeto "global" que se almacena en el servicio de temas .
  • 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 desencadena el método onClick () , que solo detiene el burbujeo de eventos en la parte superior (y el componente de la aplicación)

Aquí está el código

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 uno de los padres del componente desplegable):

  • Este componente captura cada evento de clic y emite en el mismo sujeto 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 el conector entre esos componentes.
  2. El setTimeout : Esto es necesario porque aquí está lo que sucede de otra manera si el usuario hace clic en el botón que muestra el menú desplegable:
    • El usuario hace clic en el botón (que no forma parte del componente desplegable) para mostrar el menú desplegable.
    • Se muestra el menú desplegable e inmediatamente se suscribe al tema userMenu .
    • El evento de clic burbujea hasta el componente de la aplicación y queda atrapado
    • El componente de aplicación emite un evento en el tema userMenu
    • El componente desplegable capta 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 turno del código JavaScript actual que resuelve el problema, pero de una manera 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 :)!




MÉTODO ELEGANTE : Encontré esta directiva clickOut : https://github.com/chliebel/angular2-click-outside

Lo compruebo y funciona bien (solo copio clickOutside.directive.ts en mi proyecto). U puede usarlo de esta manera:

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

Donde close está tu función, que será llamada cuando el usuario haga clic fuera de div. Es una forma muy elegante de manejar el problema descrito en cuestión.

=== Código de directiva ===

Debajo copio el código de directiva oryginal del archivo clickOutside.directive.ts (en caso de que el enlace deje de funcionar en el futuro) - el autor es Christian Liebel :

import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angular/core';

@Directive({
    selector: '[clickOutside]'
})
export class ClickOutsideDirective {
    constructor(private _elementRef: ElementRef) {
    }

    @Output()
    public clickOutside = new EventEmitter<MouseEvent>();

    @HostListener('document:click', ['$event', '$event.target'])
    public onClick(event: MouseEvent, targetElement: HTMLElement): void {
        if (!targetElement) {
            return;
        }

        const clickedInside = this._elementRef.nativeElement.contains(targetElement);
        if (!clickedInside) {
            this.clickOutside.emit(event);
        }
    }
}



Hemos estado trabajando en un problema similar en el trabajo hoy, tratando de descubrir cómo hacer desaparecer un elemento desplegable al hacer clic en él. La nuestra es ligeramente diferente a la pregunta inicial del póster porque no queríamos hacer clic fuera de un componente o directiva diferente, sino meramente fuera de la div específica.

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

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

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

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

3.) En nuestro controlador, creamos el método que queríamos que se llame en el evento de hacer clic, y usamos el evento.closest ( documentos aquí ) para averiguar si el lugar en el que se hizo clic se encuentra dentro de nuestro div de clase específica.

 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>




Podría crear un elemento hermano en el menú desplegable que cubra toda la pantalla que sería invisible y estar allí solo para capturar eventos de clic. Luego, puede detectar los clics en ese elemento y cerrar el menú desplegable al hacer clic en él. Digamos que ese elemento es de serigrafía de clase, aquí hay un poco de estilo para él:

.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 menos de 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 algunas veces mi menú desplegable se cerró cuando interactué 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 ordenar ese desastre, pensé que lo haría de la manera de la serigrafía.




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

DOWNSIDES: - Dos oyentes de eventos de clic 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.




Si está utilizando Bootstrap, puede hacerlo directamente con el método bootstrap a través de las listas 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()" en el botón. http://getbootstrap.com/javascript/#dropdowns




La respuesta correcta tiene un problema, si tienes un componente clicable en tu popover, el elemento ya no estará en el método contenedo y se cerrará, basado en @ JuHarm89 creé el mío propio:

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!




Puedes escribir una 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 tu 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á conteniendo en DOM y cuando la propiedad de entrada [clickOut] es 'verdadera'. Escucha el evento mousedown para manejar el evento antes de que el elemento sea eliminado de DOM.

Y una nota: firefox no contiene 'ruta' de propiedad en caso de que pueda usar la función para crear una 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 (evento)

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 fuente html.




NOTA: Para aquellos que deseen utilizar web workers y deben evitar el uso de document y nativeElement, esto funcionará.

Respondí la misma pregunta aquí: https://.com/questions/47571144

Copiar / Pegar desde el enlace de arriba:

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

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

NOTA : no he probado el siguiente código, es posible que haya problemas de sintaxis que deba solucionarse, ¡también los ajustes obvios para su propio proyecto!

Lo que hice:

Hice un div fijo separado con altura 100%, ancho 100% y transformación: escala (0), esto es esencialmente el fondo, puedes darle un estilo con color de fondo: rgba (0, 0, 0, 0.466); para hacer obvio el menú está abierto y el fondo es 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 algo 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);
}



Related

javascript drop-down-menu angular rxjs