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


Answers

Lo hice de esta manera.

Se agregó un detector de eventos al click en el documento y en ese controlador se verificó si mi container contiene event.target , de lo contrario, se oculta 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";
        }
    }
}
Question

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 :)!




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

Creé un evento (dropdownOpen) que escuché en mi componente ng-select y llamé a una función que cerrará todos los otros SelectComponent's abiertos aparte del SelectComponent actualmente abierto.

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



Creo que Sasxa aceptó 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 sin hacer clic, cambió dinámicamente. Por lo tanto, los elementos nativeElement no contenían el 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, compruebo si elementRef está en la ruta (ruta DOM al destino) del evento. De esta forma, es posible manejar Elementos creados dinámicamente.




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


    @Directive({
      selector: '[appDropDown]'
    })
    export class DropdownDirective implements OnInit {

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

      constructor(private elemRef: ElementRef) { }

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

      @HostListener('document:click', ['$event'])
      @HostListener('document:touchstart', ['$event'])
      toggle(event) {
        if (this.elemRef.nativeElement.contains(event.target)) {
          this.isOpen = !this.isOpen;
        } else {
          this.isOpen = false;
      }
    }

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




Una mejor versión para la gran solución de @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 de arranque.

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



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 cliqueable, use:

    (click)="dropstatus=true"
    

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




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


  }



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

A partir de Angular 4, la decoración HostListener es la forma preferida de hacerlo

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

}





Related