Wie kann ich ein Dropdown-Menü schließen, wenn ich nach draußen klicke?

144

Ich möchte mein Dropdown-Menü für das Anmeldemenü schließen, wenn der Benutzer auf eine beliebige Stelle außerhalb dieses Dropdowns klickt, und ich möchte dies mit Angular2 und dem Angular2-Ansatz tun ...

Ich habe eine Lösung implementiert, bin aber nicht sicher. Ich denke, es muss einen einfachsten Weg geben, um das gleiche Ergebnis zu erzielen. Wenn Sie also Ideen haben ... lassen Sie uns darüber diskutieren :)!

Hier ist meine Implementierung:

Die Dropdown-Komponente:

Dies ist die Komponente für mein Dropdown:

  • Jedes Mal, wenn diese Komponente sichtbar wird (z. B. wenn der Benutzer auf eine Schaltfläche klickt, um sie anzuzeigen), abonniert sie ein "globales" rxjs-Betreff userMenu, das im SubjectsService gespeichert ist .
  • Und jedes Mal, wenn es versteckt ist, wird dieses Thema abgemeldet.
  • Jeder Klick irgendwo in der Vorlage dieser Komponente löst die onClick () -Methode aus, die einfach verhindert, dass das Ereignis nach oben sprudelt (und die Anwendungskomponente).

Hier ist der 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;
    }
}

Die Anwendungskomponente:

Auf der anderen Seite gibt es die Anwendungskomponente (die der Dropdown-Komponente übergeordnet ist):

  • Diese Komponente fängt jedes Klickereignis ab und sendet auf demselben rxjs-Betreff ( userMenu ).

Hier ist der Code:

export class AppComponent {

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

Was mich stört:

  1. Ich fühle mich nicht wirklich wohl mit der Idee, ein globales Thema zu haben, das als Bindeglied zwischen diesen Komponenten fungiert.
  2. Das setTimeout : Dies ist erforderlich, da Folgendes passiert, wenn der Benutzer auf die Schaltfläche klickt, die das Dropdown-Menü anzeigt:
    • Der Benutzer klickt auf die Schaltfläche (die nicht Teil der Dropdown-Komponente ist), um die Dropdown-Liste anzuzeigen.
    • Das Dropdown-Menü wird angezeigt und abonniert sofort den Betreff userMenu .
    • Das Klickereignis sprudelt zur App-Komponente und wird abgefangen
    • Die Anwendungskomponente emittieren , ein Ereignis auf dem UserMenu Subjekt
    • Die Dropdown-Komponente fängt diese Aktion im Benutzermenü ab und blendet das Dropdown aus.
    • Am Ende wird das Dropdown nie angezeigt.

Diese festgelegte Zeitüberschreitung verzögert das Abonnement bis zum Ende der aktuellen JavaScript-Code-Runde, wodurch das Problem gelöst wird, aber meiner Meinung nach auf sehr elegante Weise.

Wenn Sie sauberere, bessere, intelligentere, schnellere oder stärkere Lösungen kennen, lassen Sie es mich bitte wissen :)!

Clement
quelle
Diese Antworten können Ihnen einige Ideen geben: stackoverflow.com/a/35028820/215945 , stackoverflow.com/questions/35024495#35024651
Mark Rajcok

Antworten:

245

Sie können (document:click)event verwenden:

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

Ein anderer Ansatz besteht darin, ein benutzerdefiniertes Ereignis als Direktive zu erstellen. Schauen Sie sich diese Beiträge von Ben Nadel an:

