Comment puis-je fermer un menu déroulant clic à l'extérieur?

je voudrais fermer mon menu de connexion lorsque l'utilisateur clique n'importe où en dehors de ce menu, et je voudrais le faire avec Angular2 et avec L'Angular2"approche"...

j'ai mis en place une solution, mais je ne me sens vraiment pas en confiance avec elle. Je pense qu'il doit y avoir une meilleure façon d'atteindre le même résultat, donc si vous avez des idées ... parlons-en :) !

Voici mon implémentation:

de La liste déroulante composant:

c'est le composant pour ma descente:

  • chaque fois que ce composant est mis à visible, (par exemple: lorsque l'utilisateur clique sur un bouton pour l'afficher) il s'abonne à un sujet "global" rxjs userMenu stocké dans le SubjectsService .
  • et chaque fois qu'il est caché, il se désabonne de ce sujet.
  • chaque clic n'importe où à l'intérieur le modèle de cet élément déclencheur de la onClick() la méthode, qui vient de s'arrêter de remontée d'événements vers le haut (et la demande)

voici le code

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

des composants de L'application:

d'un autre côté, il y a le composant application (qui est un parent du composant dropdown):

  • ce composant capte chaque événement de clic et émet sur le même sujet rxjs ( userMenu )

voici le code:

export class AppComponent {

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

Ce qui me dérange:

  1. Je ne suis pas vraiment à l'aise avec l'idée d'avoir un sujet global qui agit comme le connecteur entre ces composants.
  2. le setTimeout : C'est nécessaire parce que voici ce qui se passe autrement si l'utilisateur clique sur le bouton qui montrent la liste déroulante:
    • l'utilisateur clique sur le bouton (qui ne fait pas partie du composant dropdown) pour afficher le dropdown.
    • le dropdown est affiché et il souscrivent immédiatement au sujet userMenu .
    • la bulle d'événement de clic jusqu'à la composante de l'application et se fait attraper
    • la demande composant émettent un événement sur userMenu sujet
    • de L'élément déroulant attraper cette action sur les userMenu et masquer la liste déroulante.
    • à la fin, le dropdown n'est jamais affiché.

ce set timeout retarder l'abonnement à la fin de L'actuel JavaScript code turn qui résout le problème, mais d'une manière très élégante dans mon avis.

Si vous en savez plus propre, mieux, plus intelligemment, plus rapidement ou des solutions plus fortes, s'il vous plaît laissez-moi savoir :) !

107
demandé sur Peter Mortensen 2016-03-01 03:14:25

17 réponses

vous pouvez utiliser (document:click) événement:

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

une autre approche consiste à créer un événement personnalisé en tant que directive. Regardez ces messages de Ben Nadel:

189
répondu Sasxa 2017-10-13 03:29:07

j'ai fait comme ça.

a ajouté un écouteur d'événements sur le document click et dans ce handler vérifié si mon container contient event.target , si non - Cacher le dropdown.

ça ressemblerait à ça.

@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";
        }
    }
}
17
répondu Tony 2016-05-29 11:12:44

méthode élégante : j'ai trouvé cette directive clickOut : https://github.com/chliebel/angular2-click-outside

je le vérifie et il fonctionne bien (je ne copie clickOutside.directive.ts à mon projet). Vous pouvez l'utiliser de cette façon:

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

close est votre fonction qui sera appel lorsque l'utilisateur clique à l'extérieur de div. Il est très élégant façon de traiter le problème décrit en question.

= = = code de la Directive = = =

ci - dessous je copie le code de la directive originale du fichier clickOutside.directive.ts (au cas où le lien cesserait de fonctionner à l'avenir) - l'auteur est 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);
        }
    }
}
17
répondu Kamil Kiełczewski 2018-04-03 12:38:02

je pense que sasxa réponse acceptée fonctionne pour la plupart des gens. Cependant, j'ai eu une situation où le contenu de l'Élément, qui doit les écouter hors-cliquez sur événements, modifiés dynamiquement. Ainsi, les éléments nativeElement n'ont pas contenu l'événement.cible, quand il a été créé dynamiquement. Je pourrais résoudre cela avec la directive suivante

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

