Jak mogę zamknąć menu po kliknięciu na zewnątrz?

144

Chciałbym zamknąć menu logowania, gdy użytkownik kliknie gdziekolwiek poza tym menu i chciałbym to zrobić z Angular2 i „podejściem” Angular2 ...

Wdrożyłem rozwiązanie, ale naprawdę nie czuję się z nim pewnie. Myślę, że musi być najłatwiejszy sposób na osiągnięcie tego samego rezultatu, więc jeśli masz jakieś pomysły ... porozmawiajmy :)!

Oto moja realizacja:

Składnik rozwijany:

To jest składnik mojego menu:

  • Za każdym razem, gdy ten komponent jest ustawiony jako widoczny (na przykład: gdy użytkownik kliknie przycisk, aby go wyświetlić), subskrybuje „globalne” menu użytkownika tematu rxjs przechowywane w usłudze SubjectsService .
  • I za każdym razem, gdy jest ukryty, wypisuje się z tego tematu.
  • Każde kliknięcie w dowolnym miejscu w szablonie tego komponentu wyzwala metodę onClick () , która po prostu zatrzymuje bulgotanie zdarzeń na górę (i komponent aplikacji)

Oto kod

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

Składnik aplikacji:

Z drugiej strony istnieje komponent aplikacji (który jest rodzicem komponentu rozwijanego):

  • Ten komponent przechwytuje każde zdarzenie kliknięcia i emituje na tym samym temacie rxjs ( userMenu )

Oto kod:

export class AppComponent {

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

Co mi przeszkadza:

  1. Nie czuję się komfortowo z pomysłem posiadania globalnego podmiotu, który działa jako łącznik między tymi komponentami.
  2. SetTimeout : Jest to konieczne, ponieważ tutaj jest to, co się stało inaczej, gdy użytkownik kliknie na przycisk, który pokazuje listę rozwijaną:
    • Użytkownik klika przycisk (który nie jest częścią rozwijanego komponentu), aby wyświetlić listę rozwijaną.
    • Zostanie wyświetlone menu rozwijane, które natychmiast zasubskrybuje temat menu użytkownika .
    • Zdarzenie kliknięcia pojawia się w dymku do komponentu aplikacji i zostaje złapane
    • Komponent aplikacji emituje zdarzenie w temacie userMenu
    • Komponent listy rozwijanej przechwytuje tę akcję w userMenu i ukrywa listę rozwijaną.
    • Na końcu lista rozwijana nigdy nie jest wyświetlana.

Ten ustawiony limit czasu opóźnia subskrypcję do końca bieżącej tury kodu JavaScript, co rozwiązuje problem, ale moim zdaniem w bardzo elegancki sposób.

Jeśli znasz czystsze, lepsze, mądrzejsze, szybsze lub mocniejsze rozwiązania, daj mi znać :)!

Łaskawy
źródło
Te odpowiedzi mogą dać ci kilka pomysłów: stackoverflow.com/a/35028820/215945 , stackoverflow.com/questions/35024495#35024651
Mark Rajcok

Odpowiedzi:

245

Możesz użyć (document:click)wydarzenia:

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

Innym podejściem jest utworzenie zdarzenia niestandardowego jako dyrektywy. Sprawdź te posty Bena Nadela:

Sasxa
źródło
1
@Sasxa dziękuję i zgodził się. Pomyślałem, że gdyby istniał dokument API, który nie jest przestarzały, pojawiłby się w wynikach wyszukiwania, które doprowadziły mnie do tego miejsca.
danludwig
4
Jeśli event.target jest elementem, który został dodany dynamicznie przez coś takiego jak powiązanie [innerHTML], to element nativeElement elementuRef nie będzie go zawierał.
Patrick Graham,
8
Jedyną wadą tej techniki jest to, że teraz masz w swojej aplikacji odbiornik zdarzeń kliknięcia, który jest uruchamiany za każdym razem, gdy klikniesz.
codeepic
37
Zgodnie z oficjalnym przewodnikiem stylistycznym Angular 2 należy używać dekoratora @HostListener('document:click', ['$event'])zamiast hostwłaściwości Component.
Michał Miszczyszyn
15
lub możesz po prostu użyć do tego rxjs, na przykład Observable.fromEvent(document, 'click').subscribe(event => {your code here}), więc zawsze możesz subskrybować tylko wtedy, gdy chcesz słuchać, na przykład otworzyłeś rozwijane menu, a kiedy je zamkniesz, anulujesz subskrypcję
Blind Despair
42