Sasxa
quelle
1
@ Sasxa danke und stimmte zu. Ich dachte, wenn es ein nicht veraltetes API-Dokument gäbe, wäre es in der Suche aufgetaucht, die mich hierher geführt hat.
Danludwig
4
Wenn event.target ein Element ist, das dynamisch über eine [innerHTML] -Bindung hinzugefügt wurde, enthält das nativeElement des elementRef es nicht.
Patrick Graham
8
Der einzige Nachteil dieser Technik ist, dass Sie jetzt einen Listener für Klickereignisse in Ihrer Anwendung haben, der bei jedem Klick ausgelöst wird.
Codeepic
37
Laut dem offiziellen Angular 2-Styleguide sollten Sie @HostListener('document:click', ['$event'])anstelle von hostEigentum den ComponentDekorateur verwenden.
Michał Miszczyszyn
15
oder Sie könnten einfach rxjs dafür verwenden Observable.fromEvent(document, 'click').subscribe(event => {your code here}), so dass Sie immer nur abonnieren können, wenn Sie zuhören müssen, zum Beispiel wenn Sie Dropdown geöffnet haben, und wenn Sie es schließen, melden Sie sich ab
Blind Despair
42

ELEGANTE METHODE

Ich habe diese clickOutAnweisung gefunden: https://github.com/chliebel/angular2-click-outside . Ich überprüfe es und es funktioniert gut (ich kopiere nur clickOutside.directive.tsin mein Projekt). Sie können es folgendermaßen verwenden:

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

Wo close ist Ihre Funktion, die aufgerufen wird, wenn der Benutzer außerhalb von div klickt? Es ist eine sehr elegante Art, mit dem fraglichen Problem umzugehen.

Wenn Sie die obige Anweisung verwenden, um das PopUp-Fenster zu schließen, müssen Sie zuerst hinzufügen event.stopPropagation() Ereignishandler für Schaltflächenklicks , der PopUp öffnet.

BONUS:

Unten kopiere ich den Code der oryginalen Direktive aus der Datei clickOutside.directive.ts(falls der Link in Zukunft nicht mehr funktioniert) - der Autor ist Christian Liebel :

Kamil Kiełczewski
quelle
2
@Vega Meine Empfehlung wäre, die Richtlinie in einem Element mit * ngIf zu verwenden. Wenn es sich um Dropdowns handelt, kann dies ungefähr so ​​aussehen<div class="wrap" *ngIf="isOpened" (clickOutside)="...// this should set this.isOpen=false"
Gabriel Balsa Cantú
19

Ich habe es so gemacht.

Es wurde ein Ereignis-Listener zum Dokument hinzugefügt clickund in diesem Handler überprüft, ob mein containerenthält event.target, wenn nicht - das Dropdown-Menü ausblenden.

Es würde so aussehen.

@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
quelle
Hallo. Ist die Bindung (dies) notwendig?
Drenai
1
@Brian Es kann notwendig sein oder auch nicht, aber definitiv nicht, wenn er das this.offClickHandlerin eine Pfeilfunktion einwickelt .
Lansana Camara
17

Ich denke, Sasxa akzeptierte Antwort funktioniert für die meisten Menschen. Ich hatte jedoch eine Situation, in der sich der Inhalt des Elements, das Off-Click-Ereignisse abhören sollte, dynamisch änderte. Das nativeElement Elements enthielt also nicht das event.target, als es dynamisch erstellt wurde. Ich könnte dies mit der folgenden Anweisung lösen

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

Anstatt zu überprüfen, ob elementRef event.target enthält, überprüfe ich, ob sich elementRef im Pfad (DOM-Pfad zum Ziel) des Ereignisses befindet. Auf diese Weise können dynamisch erstellte Elemente verarbeitet werden.

JuHarm89
quelle
Danke - das funktioniert besser, wenn
untergeordnete
Das war sehr hilfreich für mich. Ich bin mir nicht sicher, warum der Klick außerhalb der Komponente bei anderen Antworten nicht erkannt wurde.
JavaQuest
13

Wenn Sie dies unter iOS tun, verwenden Sie die touchstart Ereignis auch:

Ab Winkel 4 ist das HostListenerDekorieren der bevorzugte Weg, dies zu tun

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

Wir haben heute an einem ähnlichen Problem gearbeitet und versucht herauszufinden, wie ein Dropdown-Div verschwinden kann, wenn es angeklickt wird. Unsere Frage unterscheidet sich geringfügig von der Frage des ursprünglichen Posters, da wir nicht von einer anderen Komponente oder Direktive wegklicken wollten , sondern nur außerhalb des jeweiligen Div.

