Como posso fechar uma lista suspensa ao clicar fora?

144

Gostaria de fechar meu menu suspenso de login quando o usuário clicar em qualquer lugar fora desse menu suspenso e gostaria de fazer isso com o Angular2 e com a "abordagem" do Angular2 ...

Eu implementei uma solução, mas realmente não me sinto confiante com ela. Eu acho que deve haver uma maneira mais fácil de obter o mesmo resultado, então se você tiver alguma idéia ... vamos discutir :)!

Aqui está a minha implementação:

O componente suspenso:

Este é o componente para o meu menu suspenso:

  • Toda vez que este componente que definir a visível, (Por exemplo: quando o usuário clique em um botão para exibi-lo) que assinar um assunto "global" rxjs usermenu armazenado dentro do SubjectsService .
  • E toda vez que está oculto, cancela a inscrição neste assunto.
  • Cada qualquer clique dentro do modelo deste gatilho componente do onClick () método, que apenas parada evento bolha ao topo (e o componente de aplicação)

Aqui está o 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;
    }
}

O componente do aplicativo:

Por outro lado, existe o componente de aplicativo (que é o pai do componente suspenso):

  • Esse componente captura todos os eventos de clique e emite no mesmo assunto rxjs ( userMenu )

Aqui está o código:

export class AppComponent {

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

O que me incomoda:

  1. Não me sinto muito à vontade com a ideia de ter um Assunto global que atue como o conector entre esses componentes.
  2. O setTimeout : Isso é necessário, porque é o que acontece caso contrário, se o usuário clicar no botão que mostra o menu suspenso:
    • O usuário clica no botão (que não faz parte do componente suspenso) para mostrar o menu suspenso.
    • O menu suspenso é exibido e ele se inscreve imediatamente no assunto userMenu .
    • O evento click clica no componente do aplicativo e é capturado
    • O componente de aplicativo emite um evento no assunto userMenu
    • O componente suspenso captura essa ação no userMenu e oculta o menu suspenso.
    • No final, o menu suspenso nunca é exibido.

Esse tempo limite definido atrasa a assinatura até o final do código JavaScript atual, que resolve o problema, mas de uma maneira muito elegante na minha opinião.

Se você souber soluções mais limpas, melhores, mais inteligentes, mais rápidas ou mais fortes, entre em contato :)!

Clemente
fonte
Estas respostas podem lhe dar algumas idéias: stackoverflow.com/a/35028820/215945 , stackoverflow.com/questions/35024495#35024651
Mark Rajcok

Respostas:

245

Você pode usar o (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();
  }
}

Outra abordagem é criar evento personalizado como uma diretiva. Confira estes posts de Ben Nadel:

Sasxa
fonte
1
@Sxaxa obrigado e concordou. Imaginei que, se houvesse um documento API não obsoleto, ele teria aparecido na pesquisa que me levou até aqui.
danludwig
4
Se event.target for um elemento que foi adicionado dinamicamente através de algo como uma ligação [innerHTML], o nativeElement do elementRef não o conterá.
Patrick Graham
8
A única desvantagem dessa técnica é que agora você tem um ouvinte de evento de clique no seu aplicativo que é acionado toda vez que você clica.
Codepic
37
De acordo com o guia de estilo oficial do Angular 2, você deve usar em @HostListener('document:click', ['$event'])vez de hostpropriedade no Componentdecorador.
Michał Miszczyszyn 8/02
15
ou você pode simplesmente usar rxjs para ele, como Observable.fromEvent(document, 'click').subscribe(event => {your code here}), assim você sempre pode se inscrever somente quando você precisa ouvir, por exemplo, você abriu suspensa, e quando você fechá-lo você Baixa
Cego Despair
42

MÉTODO ELEGANTE

Encontrei esta clickOutdiretiva: https://github.com/chliebel/angular2-click-outside . Verifico e funciona bem (apenas copio clickOutside.directive.tspara o meu projeto). Você pode usá-lo desta maneira:

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

Onde closeestá sua função que será chamada quando o usuário clicar fora de div. É uma maneira muito elegante de lidar com o problema descrito em questão.

Se você usar a diretiva acima para fechar a janela popUp, lembre-se de adicionar o event.stopPropagation()manipulador de eventos ao clique no botão que abre o popUp.

BÔNUS:

Abaixo, copio o código da diretiva original do arquivo clickOutside.directive.ts(caso o link pare de funcionar no futuro) - o autor é Christian Liebel :

Kamil Kiełczewski
fonte
2
@Vega Minha recomendação seria usar a diretiva em um elemento com * ngIf, no caso de dropdowns, isso pode ser algo como:<div class="wrap" *ngIf="isOpened" (clickOutside)="...// this should set this.isOpen=false"
Gabriel Balsa Cantú
19

Eu fiz dessa maneira.

Adicionado um ouvinte de evento no documento clicke nesse manipulador verificado se o meu containercontém event.target, se não - ocultar o menu suspenso.

Seria assim.

@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";
        }
    }
}
Tony
fonte
Oi. O.bind (isto) é necessário?
Drenai 27/07/16
1
@ Brian Pode ou não ser necessário, mas definitivamente não seria se ele envolvesse a this.offClickHandlerfunção de seta.
Lansana Camara #
17