ELEGANCKA METODA

Znalazłem tę clickOutdyrektywę: https://github.com/chliebel/angular2-click-outside . Sprawdzam i działa dobrze (kopiuję tylko clickOutside.directive.tsdo mojego projektu). Możesz to wykorzystać w ten sposób:

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

Gdzie closejest twoja funkcja, która zostanie wywołana, gdy użytkownik kliknie poza div. Jest to bardzo elegancki sposób na rozwiązanie opisywanego problemu.

Jeśli używasz powyższej dyrektywy, aby zamknąć okno popUp, pamiętaj, aby najpierw dodać event.stopPropagation()do programu obsługi zdarzenia kliknięcia przycisku, który otwiera okno popUp.

PREMIA:

Poniżej kopiuję kod dyrektywy z pliku clickOutside.directive.ts(na wypadek gdyby link przestał działać w przyszłości) - autorem jest Christian Liebel :

Kamil Kiełczewski
źródło
2
@Vega Moją rekomendacją byłoby użycie dyrektywy w elemencie z * ng Jeśli w przypadku list rozwijanych może to być coś takiego<div class="wrap" *ngIf="isOpened" (clickOutside)="...// this should set this.isOpen=false"
Gabriel Balsa Cantú
19

Zrobiłem to w ten sposób.

Dodano detektor zdarzeń na dokumencie clickiw tym module obsługi sprawdzono, czy moje containerzawiera event.target, jeśli nie - ukryj listę rozwijaną.

Wyglądałoby to tak.

@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
źródło
Cześć. Czy the.bind (this) jest konieczne?
Drenai
1
@Brian To może być konieczne lub nie, ale na pewno nie byłoby, gdyby zawinął this.offClickHandlerfunkcję strzałkową.
Lansana Camara
17

Myślę, że zaakceptowana odpowiedź Sasxa działa dla większości ludzi. Miałem jednak sytuację, w której zawartość elementu, który powinien nasłuchiwać zdarzeń off-click, zmieniała się dynamicznie. Zatem element nativeElement nie zawierał zdarzenia event.target, gdy został utworzony dynamicznie. Mógłbym rozwiązać ten problem za pomocą następującej dyrektywy

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

Zamiast sprawdzać, czy elementRef zawiera event.target, sprawdzam, czy elementRef znajduje się w ścieżce (DOM path to target) zdarzenia. W ten sposób można obsługiwać dynamicznie tworzone elementy.

JuHarm89
źródło
Dzięki - działa to lepiej, gdy obecne są komponenty podrzędne
MAhsan
To było dla mnie bardzo pomocne. nie jestem pewien, dlaczego kliknięcie poza komponentem nie zostało wykryte z innymi odpowiedziami.
JavaQuest
13

Jeśli robisz to na iOS, użyj również touchstartwydarzenia:

Od Angular 4 HostListenerdekorowanie jest preferowanym sposobem na zrobienie tego

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
źródło
10

Pracowaliśmy dzisiaj nad podobnym problemem w pracy, próbując wymyślić, jak sprawić, by rozwijany element div znikał po kliknięciu go. Nasze pytanie różni się nieco od pierwszego pytania zamieszczającego, ponieważ nie chcieliśmy odrywać się od innego składnika lub dyrektywy , a jedynie poza konkretny element div.

Skończyło się na tym, że rozwiązaliśmy to za pomocą procedury obsługi zdarzeń (window: mouseup).

Kroki:
1.) Nadaliśmy całemu div menu rozwijanego unikalną nazwę klasy.

2.) W samym wewnętrznym menu rozwijanym (jedyna część, którą chcieliśmy kliknąć, aby NIE zamknąć menu), dodaliśmy procedurę obsługi zdarzenia (window: mouseup) i przekazaliśmy zdarzenie $.

UWAGA: Nie można tego zrobić za pomocą typowego modułu obsługi kliknięcia, ponieważ powoduje to konflikt z nadrzędnym modułem obsługi kliknięcia.

3.) W naszym kontrolerze stworzyliśmy metodę, którą chcieliśmy wywołać w zdarzeniu click out, i używamy event.closest ( dokumentacja tutaj ), aby dowiedzieć się, czy kliknięte miejsce znajduje się w naszym elemencie div klasy docelowej.

 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