Wir haben es schließlich mithilfe des Ereignishandlers (window: mouseup) gelöst.

Schritte:
1.) Wir haben dem gesamten Dropdown-Menü div einen eindeutigen Klassennamen gegeben.

2.) Im inneren Dropdown-Menü selbst (der einzige Teil, für den Klicks das Menü NICHT schließen sollten) haben wir einen Ereignishandler (Fenster: mouseup) hinzugefügt und das $ -Ereignis übergeben.

HINWEIS: Dies konnte mit einem typischen "Klick" -Handler nicht durchgeführt werden, da dies mit dem übergeordneten Klick-Handler in Konflikt stand.

3.) In unserem Controller haben wir die Methode erstellt, die für das Click-out-Ereignis aufgerufen werden soll, und wir verwenden event.closest ( Dokumente hier ), um herauszufinden, ob sich der angeklickte Punkt innerhalb unserer Zielklasse div befindet.

 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
quelle
"window: mouseup" sollte im Host-Dekorator verwendet werden.
Shivam
@ Shivam-- Ich bin nicht sicher, was du mit "sollte innerhalb des Host-Dekorateurs verwendet werden" meinst. Könnten Sie vielleicht weiter erklären? Vielen Dank!
Paige Bolduc
Ich meine, anstatt das "Fenster" -Objekt direkt zu verwenden, sollten Sie die "Host" -Eigenschaft des Komponentendekorators / "HostListener" -Dekorators der Komponente verwenden. Das ist Standard bei der Arbeit mit "Fenster" - oder "Dokument" -Objekt in Winkel 2.
Shivam
2
Halten Sie einfach ein Auge auf Browserkompatibilität, .closest()wird von IE / Edge ab heute nicht unterstützt ( caniuse )
superjos
5

Sie können ein Geschwisterelement in der Dropdown-Liste erstellen, das den gesamten Bildschirm abdeckt, der unsichtbar ist und nur zum Erfassen von Klickereignissen dient. Dann können Sie Klicks auf dieses Element erkennen und die Dropdown-Liste schließen, wenn Sie darauf klicken. Nehmen wir an, das Element ist von der Klasse Siebdruck, hier ist ein Stil dafür:

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

Der Z-Index muss hoch genug sein, um ihn über allem außer Ihrem Dropdown zu positionieren. In diesem Fall wäre mein Dropdown b z-Index 2.

Die anderen Antworten funktionierten in einigen Fällen für mich, außer dass manchmal mein Dropdown geschlossen wurde, wenn ich mit Elementen darin interagierte und das nicht wollte. Ich hatte dynamisch Elemente hinzugefügt, die nicht in meiner Komponente enthalten waren, je nach Ereignisziel, wie ich erwartet hatte. Anstatt das Durcheinander zu beseitigen, dachte ich, ich würde es einfach auf Siebdruck versuchen.

Patrick Graham
quelle
5

Ich habe keine Problemumgehung vorgenommen. Ich habe gerade ein Dokument angehängt: Klicken Sie wie folgt auf meine Umschaltfunktion:

    @Directive ({
      Auswahl: '[appDropDown]'
    })
    Exportklasse DropdownDirective implementiert OnInit {

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

      Konstruktor (privates elemRef: ElementRef) {}

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

      @HostListener ('document: click', ['$ event'])
      @HostListener ('document: touchstart', ['$ event'])
      umschalten (Ereignis) {
        if (this.elemRef.nativeElement.contains (event.target)) {
          this.isOpen =! this.isOpen;
        } else {
          this.isOpen = false;
      }}
    }}

Wenn ich mich außerhalb meiner Richtlinie befinde, schließe ich die Dropdown-Liste.

Elie Nehmé
quelle
4
import { Component, HostListener } from '@angular/core';

@Component({
    selector: 'custom-dropdown',
    template: `
        <div class="custom-dropdown-container">
            Dropdown code here
        </div>
    `
})
export class CustomDropdownComponent {
    thisElementClicked: boolean = false;

    constructor() { }

    @HostListener('click', ['$event'])
    onLocalClick(event: Event) {
        this.thisElementClicked = true;
    }