Acho que Sasxa aceitou resposta funciona para a maioria das pessoas. No entanto, tive uma situação em que o conteúdo do elemento, que deveria ouvir eventos fora do clique, mudou dinamicamente. Portanto, o Elements nativeElement não continha o event.target, quando foi criado dinamicamente. Eu poderia resolver isso com a seguinte diretiva

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

Em vez de verificar se elementRef contém event.target, verifico se elementRef está no caminho (caminho DOM para o destino) do evento. Dessa forma, é possível manipular elementos criados dinamicamente.

JuHarm89
fonte
Graças - isso funciona melhor quando os componentes estão presentes crianças
Mahsan
Isso foi muito útil para mim. não sei por que, fora do componente, o clique não foi detectado com outras respostas.
JavaQuest 20/08/19
13

Se você estiver fazendo isso no iOS, use o touchstart evento, bem como:

A partir do Angular 4, a HostListenerdecoração é a maneira preferida de fazer isso

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

}
Xavier
fonte
10

Estamos trabalhando em um problema semelhante no trabalho hoje, tentando descobrir como fazer com que uma div suspensa desapareça quando ela é clicada. A nossa é um pouco diferente da pergunta do pôster inicial, porque não queremos clicar fora de um componente ou diretiva diferente , mas apenas fora da div específica.

Acabamos resolvendo isso usando o manipulador de eventos (window: mouseup).

Etapas:
1.) Atribuímos a todo o menu suspenso um nome de classe exclusivo.

2.) No próprio menu suspenso (a única parte em que desejamos clicar para NÃO fechar o menu), adicionamos um manipulador de eventos (window: mouseup) e passamos o evento $.

NOTA: Não foi possível fazer isso com um manipulador de "clique" típico, porque isso entrava em conflito com o manipulador de clique pai.

3.) Em nosso controller, criamos o método que queríamos chamar no evento click out e usamos o event.closest ( documentos aqui ) para descobrir se o ponto clicado está dentro da nossa classe de destino.

 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>

Paige Bolduc
fonte
"window: mouseup" deve ser usado no decorador do host.
Shivam
@ Shivam-- Não sei ao certo o que você quer dizer com "deve ser usado no decorador do host". Você poderia explicar melhor? Obrigado!
Paige Bolduc 21/10
Quero dizer, em vez de usar o objeto "window" diretamente, você deve usar a propriedade "host" do decorador de componentes / decorador "HostListener" do componente. Essa é uma prática padrão ao trabalhar com "janela" ou objeto "documento" em angular 2.
Shivam
2
Basta manter um olho para a compatibilidade do navegador, .closest()não é suportada no IE / Borda a partir de hoje ( caniuse )
superjos
5

Você pode criar um elemento irmão no menu suspenso que cubra a tela inteira que seria invisível e estaria lá apenas para capturar eventos de clique. Em seguida, você pode detectar cliques nesse elemento e fechar a lista suspensa quando é clicada. Digamos que esse elemento seja da classe silkscreen, aqui está um estilo para ele:

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

O índice z precisa ser alto o suficiente para posicioná-lo acima de tudo, exceto do menu suspenso. Nesse caso, meu menu suspenso seria z-index 2.

As outras respostas funcionaram em alguns casos para mim, exceto que, às vezes, meu menu suspenso fechava quando eu interagia com elementos dentro dele e não queria isso. Eu adicionei dinamicamente elementos que não estavam contidos no meu componente, de acordo com o destino do evento, como eu esperava. Em vez de resolver essa bagunça, imaginei que tentaria da maneira serigráfica.

Patrick Graham
fonte
5

Eu não fiz nenhuma solução alternativa. Acabei de anexar o documento: clique na minha função de alternância da seguinte forma:

    @Directive ({
      seletor: '[appDropDown]'
    })
    classe de exportação DropdownDirective implementa OnInit {

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

      construtor (elemRef privado: ElementRef) {}

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

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

Então, quando estou fora da minha diretiva, fecho a lista suspensa.

Elie Nehmé
fonte
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;
    }
}

DOWNSIDES: - Dois ouvintes de eventos de clique para cada um desses componentes na página. Não use isso em componentes que estão na página centenas de vezes.

Alex Egli
fonte
Não, eu usei apenas no navegador de desktop.
precisa
3

Gostaria de complementar a resposta do @Tony, pois o evento não está sendo removido após o clique fora do componente. Recibo completo:

  • Marque seu elemento principal com #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);
  • No elemento clicável, use:

    (click)="dropstatus=true"

Agora você pode controlar seu estado suspenso com a variável dropstatus e aplicar classes apropriadas com [ngClass] ...

Gauss
fonte
3

Você pode escrever a diretiva:

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


}

No seu 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 diretiva emite um evento quando o elemento html está contido no DOM e quando a propriedade de entrada [clickOut] é 'true'. Ele escuta o evento do mouse para manipular o evento antes que o elemento seja removido do DOM.

E uma observação: o Firefox não contém a propriedade 'caminho' no evento, você pode usar a função para criar o caminho:

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