źródło
"window: mouseup" powinno być używane w dekoratorze hosta.
Shivam,
@ Shivam - Nie jestem pewien, co masz na myśli, mówiąc „powinien być używany w dekoratorze hosta”. Czy mógłbyś wyjaśnić dalej? Dzięki!
Paige Bolduc
Chodzi mi o to, że zamiast bezpośrednio używać obiektu "window", powinieneś użyć właściwości "host" dekoratora komponentu / dekoratora "HostListener" komponentu. Jest to standardowa praktyka podczas pracy z obiektem „okno” lub „dokument” w trybie kątowym 2.
Shivam,
2
Miej tylko oko na kompatybilność z przeglądarkami, .closest()na dzień dzisiejszy nie jest obsługiwane w IE / Edge ( caniuse )
superjos Kwietnia
5

Możesz utworzyć element siostrzany w menu rozwijanym, który obejmuje cały ekran, który byłby niewidoczny i dostępny tylko do przechwytywania zdarzeń kliknięcia. Następnie możesz wykryć kliknięcia tego elementu i zamknąć menu po kliknięciu. Powiedzmy, że element jest klasy sitodruku, oto styl:

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

Indeks Z musi być wystarczająco wysoki, aby umieścić go nad wszystkim oprócz listy rozwijanej. W tym przypadku moje menu rozwijane byłoby b z-index 2.

Inne odpowiedzi działały w niektórych przypadkach dla mnie, z wyjątkiem tego, że czasami moje menu zamykało się, gdy wchodziłem w interakcję z elementami w nim zawartymi, a tego nie chciałem. Dodałem dynamicznie elementy, których nie było w moim komponencie, zgodnie z celem zdarzenia, tak jak się spodziewałem. Zamiast uporządkować ten bałagan, pomyślałem, że po prostu spróbuję tego metodą sitodruku.

Patrick Graham
źródło
5

Nie wykonałem żadnego obejścia. Właśnie załączyłem dokument: kliknij moją funkcję przełączania w następujący sposób:

    @Dyrektywa({
      selector: „[appDropDown]”
    })
    klasa eksportu DropdownDirective implementuje OnInit {

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

      konstruktor (prywatny elemRef: ElementRef) {}

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

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

Tak więc, gdy jestem poza zakresem mojej dyrektywy, zamykam menu.

Elie Nehmé
źródło
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;
    }
}

WADY: - Dwa detektory zdarzeń kliknięcia dla każdego z tych składników na stronie. Nie używaj tego na komponentach, które znajdują się na stronie setki razy.

Alex Egli
źródło
Nie, używałem go tylko w przeglądarce na komputerze.
Alex Egli
3

Chciałbym uzupełnić odpowiedź @Tony, ponieważ zdarzenie nie jest usuwane po kliknięciu poza komponentem. Kompletny odbiór:

  • Oznacz swój główny element #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);
  • Na klikalnym elemencie użyj:

    (click)="dropstatus=true"

Teraz możesz kontrolować stan listy rozwijanej za pomocą zmiennej dropstatus i stosować odpowiednie klasy za pomocą [ngClass] ...

Gaus
źródło
3

Możesz napisać dyrektywę:

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


}

W twoim komponencie:

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

Ta dyrektywa emituje zdarzenie, gdy element html jest zawarty w DOM i gdy właściwość input [clickOut] ma wartość „true”. Nasłuchuje zdarzenia mousedown, aby obsłużyć zdarzenie, zanim element zostanie usunięty z DOM.

I jedna uwaga: Firefox nie zawiera właściwości 'path' w przypadku zdarzenia, którego możesz użyć do utworzenia ścieżki:

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

Więc powinieneś zmienić obsługę zdarzeń w dyrektywie: event.path powinien zostać zastąpiony getEventPath (event)

Ten moduł może pomóc. https://www.npmjs.com/package/ngx-clickout Zawiera tę samą logikę, ale obsługuje również zdarzenie esc w źródłowym elemencie html.

Alex Mikitevich
źródło
3

Prawidłowa odpowiedź ma problem, jeśli masz klikalny komponent w swoim wyskakującym okienku, element nie będzie już w containmetodzie i zostanie zamknięty, na podstawie @ JuHarm89 stworzyłem własny:

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

Dzięki za pomoc!

Douglas Caina
źródło
2

Lepsza wersja dla @Tony świetne rozwiązanie:

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

        }
    }
}

W pliku css: // NIE jest potrzebne, jeśli używasz listy rozwijanej bootstrap.

.ourDropdown{
   display: none;
}
.ourDropdown.open{
   display: inherit;
}
Dudi
źródło
2

Powinieneś sprawdzić, czy zamiast tego klikasz nakładkę modalną, o wiele łatwiej.

Twój szablon:

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

I metoda:

  @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