    @HostListener('document:click', ['$event'])
    onClick(event: Event) {
        if (!this.thisElementClicked) {
            //click was outside the element, do stuff
        }
        this.thisElementClicked = false;
    }
}

DOWNSIDES: - Zwei Klickereignis-Listener für jede dieser Komponenten auf Seite. Verwenden Sie dies nicht für Komponenten, die sich hunderte Male auf der Seite befinden.

Alex Egli
quelle
Nein, ich habe es nur im Desktop-Browser verwendet.
Alex Egli
3

Ich möchte die @ Tony-Antwort ergänzen, da das Ereignis nach dem Klicken außerhalb der Komponente nicht entfernt wird. Vollständiger Beleg:

  • Markieren Sie Ihr Hauptelement mit #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);
  • Verwenden Sie für das anklickbare Element:

    (click)="dropstatus=true"

Jetzt können Sie Ihren Dropdown-Status mit der Variablen dropstatus steuern und mit [ngClass] die richtigen Klassen anwenden ...

Gauß
quelle
3

Sie können Direktive schreiben:

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


}

In Ihrer Komponente:

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

Diese Anweisung gibt ein Ereignis aus, wenn das HTML-Element in DOM enthalten ist und wenn die Eingabeeigenschaft [clickOut] 'true' ist. Es hört das Mousedown-Ereignis ab, um das Ereignis zu behandeln, bevor das Element aus dem DOM entfernt wird.

Und ein Hinweis: Firefox enthält keine Eigenschaft 'Pfad' für den Fall, dass Sie die Funktion zum Erstellen eines Pfads verwenden können:

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

Daher sollten Sie den Ereignishandler in der Direktive ändern: event.path sollte durch getEventPath (event) ersetzt werden.

Dieses Modul kann helfen. https://www.npmjs.com/package/ngx-clickout Es enthält dieselbe Logik, behandelt aber auch das esc-Ereignis im Quell-HTML-Element.

Alex Mikitevich
quelle
3

Die richtige Antwort hat ein Problem. Wenn Sie eine Clicakble-Komponente in Ihrem Popover haben, wird das Element nicht mehr in der containMethode angezeigt und geschlossen, basierend auf @ JuHarm89, das ich selbst erstellt habe:

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

Danke für die Hilfe!

Douglas Caina
quelle
2

Eine bessere Version für @ Tony großartige Lösung:

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

        }
    }
}

In einer CSS-Datei: // NICHT erforderlich, wenn Sie das Bootstrap-Dropdown verwenden.

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

Sie sollten überprüfen, ob Sie stattdessen viel einfacher auf das modale Overlay klicken.

Ihre Vorlage:

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

Und die Methode:

  @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
quelle
2

Ich habe eine Anweisung zur Behebung dieses ähnlichen Problems erstellt und verwende Bootstrap. Aber in meinem Fall, anstatt darauf zu warten, dass das Klickereignis außerhalb des Elements das aktuell geöffnete Dropdown-Menü schließt, ist es meiner Meinung nach besser, wenn wir über das Ereignis 'Mouseleave' wachen, um das Menü automatisch zu schließen.

Hier ist meine Lösung:

Richtlinie

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

Wenn Sie Bootstrap verwenden, können Sie dies direkt mit Bootstrap über Dropdowns (Bootstrap-Komponente) tun.

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

Jetzt ist es in Ordnung, (click)="clickButton()"Dinge auf den Knopf zu legen . http://getbootstrap.com/javascript/#dropdowns

vusan
quelle
1

Ich habe auch selbst eine kleine Problemumgehung durchgeführt.

Ich habe ein (dropdownOpen) erstellt , das ich an meiner ng-select-Elementkomponente abhöre und eine Funktion aufrufe, die alle anderen geöffneten SelectComponent-Elemente außer der aktuell geöffneten SelectComponent schließt.

Ich habe eine Funktion in der Datei select.ts wie folgt geändert , um das Ereignis auszulösen :

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

Im HTML habe ich einen Ereignis-Listener für (dropdownOpened) hinzugefügt :

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

