Comment puis-je fermer une liste déroulante en cliquant à l'extérieur?

144

Je voudrais fermer ma liste déroulante de menu de connexion lorsque l'utilisateur clique n'importe où en dehors de cette liste déroulante, et j'aimerais le faire avec Angular2 et avec "l'approche" Angular2 ...

J'ai mis en place une solution, mais je ne me sens vraiment pas en confiance. Je pense qu'il doit y avoir un moyen le plus simple d'obtenir le même résultat, donc si vous avez des idées ... discutons-en :)!

Voici ma mise en œuvre:

Le composant déroulant:

C'est le composant de ma liste déroulante:

  • Chaque fois que ce composant est défini sur visible, (par exemple: lorsque l'utilisateur clique sur un bouton pour l'afficher), il s'abonne à un userMenu "global" sujet rxjs stocké dans le SubjectsService .
  • Et chaque fois qu'il est caché, il se désabonne à ce sujet.
  • Chaque clic n'importe où dans le modèle de ce composant déclenche la méthode onClick () , qui arrête simplement l'événement de remonter vers le haut (et le composant d'application)

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

Le composant applicatif:

D'autre part, il y a le composant d'application (qui est un parent du composant déroulant):

  • Ce composant capture 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 me sens pas vraiment à l'aise avec l'idée d'avoir un sujet global qui fasse office de connecteur entre ces composants.
  2. Le setTimeout : Ceci est nécessaire car voici ce qui se passe sinon si l'utilisateur clique sur le bouton qui affiche la liste déroulante:
    • L'utilisateur clique sur le bouton (qui ne fait pas partie du composant déroulant) pour afficher le menu déroulant.
    • La liste déroulante s'affiche et il s'abonne immédiatement à l'objet userMenu .
    • L'événement de clic s'incline jusqu'au composant de l'application et se fait prendre
    • Le composant application émet un événement sur le sujet userMenu
    • Le composant déroulant capture cette action sur userMenu et masque la liste déroulante.
    • À la fin, la liste déroulante n'est jamais affichée.

Ce délai d'expiration retardé l'abonnement à la fin du tour de code JavaScript actuel qui résout le problème, mais d'une manière très élégante à mon avis.

Si vous connaissez des solutions plus propres, meilleures, plus intelligentes, plus rapides ou plus fortes, faites-le moi savoir :)!

Clément
la source
Ces réponses peuvent vous donner quelques idées: stackoverflow.com/a/35028820/215945 , stackoverflow.com/questions/35024495#35024651
Mark Rajcok

Réponses:

245

Vous pouvez utiliser l' (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. Découvrez ces articles de Ben Nadel:

Sasxa
la source
1
@Sasxa merci, et d'accord. Je me suis dit que s'il y avait un document d'API non obsolète, il serait apparu dans la recherche qui m'a conduit ici.
danludwig
4
Si event.target est un élément qui a été ajouté dynamiquement via quelque chose comme une liaison [innerHTML], alors l'élément nativeElement de l'élémentRef ne le contiendra pas.
Patrick Graham
8
Le seul inconvénient de cette technique est que vous disposez désormais d'un écouteur d'événement de clic dans votre application qui se déclenche chaque fois que vous cliquez.
codeepic
37
Selon le guide de style officiel Angular 2, vous devez utiliser à la @HostListener('document:click', ['$event'])place de la hostpropriété sur le Componentdécorateur.
Michał Miszczyszyn
15
ou vous pouvez simplement utiliser rxjs pour cela, comme Observable.fromEvent(document, 'click').subscribe(event => {your code here}), afin que vous puissiez toujours vous abonner uniquement lorsque vous avez besoin d'écouter, par exemple, vous avez ouvert le menu déroulant, et lorsque vous le fermez, vous vous désabonnez
Blind Despair
43

MÉTHODE ÉLÉGANTE

J'ai trouvé cette clickOutdirective: https://github.com/chliebel/angular2-click-outside . Je le vérifie et cela fonctionne bien (je ne copie que clickOutside.directive.tsdans mon projet). Vous pouvez l'utiliser de cette manière:

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

closeest votre fonction qui sera appelée lorsque l'utilisateur cliquera en dehors de div. C'est une manière très élégante de traiter le problème décrit en question.

Si vous utilisez la directive ci-dessus pour fermer la fenêtre popUp, n'oubliez pas d'abord d'ajouter event.stopPropagation()au gestionnaire d'événements de clic de bouton qui ouvre popUp.

PRIME:

Ci-dessous, je copie le code de directive oryginal à partir du fichier clickOutside.directive.ts(au cas où le lien cesserait de fonctionner à l'avenir) - l'auteur est Christian Liebel :

Kamil Kiełczewski
la source
2
@Vega Ma recommandation serait d'utiliser la directive dans un élément avec * ngIf, dans le cas des listes déroulantes, cela peut être quelque chose comme<div class="wrap" *ngIf="isOpened" (clickOutside)="...// this should set this.isOpen=false"
Gabriel Balsa Cantú
19

Je l'ai fait de cette façon.

Ajout d'un écouteur d'événements sur le document clicket dans ce gestionnaire vérifié si my containercontient event.target, sinon - cachez la liste déroulante.

Cela ressemblerait à ceci.

@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
la source
Salut. Le.bind (this) est-il nécessaire?
Drenai
1
@Brian Cela peut être nécessaire ou non, mais ce ne serait certainement pas le cas s'il enveloppait la this.offClickHandlerfonction de flèche.
Lansana Camara
17

Je pense que la réponse acceptée par Sasxa fonctionne pour la plupart des gens. Cependant, j'ai eu une situation où le contenu de l'élément, qui devrait écouter les événements hors clic, a changé de manière dynamique. Ainsi, les éléments nativeElement ne contenaient pas le event.target, lors de sa création dynamique. 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 event.target, je vérifie si elementRef est dans le chemin (chemin DOM vers la cible) de l'événement. De cette façon, il est possible de gérer des éléments créés dynamiquement.

JuHarm89
la source
Merci - cela fonctionne mieux lorsque des composants enfants sont présents
MAhsan
Cela m'a beaucoup aidé. Je ne sais pas pourquoi en dehors du composant, le clic n'a pas été détecté avec d'autres réponses.
JavaQuest
13

Si vous faites cela sur iOS, utilisez le touchstart événement:

À partir de Angular 4, la HostListenerdécoration est le moyen préféré 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();
    }
  }

}
Xavier
la source
10

Nous avons travaillé sur un problème similaire au travail aujourd'hui, en essayant de comprendre comment faire disparaître un div déroulant lorsque vous cliquez dessus. La nôtre est légèrement différente de la question de l'affiche initiale parce que nous ne voulions pas cliquer loin d'un composant ou d'une directive différent , mais simplement en dehors de la div particulière.

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

Étapes:
1.) Nous avons donné à l'ensemble du menu déroulant div un nom de classe unique.

2.) Sur le menu déroulant interne lui-même (la seule partie sur laquelle nous voulions que les clics ne ferme PAS le menu), nous avons ajouté un gestionnaire d'événements (window: mouseup) et avons passé l'événement $.

REMARQUE: il n'a pas pu être effectué avec un gestionnaire de «clic» typique car cela était en conflit avec le gestionnaire de clic parent.

3.) Dans notre contrôleur, nous avons créé la méthode que nous voulions appeler lors de l'événement de clic, et nous utilisons event.closest ( 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>

Paige Bolduc
la source
"window: mouseup" doit être utilisé dans le décorateur hôte.
Shivam
@ Shivam - Je ne suis pas sûr de ce que vous entendez par "devrait être utilisé dans le décorateur hôte". Pouvez-vous expliquer davantage? Merci!
Paige Bolduc
Je veux dire au lieu d'utiliser l'objet "window" directement, vous devriez utiliser la propriété "host" du décorateur de composant / décorateur "HostListener" du composant. C'est la pratique courante lorsque vous travaillez avec un objet "fenêtre" ou "document" en angulaire 2.
Shivam
2
Gardez juste un œil pour la compatibilité du navigateur, .closest()n'est pas pris en charge sur IE / Edge à partir d'aujourd'hui ( caniuse )
superjos
5

Vous pouvez créer un élément frère dans la liste déroulante qui couvre la totalité de l'écran qui serait invisible et ne serait là que pour capturer les événements de clic. Ensuite, vous pouvez détecter les clics sur cet élément et fermer la liste déroulante lorsque vous cliquez dessus. Disons que l'élément est de la sérigraphie de classe, voici un style pour cela:

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

Le z-index doit être suffisamment élevé pour le positionner au-dessus de tout sauf de votre liste déroulante. Dans ce cas, ma liste déroulante serait b z-index 2.

Les autres réponses ont fonctionné dans certains cas pour moi, sauf parfois ma liste déroulante se fermait lorsque j'interagissais avec des éléments à l'intérieur et je ne voulais pas cela. J'avais ajouté dynamiquement des éléments qui n'étaient pas contenus dans mon composant, en fonction de la cible de l'événement, comme je m'y attendais. Plutôt que de trier ce gâchis, je me suis dit que je l'essayerais simplement en sérigraphie.

Patrick Graham
la source
5

Je n'ai fait aucune solution de contournement. Je viens de joindre le document: cliquez sur ma fonction de bascule comme suit:

    @Directif({
      sélecteur: '[appDropDown]'
    })
    la classe d'exportation DropdownDirective implémente OnInit {

      @HostBinding ('class.open') isOpen: booléen;

      constructeur (elemRef privé: ElementRef) {}

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

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

Ainsi, quand je suis en dehors de ma directive, je ferme la liste déroulante.

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

INCONVÉNIENTS: - Deux écouteurs d'événement 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.

Alex Egli
la source
Non, je ne l'ai utilisé que sur le navigateur de bureau.
Alex Egli
3

Je voudrais compléter la réponse @Tony, car l'événement n'est pas supprimé après le clic en dehors du composant. Reçu complet:

  • 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, utilisez:

    (click)="dropstatus=true"