au lieu de vérifier si elementRef contient l'événement.cible, je vérifie si elementRef est dans le chemin (DOM chemin à cible) de la événement. De cette façon, il est possible de gérer les éléments créés dynamiquement.

11
répondu JuHarm89 2017-02-26 21:59:21

Nous avons travaillé sur un problème similaire à l'œuvre aujourd'hui, en essayant de comprendre comment faire une liste déroulante div disparaître lorsqu'il est cliqué. La nôtre est légèrement différente de la question initiale de l'affiche parce que nous ne voulions pas cliquer à l'écart d'un autre composant ou directive , mais simplement à l'extérieur de la div particulière.

nous avons fini par le résoudre en utilisant le gestionnaire d'événements (window:mouseup).

Suit:

1.) Nous avons donné au menu déroulant entier div un nom de classe unique.



2.) Sur le menu déroulant interne lui-même (la seule partie que nous voulions clics pour ne pas fermer le menu), nous avons ajouté un handler d'événement (window:mouseup) et passé dans l'événement$.



NOTE: cela ne pouvait pas être fait avec un handler "clic" typique parce que cela entrait en conflit avec le clic parent manipulateur.



3. Dans notre contrôleur, nous avons créé la méthode que nous voulions être appelée sur le click événement, et nous utilisons l'événement.le plus proche ( docs ici ) pour savoir si l'endroit cliqué est dans notre div de classe ciblée.

 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>
7
répondu Paige Bolduc 2016-06-15 17:52:47

si vous faites cela sur iOS, utilisez aussi l'événement touchstart :

à partir de L'angle 4, le HostListener décorer est la meilleure façon de le faire

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

}
6
répondu Xavier 2017-09-04 01:14:44

vous pourriez créer un élément apparenté à la liste déroulante qui couvre l'écran entier qui serait invisible et être là juste pour capturer les évènements de clic. Ensuite, vous pouvez détecter les clics sur cet élément et fermer le dropdown quand il est cliqué. Disons que cet élément est de classe sérigraphie, voici un certain style pour elle:

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

le z-index doit être assez haut pour le placer au-dessus de tout sauf votre dropdown. Dans ce cas, ma liste déroulante serait b z-index 2.

[Traduction]

les autres réponses ont fonctionné dans certains cas pour moi, sauf que, parfois, ma liste déroulante a pris fin lorsque j'ai interagi avec des éléments à l'intérieur de celle-ci et que je ne le voulais pas. J'avais dynamiquement ajouté des éléments qui n'étaient pas contenus dans mon composant, selon la cible de l'événement, comme je m'y attendais. Plutôt que de régler ce problème, je me suis dit que j'essaierais à la sérigraphie.

3
répondu Patrick Graham 2017-01-12 06:55:07

je souhaiterais compléter @Tony réponse, puisque l'événement n'est pas supprimé après le clic à l'extérieur de la composante. Réception complète:

  • marquez votre élément principal avec #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);
    
  • sur l'élément cliquable, utiliser:

    (click)="dropstatus=true"
    

Maintenant vous pouvez contrôler votre état de descente avec la variable dropstatus, et appliquer correctement classes avec [ngClass]...

2
répondu Gauss 2017-02-18 01:27:28

la bonne réponse a un problème, si vous avez un composant clicakble dans votre popover, l'élément ne sera plus sur la méthode contain et se fermera, basé sur @JuHarm89 j'ai créé le mien:

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

Merci pour l'aide!

2
répondu Douglas Caina 2017-12-11 17:25:47
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;
    }
}

inconvénients: - Deux écouteurs d'événements de clic pour chacun de ces composants sur la page. Ne l'utilisez pas sur des composants qui sont sur la page des centaines de fois.

1
répondu Alex Egli 2017-06-05 15:43:13

vous devriez vérifier si vous cliquez sur la superposition modale à la place, beaucoup plus facile.

votre modèle:

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