źródło
2

Stworzyłem dyrektywę, aby rozwiązać ten podobny problem i używam Bootstrap. Ale w moim przypadku zamiast czekać, aż zdarzenie kliknięcia poza elementem zamknie aktualnie otwarte menu rozwijane, myślę, że lepiej jest obserwować zdarzenie „mouseleave”, aby automatycznie zamknąć menu.

Oto moje rozwiązanie:

Dyrektywa

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
źródło
1

Jeśli używasz Bootstrap, możesz to zrobić bezpośrednio za pomocą metody bootstrap poprzez listy rozwijane (komponent 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>

Teraz można umieścić (click)="clickButton()"rzeczy na przycisku. http://getbootstrap.com/javascript/#dropdowns

vusan
źródło
1

Zrobiłem też własne małe obejście.

Utworzyłem zdarzenie (dropdownOpen) , którego słucham w moim komponencie elementu ng-select i wywołuję funkcję, która zamknie wszystkie inne otwarte elementy SelectComponent oprócz aktualnie otwartego SelectComponent.

Zmodyfikowałem jedną funkcję w pliku select.ts , jak poniżej, aby emitować zdarzenie:

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

W HTML dodałem detektor zdarzeń dla (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>

To jest moja funkcja wywołująca wyzwalacz zdarzenia wewnątrz komponentu mającego 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
źródło
1

UWAGA: Dla tych, którzy chcą korzystać z pracowników sieci i powinni unikać korzystania z document i nativeElement, to zadziała.

Odpowiedziałem na to samo pytanie tutaj: /programming/47571144

Skopiuj / wklej z powyższego linku:

Miałem ten sam problem, gdy tworzyłem menu rozwijane i okno dialogowe potwierdzenia, które chciałem odrzucić po kliknięciu na zewnątrz.

Moja ostateczna implementacja działa idealnie, ale wymaga trochę animacji css3 i stylizacji.

UWAGA : nie testowałem poniższego kodu, mogą wystąpić problemy ze składnią, które należy rozwiązać, a także oczywiste poprawki dla własnego projektu!

Co ja zrobiłem:

Zrobiłem osobny stały div z wysokością 100%, szerokością 100% i transformacją: scale (0), to jest w zasadzie tło, możesz je stylizować na kolor tła: rgba (0, 0, 0, 0.466); aby było oczywiste, że menu jest otwarte, a tło jest zamykane przez kliknięcie. Menu otrzymuje indeks Z wyższy niż wszystko inne, a następnie element div tła otrzymuje indeks Z niższy niż menu, ale także wyższy niż wszystko inne. Następnie w tle znajduje się zdarzenie kliknięcia, które zamyka menu.

Tutaj jest z Twoim kodem 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>

Oto css3, który potrzebuje kilku prostych animacji.

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

Jeśli nie szukasz niczego wizualnego, możesz po prostu użyć takiego przejścia

.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
źródło
1

Natrafiłem na inne rozwiązanie, zainspirowane przykładami z fokusem / rozmyciem.

Tak więc, jeśli chcesz osiągnąć tę samą funkcjonalność bez dołączania globalnego nasłuchiwania dokumentów, możesz rozważyć następujący przykład jako prawidłowy. Działa również w Safari i Firefox na OSx, mimo że mają inną obsługę zdarzenia skupienia przycisku: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus

Przykład pracy na stackbiz z Angular 8: https://stackblitz.com/edit/angular-sv4tbi?file=src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts

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

Dyrektywa będzie wyglądać następująco:

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
źródło
1

Możesz użyć mouseleavew swoim widoku w ten sposób

Przetestuj za pomocą kątownika 8 i działaj idealnie

<ul (mouseleave)="closeDropdown()"> </ul>
Tony Ngo
źródło
Spowoduje to zamknięcie pojemnika, gdy mysz opuści, ale i tak dziękuję za udostępnienie, ponieważ nie wiedziałem o jego istnieniu.
Ben Hayward,
0

NAJBARDZIEJ ELEGANCKA METODA: D.

Jest na to najłatwiejszy sposób, nie potrzeba do tego żadnych dyrektyw.

„element-to-przełącz-twoje-menu” powinno być tagiem przycisku. Użyj dowolnej metody w atrybucie (blur). To wszystko.

<button class="element-that-toggle-your-dropdown"
               (blur)="isDropdownOpen = false"
               (click)="isDropdownOpen = !isDropdownOpen">
</button>
George Reznichenko
źródło
To nie zadziała, jeśli chcesz, aby
lista