Vous pouvez maintenant contrôler l'état de votre liste déroulante avec la variable dropstatus et appliquer les classes appropriées avec [ngClass] ...

Gauss
la source
3

Vous pouvez écrire une 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é d'entrée [clickOut] est "true". Il écoute l'événement mousedown pour gérer l'événement avant que l'élément ne soit supprimé du DOM.

Et une note: Firefox ne contient pas la propriété 'chemin' sur l'événement, vous pouvez utiliser la fonction pour créer le chemin:

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: event.path 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 également l'événement esc sur l'élément html source.

Alex Mikitevich
la source
3

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 containméthode 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!

Douglas Caina
la source
2

Une meilleure version pour l'excellente solution @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");

        }
    }
}

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

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

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

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


  }
Stefdelec
la source
2

J'ai fait une directive pour résoudre ce problème similaire et j'utilise Bootstrap. Mais dans mon cas, au lieu d'attendre l'événement de clic en dehors de l'élément pour fermer le menu déroulant actuellement ouvert, je pense qu'il est préférable de surveiller l'événement 'mouseleave' pour fermer automatiquement le menu.

Voici ma solution:

Directif

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
la source
1

Si vous utilisez Bootstrap, vous pouvez le faire directement avec bootstrap via des listes déroulantes (composant 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>

Maintenant, vous pouvez mettre des (click)="clickButton()"éléments sur le bouton. http://getbootstrap.com/javascript/#dropdowns

Vusan
la source
1

J'ai également fait une petite solution de contournement par moi-même.

J'ai créé un (dropdownOpen) événement que j'écoute dans mon composant d'élément ng-select et j'appelle une fonction qui fermera tous les autres SelectComponent ouverts à l'exception du SelectComponent actuellement ouvert.

J'ai modifié une fonction dans le fichier select.ts comme ci-dessous pour émettre l'é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 (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>

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();
    })
}
Gaurav Pandvia
la source
1

REMARQUE: pour ceux qui souhaitent utiliser des travailleurs Web et que vous devez éviter d'utiliser document et nativeElement, cela fonctionnera.

J'ai répondu à la même question ici: /programming/47571144

Copiez / collez à partir du lien ci-dessus:

J'ai eu le même problème lorsque je faisais un menu déroulant et une boîte de dialogue de confirmation que je voulais les ignorer en cliquant à l'extérieur.

Ma mise en œuvre finale fonctionne parfaitement mais nécessite des animations et un style css3.

REMARQUE : je n'ai pas testé le code ci-dessous, il peut y avoir des problèmes de syntaxe à régler, ainsi que des ajustements évidents pour votre propre projet!

Ce que j'ai fait:

J'ai créé un div fixe séparé avec une hauteur de 100%, une largeur de 100% et une transformation: scale (0), c'est essentiellement l'arrière-plan, vous pouvez le styliser avec background-color: rgba (0, 0, 0, 0.466); pour rendre évident le menu est ouvert et l'arrière-plan est un clic pour fermer. Le menu obtient un z-index plus élevé que tout le reste, puis le div d'arrière-plan obtient un z-index inférieur au menu mais également supérieur à tout le reste. Ensuite, l'arrière-plan a un événement de clic qui ferme la liste déroulante.

Le voici 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 le css3 qui a besoin de 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 ne recherchez rien de visuel, vous pouvez simplement 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);
}
Shannon
la source
1

Je suis tombé sur une autre solution, inspirée d'exemples avec événement focus / flou.

Ainsi, si vous souhaitez obtenir les mêmes fonctionnalités sans attacher un écouteur de document global, vous pouvez considérer comme valide l'exemple suivant. Cela fonctionne également dans Safari et Firefox sur OSx, malgré une autre gestion de l'événement de focus de bouton: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus

Exemple de travail sur stackbiz avec angular 8: https://stackblitz.com/edit/angular-sv4tbi?file=src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts

Balisage 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 directive ressemblera à ceci:

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
la source
1

Vous pouvez utiliser mouseleavedans votre vue comme ceci

Testez avec angular 8 et travaillez parfaitement

<ul (mouseleave)="closeDropdown()"> </ul>
Tony Ngo
la source
Cela fermera le container quand la souris partira, mais merci de partager quand même car j'ignorais son existence.
Ben Hayward
0

LA MÉTHODE LA PLUS ÉLÉGANTE: D

Il existe un moyen le plus simple de le faire, pas besoin de directives pour cela.

"element-that-toggle-your-dropdown" doit être une balise button. Utilisez n'importe quelle méthode dans l'attribut (flou). C'est tout.

<button class="element-that-toggle-your-dropdown"
               (blur)="isDropdownOpen = false"
               (click)="isDropdownOpen = !isDropdownOpen">
</button>
George Reznichenko
la source
Cela ne fonctionnera pas si vous souhaitez garder la liste déroulante ouverte en cas de clic, par exemple un utilisateur peut manquer de cliquer sur un bouton
Oui, le