et la méthode:

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


  }
1
répondu Stefdelec 2017-07-31 08:55:34

si vous utilisez Bootstrap, vous pouvez le faire directement avec bootstrap way via dropdowns (Bootstrap component).

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

maintenant, il est possible de mettre (click)="clickButton()" stuff sur le bouton. http://getbootstrap.com/javascript/#dropdowns

1
répondu vusan 2017-08-29 20:17:52

Je n'ai rien fait. J'ai juste joint le document:cliquez sur ma fonction de bascule comme suit:


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

donc, quand je suis en dehors de mes directives, je ferme la descente.

1
répondu Elie Nehmé 2017-10-30 18:23:19

une meilleure version pour @Tony grande solution:

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

        }
    }
}

Dans un fichier css: //PAS nécessaire si vous utilisez bootstrap déroulante.

.ourDropdown{
   display: none;
}
.ourDropdown.open{
   display: inherit;
}
0
répondu Dudi 2017-03-15 13:56:08

vous pouvez écrire directive:

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


}

dans votre composant:

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

cette directive émet un événement lorsque l'élément html contient dans DOM et lorsque la propriété input [clickOut] est 'true'. Il écoute l'événement mousedown pour gérer l'événement avant que l'élément soit retiré du DOM.

et une note: firefox ne contient pas la propriété 'path' dans le cas où vous pouvez utiliser la fonction pour créer path:

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

vous devez donc changer le gestionnaire d'événements sur la directive: événement.chemin d'accès doit être remplacé getEventPath(event)

ce module peut vous aider. https://www.npmjs.com/package/ngx-clickout Il contient la même logique mais gère aussi l'événement esc sur l'élément html source.

0
répondu Alex Mikitevich 2017-07-13 10:21:00

j'ai aussi fait un petit contournement de mon propre.

j'ai créé un événement (dropdownOpen) que j'écoute à mon composant d'élément de sélection ng et j'appelle une fonction qui fermera tous les autres composants de sélection ouverts en dehors du composant de sélection actuellement ouvert.

j'ai modifié une fonction à l'intérieur du select.ts fichier comme ci-dessous pour émettre le événement:

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

dans le HTML j'ai ajouté un écouteur d'événements pour (dropdownened) :

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

c'est ma fonction d'appel sur le déclencheur d'événement à l'intérieur du composant ayant la balise 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();
    })
}
0
répondu Gaurav Pandvia 2017-08-29 20:20:43

NOTE: pour ceux qui veulent utiliser des travailleurs web et vous devez éviter d'utiliser le document et nativeElement cela fonctionnera.

j'ai répondu à la même question ici: https://stackoverflow.com/questions/47571144

copier / coller à partir du lien ci-dessus:

j'ai eu le même problème quand je faisais un menu déroulant et un dialogue de confirmation je voulais les rejeter en cliquant dehors.

mon implémentation finale fonctionne parfaitement mais nécessite quelques animations css3 et un style.

NOTE : Je n'ai pas testé le code ci-dessous, il peut y avoir des problèmes de syntaxe qui doivent être résolus, aussi les ajustements évidents pour votre propre projet!

ce que j'ai fait:

j'ai fait un div séparé fixe avec hauteur 100%, largeur 100% et transformer: scale (0), c'est essentiellement l'arrière-plan, vous pouvez le style avec l'arrière-plan-Couleur: rgba(0, 0, 0, 0.466); pour rendre évident, le menu est ouvert et l'arrière-plan est cliqué sur Fermer. Le menu devient un z-index plus élevé que tout le reste, puis le fond div obtient un z-index inférieur au menu, mais aussi plus que tout le reste. Puis l'arrière-plan a un événement de clic qui ferme le drop-down.

ici, il est avec votre code 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>

Voici la css3 qui nécessite quelques animations 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 vous n'êtes pas après quelque chose de visuel, vous pouvez juste utiliser une transition comme celle-ci

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

.dropdownbackground.showbackground{
     transform: scale(1);
}
0
répondu Shannon 2018-03-19 19:44:46