Portanto, você deve alterar o manipulador de eventos na diretiva: event.path deve ser substituído getEventPath (event)

Este módulo pode ajudar. https://www.npmjs.com/package/ngx-clickout Ele contém a mesma lógica, mas também manipula o evento esc no elemento html de origem.

Alex Mikitevich
fonte
3

A resposta correta tem um problema: se você tiver um componente clicakble na sua popover, o elemento não estará mais no containmétodo e será fechado, com base no @ JuHarm89 que criei o meu:

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

Obrigado pela ajuda!

Douglas Caina
fonte
2

Uma versão melhor para a ótima solução @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");

        }
    }
}

Em um arquivo css: // NÃO é necessário se você usar o menu suspenso de auto-inicialização.

.ourDropdown{
   display: none;
}
.ourDropdown.open{
   display: inherit;
}
Dudi
fonte
2

Você deve verificar se clica na sobreposição modal, muito mais fácil.

Seu modelo:

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

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


  }
Stefdelec
fonte
2

Eu fiz uma diretiva para resolver esse problema semelhante e estou usando o Bootstrap. Mas, no meu caso, em vez de aguardar o evento click fora do elemento para fechar o menu suspenso aberto atual, acho melhor se observarmos o evento 'mouseleave' para fechar automaticamente o menu.

Aqui está a minha solução:

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>
Lemuel Layola Apa
fonte
1

Se você estiver usando o Bootstrap, poderá fazê-lo diretamente com o modo Bootstrap através de menus suspensos (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>

Agora não há problema em colocar (click)="clickButton()"coisas no botão. http://getbootstrap.com/javascript/#dropdowns

vusan
fonte
1

Eu também fiz uma pequena solução alternativa.

Criei um evento (dropdownOpen) que escuto no meu componente de elemento ng-select e chamo uma função que fechará todos os outros SelectComponent's abertos, exceto o SelectComponent aberto no momento.

Modifiquei uma função dentro do arquivo select.ts como abaixo para emitir o 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);
}

No HTML, adicionei um ouvinte de evento 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 é minha função de chamada no acionador de eventos dentro do componente que possui a tag 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();
    })
}
Gaurav Pandvia
fonte
1

NOTA: Para aqueles que desejam usar trabalhadores da Web e você precisa evitar o uso de documentos e elementos nativos, isso funcionará.

Respondi à mesma pergunta aqui: /programming/47571144

Copie / cole no link acima:

Eu tive o mesmo problema quando criava um menu suspenso e uma caixa de diálogo de confirmação que queria descartá-los ao clicar fora.

Minha implementação final funciona perfeitamente, mas requer algumas animações e estilo do CSS3.

NOTA : não testei o código abaixo; pode haver alguns problemas de sintaxe que precisam ser resolvidos, além dos ajustes óbvios para o seu próprio projeto!

O que eu fiz:

Eu fiz uma div fixa separada com altura 100%, largura 100% e transformação: scale (0), este é essencialmente o plano de fundo, você pode estilizá-lo com a cor de fundo: rgba (0, 0, 0, 0,466); para tornar óbvio, o menu é aberto e o plano de fundo é clicado para fechar. O menu obtém um índice z mais alto do que tudo o resto, então a div background obtém um índice z mais baixo do que o menu, mas também mais alto do que tudo o resto. Em seguida, o plano de fundo possui um evento de clique que fecha a lista suspensa.

Aqui está o seu 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>

Aqui está o css3 que precisa de algumas animações 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;
    }
}

Se você não está procurando nada visual, basta usar uma transição 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);
}
Shannon
fonte
1

Me deparei com outra solução, inspirada em exemplos com evento de foco / desfoque.

Portanto, se você deseja obter a mesma funcionalidade sem anexar o ouvinte de documento global, considere válido o exemplo a seguir. Funciona também no Safari e Firefox no OSx, apesar de terem outras formas de manipulação do evento de foco de botão: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus

Exemplo de trabalho no stackbiz com angular 8: https://stackblitz.com/edit/angular-sv4tbi?file=src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts

Marcação 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>

A diretiva ficará assim:

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();
  }
}
Andrei Shekhau
fonte
1

Você pode usar mouseleavena sua opinião assim

Teste com angular 8 e trabalhe perfeitamente

<ul (mouseleave)="closeDropdown()"> </ul>
Tony Ngo
fonte
Isso fechará o contêiner quando o mouse sair, mas obrigado por compartilhar de qualquer maneira, pois eu desconhecia sua existência.
Ben Hayward
0

O MÉTODO MAIS ELEGANTE: D

Existe uma maneira mais fácil de fazer isso, sem necessidade de diretrizes para isso.

"elemento que alterna seu menu suspenso" deve ser a tag do botão. Use qualquer método no atributo (desfoque). Isso é tudo.

<button class="element-that-toggle-your-dropdown"
               (blur)="isDropdownOpen = false"
               (click)="isDropdownOpen = !isDropdownOpen">
</button>
George Reznichenko
fonte
Isso não funcionará se você quiser manter a lista suspensa aberta ao clicar, por exemplo, um usuário pode perder o clique em um botão
Sim