Je souhaite fermer le menu déroulant du menu de connexion lorsque l'utilisateur clique n'importe où en dehors de ce menu, et j'aimerais le faire avec Angular2 et avec "l'approche" d'Angular2 ...
J'ai implémenté une solution, mais je ne me sens vraiment pas en confiance avec elle. Je pense qu’il doit exister le moyen le plus simple d’atteindre le même résultat, alors si vous avez des idées, discutons :)!
Voici ma mise en œuvre:
Voici le composant de ma liste déroulante:
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;
}
}
D'autre part, il y a le composant d'application (qui est un parent du composant déroulant):
Voici le code:
export class AppComponent {
constructor( public subjects: SubjectsService) {
document.addEventListener('click', () => this.onClick());
}
onClick( ) {
this.subjects.userMenu.next({});
}
}
Ce délai défini retarde l'abonnement à la fin du code JavaScript actuel, ce qui résout le problème, mais de manière très élégante à mon avis.
Si vous connaissez des solutions plus propres, meilleures, plus intelligentes, plus rapides ou plus puissantes, merci de me le faire savoir :)!
Vous pouvez utiliser l'événement (document:click)
:
@Component({
Host: {
'(document:click)': 'onClick($event)',
},
})
class SomeComponent() {
constructor(private _eref: ElementRef) { }
onClick(event) {
if (!this._eref.nativeElement.contains(event.target)) // or some similar check
doSomething();
}
}
Une autre approche consiste à créer un événement personnalisé en tant que directive. Découvrez ces messages par Ben Nadel:
J'ai trouvé cette directive clickOut
: https://github.com/chliebel/angular2-click-outside . Je vérifie et cela fonctionne bien (je ne copie que clickOutside.directive.ts
dans mon projet). Vous pouvez l'utiliser de cette façon:
<div (clickOutside)="close($event)"></div>
Où close
est 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 dans la question.
Si vous utilisez la directive ci-dessus pour fermer la fenêtre popUp, rappelez-vous d’abord d’ajouter event.stopPropagation()
au gestionnaire d’événements de clic qui ouvre popUp.
Ci-dessous, je copie le code de directive oryginal du fichier clickOutside.directive.ts
(au cas où le lien ne fonctionnerait plus à 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 l'ai fait de cette façon.
Ajout d'un écouteur d'événement sur le document click
et dans ce gestionnaire vérifié si ma container
contient event.target
, si ce n'est pas le cas - masquer le menu déroulant.
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";
}
}
}
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 devait écouter les événements hors-clic, change de façon dynamique. Ainsi, le nativeElement Elements ne contenait pas le fichier event.target, lorsqu’il a été créé dynamiquement ..__ Je pourrais résoudre ce problème 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 (DOM path to target) de l'événement. De cette façon, il est possible de gérer des éléments créés dynamiquement.
Nous travaillons sur un problème similaire au travail aujourd'hui, essayant de trouver un moyen de faire disparaître un div du menu déroulant lorsque ce dernier est cliqué. La nôtre est légèrement différente de la question de l’affiche initiale car nous ne voulions pas quitter une autre composante ou directive, mais simplement en dehors de la div en question.
Nous avons finalement résolu le problème en utilisant le gestionnaire d'événements (window: mouseup).
Pas:
1.) Nous avons donné à l'ensemble du menu déroulant un nom de classe unique.
2.) Dans le menu déroulant interne lui-même (la seule partie sur laquelle nous voulions que le clic ne ferme PAS le menu), nous avons ajouté un gestionnaire d’événements (window: mouseup) et avons transmis l’événement $.
REMARQUE: cela n'a pas pu être fait avec un gestionnaire de "clic" typique, car il é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 click out et nous utilisons event.closest ( docs here ) pour savoir si le point cliqué est à l'intérieur. 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>
touchstart
:À partir de 4 angulaires, le HostListener
décorer est le moyen préféré
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();
}
}
}
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 qui serait là uniquement 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 cet élément est de la classe sérigraphie, voici un peu de style pour cela:
.silkscreen {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
}
L'indice z 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, à l'exception parfois de la fermeture de ma liste déroulante lorsque j'interagissais avec des éléments qu'elle contenait et que je ne le voulais pas. J'avais dynamiquement ajouté 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 régler ce gâchis, je me suis dit que je l'essayerais simplement à la sérigraphie.
Je n'ai fait aucune solution de contournement. Je viens de joindre le document: cliquez sur ma fonction de basculement comme suit:
@Directive ({ Selector: '[appDropDown]' }) La classe d'exportation DropdownDirective implémente OnInit { @HostBinding ('class.open') estOpen: boolean; constructeur (private elemRef: ElementRef) {} ngOnInit (): void { this.isOpen = false; } @HostListener ('document: cliquez sur', ['$ event']) @HostListener ('document: touchstart', ['$ event']) bascule (événement) { if (this.elemRef.nativeElement.contains (event.target)) { this.isOpen =! this.isOpen; } autre { this.isOpen = false; } }
Donc, quand je suis en dehors de ma directive, je ferme le menu déroulant.
Je voudrais compléter la réponse @ Tony, car l'événement n'est pas supprimé après le clic en dehors du composant. Accusé de réception 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 votre état déroulant avec la variable dropstatus et appliquer les classes appropriées avec [ngClass] ...
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, en fonction de @ 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!
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: - Deux écouteurs d’événements clic pour chacun de ces composants sur la page. Ne l'utilisez pas sur des composants qui se trouvent sur la page des centaines de fois.
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 emit event lorsque l'élément html est contenu dans DOM et que la propriété d'entrée [clickOut] est 'true' . Elle écoute l'événement mousedown pour gérer l'événement avant que l'élément soit supprimé de DOM.
Et une remarque: Firefox ne contient pas la propriété 'path' on event, vous pouvez utiliser la fonction pour créer un 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;
};
Donc, vous devriez changer le gestionnaire d’événements sur la directive: event.path doit être remplacé par getEventPath (event)
Ce module peut aider. https://www.npmjs.com/package/ngx-clickout Il contient la même logique mais permet également de gérer les événements échap sur l'élément HTML source.
Si vous utilisez Bootstrap, vous pouvez le faire directement avec bootstrap via des 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, vous pouvez mettre des éléments (click)="clickButton()"
sur le bouton . http://getbootstrap.com/javascript/#dropdowns
Vous devriez vérifier si vous cliquez sur la superposition modale à la place, beaucoup plus facilement.
Votre template:
<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();
}
}
Une meilleure version pour la grande solution de @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;
}
J'ai aussi fait une petite solution personnelle.
J'ai créé un événement(dropdownOpen)que j'écoute sur mon composant d'élément ng-select et appelle une fonction qui ferme tous les autres SelectComponent ouverts, à l'exception du SelectComponent actuellement ouvert.
J'ai modifié une fonction dans le fichierselect.tscomme 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 code HTML, j'ai ajouté un écouteur 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>
Voici la fonction que j'appelle 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();
})
}
J'ai fait une directive pour résoudre ce problème similaire et j'utilise Bootstrap. Mais dans mon cas, au lieu d'attendre que l'événement click en dehors de l'élément ferme 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:
Directive
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>
Je suis tombé sur une autre solution, inspirée par des exemples d'événements focus/blur.
Ainsi, si vous souhaitez obtenir les mêmes fonctionnalités sans attacher un écouteur de document global, vous pouvez considérer l’exemple suivant comme valide. Cela fonctionne également dans Safari et Firefox sur OSx, bien qu'ils aient un autre traitement 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
Balise 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();
}
}
LA METHODE LA PLUS ELEGANTE: D
Il existe un moyen plus simple de le faire, aucune directive n’est nécessaire pour cela.
"élément-qui-bascule-votre-liste" devrait ê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>
NOTE: 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: https://stackoverflow.com/questions/47571144
Copier/Coller à partir du lien ci-dessus:
J'avais le même problème lorsque je créais un menu déroulant et une boîte de dialogue de confirmation que je voulais faire disparaître lorsque je cliquais à l'extérieur.
Ma dernière implémentation fonctionne parfaitement mais nécessite quelques animations et styles CSS3.
NOTE: je n'ai pas testé le code ci-dessous, il peut y avoir des problèmes de syntaxe qui doivent être résolus, ainsi que les ajustements évidents pour votre propre projet!
Ce que j'ai fait:
J'ai fait un div fixe séparé avec hauteur 100%, largeur 100% et transformation: scale (0), il s'agit essentiellement de l'arrière-plan, vous pouvez l'appeler avec background-color: rgba (0, 0, 0, 0.466); pour rendre évident que le menu est ouvert et que l’arrière-plan est un clic de souris . Le menu obtient un z-index supérieur à tout le reste, puis la div de l’arrière-plan obtient un z-index inférieur au menu mais également supérieur à tout. autre. Ensuite, l'arrière-plan a un événement de clic qui ferme la liste déroulante.
La 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 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 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);
}