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

Je voudrais fermer ma liste déroulante du menu de connexion lorsque l'utilisateur clique n'importe où en dehors de ce menu déroulant, et j'aimerais le faire avec Angular2 et avec l'approche angular2 …

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

Voici ma mise en œuvre:

La composante déroulante:

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

  • Chaque fois que ce composant est réglé sur visible, (Par exemple: lorsque l'utilisateur clique sur un bouton pour l'afficher), il s'abonne à un sujet «sujet global» rxjs userMenu stocké dans le service Sujet.
  • 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 ne fait que suspendre l' événement vers le haut (et le composant de l'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 d'application:

D'autre part, il existe 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 Sujet ( 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 agit comme un connecteur entre ces composants
  2. SetTimeout : Ceci est nécessaire parce que c'est ce qui se produit autrement 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 la liste déroulante.
    • Le menu déroulant est affiché et il s'inscrit immédiatement sur le sujet userMenu .
    • L'événement de clic se décompose jusqu'au composant de l'application et se fait prendre
    • Le composant de l'application émet un événement sur le sujet userMenu
    • Le composant déroulant capture cette action sur userMenu et cache le menu déroulant.
    • À la fin, le menu déroulant n'est jamais affiché.

Cette temporisation définie retarde l'abonnement à la fin du tour JS actuel qui résolvent 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 :)!

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. Consultez ces publications de Ben Nadel:

  • Tracking-click-events-outside-the-current-component
  • Sélecteurs-et-sorties-peut-avoir-le-même-nom
  • DirectiveMetadata
  • Reliure de l'hôte

Je l'ai fait de cette façon.

Ajout d'un auditeur d'événement sur le click document et dans ce gestionnaire vérifié si mon container contient event.target , sinon – cacher le menu déroulant.

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

Nous avons travaillé sur un problème similaire au travail aujourd'hui, en essayant de comprendre comment faire apparaître une division déroulante lorsqu'elle est cliquée. La nôtre est légèrement différente de la question de l'affiche initiale parce que nous ne voulions pas cliquer sur un autre composant ou directive , mais simplement en dehors de la division particulière.

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

Pas:
1.) Nous avons donné à tout le menu déroulant div un nom de classe unique.

2.) Sur le menu déroulant intérieur lui-même (la seule partie que nous voulions clique sur NE PAS fermer le menu), nous avons ajouté un gestionnaire d'événements (fenêtre: mouseup) et passé dans l'événement $.

REMARQUE: il ne pouvait pas être fait avec un gestionnaire de «clic» typique car celui-ci était en conflit avec le gestionnaire de clics parent.

3.) Dans notre contrôleur, nous avons créé la méthode que nous voulions appeler sur l'événement de clics, et nous utilisons le event.closest ( docs ici ) pour savoir si le point cliqué se trouve dans notre division 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> 

J'ai trouvé cette directive clickOut : https://github.com/chliebel/angular2-click-outs

Je le vérifie et ça fonctionne bien (je ne copie que clickOutside.directive.ts dans mon projet). U peut l'utiliser de cette façon:

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

Lorsque la fonction est close vous allez appeler lorsque l'utilisateur clique sur la division extérieure. C'est un moyen très élégant de traiter le problème décrit en question.

=== Code de la directive ===

Ci-dessous, je copie le code de directive clickOutside.directive.ts depuis le fichier clickOutside.directive.ts (dans le cas où le lien va cesser 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); } } } 

Je pense que Sasxa a accepté que la réponse 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é dynamiquement. Ainsi, les Elements nativeElement ne contiennent pas le event.target, lorsqu'il a été créé dynamiquement. Je pourrais résoudre ceci 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 à cibler) de l'événement. De cette façon, il est possible de gérer des éléments créés dynamiquement.

Vous pouvez créer un élément de fratrie dans la liste déroulante qui couvre l'écran entier qui serait invisible et être là pour capturer des événements de clic. Ensuite, vous pouvez détecter les clics sur cet élément et fermer le menu déroulant lorsque vous cliquez dessus. Disons que cet élément est de sérigraphie de classe, voici un style pour cela:

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

L'index z doit être suffisamment élevé pour le positionner au-dessus de tout, mais de votre liste déroulante. Dans ce cas, mon menu déroulant serait b z-index 2.

Les autres réponses ont fonctionné dans certains cas pour moi, sauf que parfois mon menu déroulant a été fermé lorsque j'ai interagi avec des éléments en son sein et que je ne voulais pas cela. J'avais ajouté dynamiquement des éléments qui n'étaient pas contenus dans mon composant, selon la cible de l'événement, comme je l'avais prévu. Plutôt que de trier ce gâchis, j'ai pensé que je l'essayerais à la manière de la sérigraphie.

Si vous utilisez bootstrap, vous pouvez le faire directement via bootstrap via les menus déroulants (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, il est bon de mettre (click)="clickButton()" sur le bouton.
http://getbootstrap.com/javascript/#dropdowns

 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: – Écoutes de deux clics pour chacun de ces composants à la page. N'utilisez pas cela sur des composants qui sont sur la page des centaines de fois.

Je voudrais compléter la réponse @Tony, car l'événement n'est pas supprimé après le clic en dehors du composant. Remboursement 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" 

Maintenant, vous pouvez contrôler votre état déroulant avec la variable dropstatus et appliquer des classes appropriées avec [ngClass] …

Une meilleure version pour @Tony excellente 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: // n'est PAS nécessaire si vous utilisez le menu déroulant bootstrap.

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

J'ai également fait un peu de contournement, j'ai créé un événement (dropdownOpen) que j'écoute dans mon composant ng-select element et j'appelle une fonction qui fermera tout l'autre SelectComponent ouvert à part du SelectComponent actuellement ouvert. J'ai modifié une fonction dans le fichier select.ts comme ci-dessous pour émettre un é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); } 

En html, j'ai ajouté un auditeur d'événement 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 dans le 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(); }) } 

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 du DOM et lorsque la propriété [clickOut] est vraie. Il écoute l'événement mousedown pour gérer l'événement avant que l'élément ne soit retiré de DOM.

Et une note: firefox ne contient pas de "chemin" de propriété sur l'événement, vous pouvez utiliser la fonction pour créer le chemin d'accès:

 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 devriez donc modifier 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.

Vous devriez vérifier si vous cliquez plutôt sur la superposition modale, 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(); } }