vertical ocultar hacer desplegar desplegable como clic bootstrap javascript drop-down-menu angular rxjs

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



menu desplegable vertical html (20)

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 dentro del Asistente de Servicio .
  • 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. SetTimeout : esto es necesario porque esto es lo que sucederá si el usuario hace clic 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 turno actual del código JavaScript 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 :)!


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á 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() para hacer clic en el controlador de eventos que abre la ventana event.stopPropagation() .

PRIMA:

A continuación copio 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 :

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


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

A partir de Angular 4, la decoración preferida de 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(); } } }


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. Así que 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.


En su lugar, debe verificar si hace clic en la superposición modal, 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(); } }


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

Por lo tanto, si desea lograr la misma funcionalidad sin adjuntar 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(); } }


He elaborado una directiva para abordar este problema similar y estoy usando Bootstrap. Pero en mi caso, en lugar de esperar al 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>


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 en sí (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>


La respuesta correcta tiene un problema, si tiene un componente clicakble en su popover, el elemento ya no estará en el método contain 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!


Lo he hecho de esta manera.

Se agregó un detector de eventos al click en el documento y en ese controlador verificó si mi container contiene 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"; } } }


Me gustaría complementar la respuesta @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] ...


No hice ninguna solución. Acabo de adjuntar el documento: haga clic en mi función de alternar 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.


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 haga 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 la serigrafía.


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 un evento personalizado como directiva. Echa un vistazo a estas publicaciones de Ben Nadel:


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.


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


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


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


EL MÉTODO MÁS ELEGANTE: D

Hay una manera más fácil de hacerlo, no se necesitan 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>


NOTA: Para aquellos que deseen utilizar trabajadores web y que necesiten 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 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); }


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.