Dies ist meine aufrufende Funktion beim Ereignisauslöser in der Komponente mit dem Tag ng2-select:

@ViewChildren(SelectComponent) selectElem :QueryList<SelectComponent>;

public closeOtherElems(element){
    let a = this.selectElem.filter(function(el){
                return (el != element)
            });

    a.forEach(function(e:SelectComponent){
        e.closeDropdown();
    })
}
Gaurav Pandvia
quelle
1

HINWEIS: Für diejenigen, die Web-Worker verwenden möchten und die Verwendung von document und nativeElement vermeiden müssen, funktioniert dies.

Ich habe die gleiche Frage hier beantwortet: /programming/47571144

Kopieren / Einfügen über den obigen Link:

Ich hatte das gleiche Problem, als ich ein Dropdown-Menü und einen Bestätigungsdialog erstellte. Ich wollte sie schließen, wenn ich nach draußen klickte.

Meine endgültige Implementierung funktioniert perfekt, erfordert jedoch einige CSS3-Animationen und -Styling.

HINWEIS : Ich habe den folgenden Code nicht getestet. Möglicherweise müssen einige Syntaxprobleme behoben werden, auch die offensichtlichen Anpassungen für Ihr eigenes Projekt!

Was ich getan habe:

Ich habe ein separates festes Div mit Höhe 100%, Breite 100% und Transformation erstellt: scale (0), dies ist im Wesentlichen der Hintergrund, Sie können es mit der Hintergrundfarbe stylen: rgba (0, 0, 0, 0,466); Um dies zu verdeutlichen, ist das Menü geöffnet und der Hintergrund ist Click-to-Close. Das Menü erhält einen höheren Z-Index als alles andere, dann erhält das Hintergrund-Div einen niedrigeren Z-Index als das Menü, aber auch einen höheren Wert als alles andere. Dann hat der Hintergrund ein Klickereignis, das das Dropdown-Menü schließt.

Hier ist es mit Ihrem HTML-Code.

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

Hier ist das CSS3, das einige einfache Animationen benötigt.

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

Wenn Sie nichts Visuelles suchen, können Sie einfach einen solchen Übergang verwenden

.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
quelle
1

Ich bin auf eine andere Lösung gestoßen, die von Beispielen mit Fokus- / Unschärfeereignis inspiriert wurde.

Wenn Sie also dieselbe Funktionalität erreichen möchten, ohne den globalen Listener für Dokumente anzuhängen, können Sie das folgende Beispiel als gültig betrachten. Es funktioniert auch in Safari und Firefox unter OSx, obwohl sie andere Behandlung von Schaltflächenfokusereignissen haben: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus

Arbeitsbeispiel für Stackbiz mit Winkel 8: https://stackblitz.com/edit/angular-sv4tbi?file=src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts

HTML-Markup:

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

Die Richtlinie sieht folgendermaßen aus:

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

Sie können mouseleavein Ihrer Ansicht so verwenden

Testen Sie mit Winkel 8 und arbeiten Sie perfekt

<ul (mouseleave)="closeDropdown()"> </ul>
Tony Ngo
quelle
Dadurch wird der Container geschlossen, wenn die Maus verlässt, aber ich danke Ihnen, dass Sie ihn trotzdem geteilt haben, da ich nichts von seiner Existenz wusste.
Ben Hayward
0

DIE ELEGANTSTE METHODE: D.

Es gibt einen einfachsten Weg, dies zu tun, dafür sind keine Anweisungen erforderlich.

"Element-das-umschalten-dein-Dropdown" sollte Button-Tag sein. Verwenden Sie eine beliebige Methode im Attribut (Unschärfe). Das ist alles.

<button class="element-that-toggle-your-dropdown"
               (blur)="isDropdownOpen = false"
               (click)="isDropdownOpen = !isDropdownOpen">
</button>
George Reznichenko
quelle
Dies funktioniert nicht, wenn Sie die Dropdown-Liste beim Klicken geöffnet lassen möchten, z. B. wenn ein Benutzer möglicherweise nicht auf eine Schaltfläche
klickt