Как я могу закрыть раскрывающийся список при нажатии снаружи?


148

Я хотел бы закрыть раскрывающееся меню входа в систему, когда пользователь щелкает где-нибудь за пределами этого раскрывающегося списка, и я хотел бы сделать это с помощью Angular2 и с "подходом" Angular2 ...

Я реализовал решение, но я действительно не уверен в нем. Я думаю, что должен быть самый простой способ добиться того же результата, так что если у вас есть идеи ... давайте обсудим :)!

Вот моя реализация:

Выпадающий компонент:

Это компонент моего раскрывающегося списка:

  • Каждый раз , когда этот компонент он установлен в видимой (Например: когда пользователь нажимает на кнопку , чтобы отобразить его) это подписаться на «глобальный» rxjs предмет UserMenu хранится в SubjectsService .
  • И каждый раз, когда его прячут, он отписывается в этой теме.
  • Каждый щелчок в любом месте в пределах шаблона этого компонента триггера OnClick () метод, который просто остановка событие барботирования в верхней части (и компонент приложения)

Вот код

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

Компонент приложения:

С другой стороны, есть компонент приложения (который является родителем раскрывающегося компонента):

  • Этот компонент улавливает каждое событие щелчка и генерирует один и тот же объект rxjs ( userMenu )

Вот код:

export class AppComponent {

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

Что меня беспокоит:

  1. Мне не очень нравится идея иметь глобальный субъект, который действует как связующее звено между этими компонентами.
  2. SetTimeout : Это необходимо , потому что здесь это то , что произойдет в противном случае , если пользователь нажимает на кнопку , которая показывает выпадающее меню:
    • Пользователь нажимает кнопку (которая не является частью раскрывающегося списка), чтобы отобразить раскрывающийся список.
    • Отображается раскрывающийся список, и он сразу же подписывается на тему userMenu .
    • Пузырь события клика поднимается до компонента приложения и ловится
    • Компонент приложения генерирует событие для темы userMenu
    • Компонент раскрывающегося списка улавливает это действие в userMenu и скрывает раскрывающийся список.
    • В конце раскрывающийся список никогда не отображается.

Этот установленный тайм-аут задерживает подписку до конца текущей очереди кода JavaScript, что решает проблему, но, на мой взгляд, очень элегантно.

Если вы знаете более чистые, лучшие, умные, быстрые или сильные решения, дайте мне знать :)!


Эти ответы могут дать вам некоторые идеи: stackoverflow.com/a/35028820/215945 , stackoverflow.com/questions/35024495#35024651
Марк Райкок 01

Ответы:


250

Вы можете использовать (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();
  }
}

Другой подход - создать настраиваемое событие в виде директивы. Посмотрите эти сообщения Бена Наделя:


1
@Sasxa спасибо и согласился. Я подумал, что если бы существовал нерекомендуемый документ API, он бы появился в поиске, который привел меня сюда.
danludwig

4
Если event.target - это элемент, который был добавлен динамически через что-то вроде привязки [innerHTML], тогда nativeElement elementRef не будет его содержать.
Патрик Грэм

8
Единственным недостатком этого метода является то, что теперь у вас есть прослушиватель событий щелчка в вашем приложении, который запускается каждый раз, когда вы щелкаете мышью.
codeepic

38
Согласно официальному руководству по стилю Angular 2, вы должны использовать @HostListener('document:click', ['$event'])вместо hostсвойства в Componentдекораторе.
Michał Miszczyszyn

15
или вы можете просто использовать для этого rxjs, например Observable.fromEvent(document, 'click').subscribe(event => {your code here}), чтобы вы всегда могли подписаться только тогда, когда вам нужно слушать, например, вы открыли раскрывающийся список, а когда вы его закрываете, вы
Blind Despair

44

ЭЛЕГАНТНЫЙ МЕТОД

Я нашел эту clickOutдирективу: https://github.com/chliebel/angular2-click-outside . Проверяю, работает хорошо (копирую только clickOutside.directive.tsв свой проект). Вы можете использовать это так:

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

Где closeнаходится ваша функция, которая будет вызываться, когда пользователь щелкнет за пределами div. Это очень элегантный способ решения описанной проблемы.

Если вы используете указанную выше директиву для закрытия всплывающего окна, не забудьте сначала добавить event.stopPropagation()в обработчик события нажатия кнопки, который открывает всплывающее окно.

БОНУС:

Ниже я копирую исходный код директивы из файла clickOutside.directive.ts(на случай, если ссылка перестанет работать в будущем) - автор Кристиан Либель :


2
@Vega Я бы порекомендовал использовать директиву в элементе с * ng Если, в случае выпадающих списков это может быть что-то вроде<div class="wrap" *ngIf="isOpened" (clickOutside)="...// this should set this.isOpen=false"
Габриэль Бальса Канту

19

Я так и сделал.

Добавил прослушиватель событий в документ clickи в этом обработчике проверил, containerсодержит ли мой event.target, если нет - скрыть раскрывающийся список.

Это выглядело бы так.

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

Здравствуй. Необходим ли the.bind (this)?
Дренай

1
@Brian: Это может быть необходимо, а может и нет, но определенно не было бы, если бы он this.offClickHandlerзаключил в функцию стрелки.
Camara

17

Я думаю, что принятый ответ Sasxa работает для большинства людей. Однако у меня была ситуация, когда содержимое элемента, который должен прослушивать события вне щелчка, изменялся динамически. Таким образом, Elements nativeElement не содержал event.target, когда он создавался динамически. Я мог бы решить эту проблему с помощью следующей директивы

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

Вместо того, чтобы проверять, содержит ли elementRef event.target, я проверяю, находится ли elementRef в пути (путь DOM к цели) события. Таким образом можно обрабатывать динамически созданные элементы.


Спасибо - это работает лучше, когда присутствуют дочерние компоненты
MAhsan

Это мне очень помогло. не уверен, почему щелчок за пределами компонента не обнаруживался с другими ответами.
JavaQuest

13

Если вы делаете это на iOS, также используйте touchstartсобытие:

Начиная с Angular 4, HostListenerукрашение является предпочтительным способом сделать это.

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

}

10

Сегодня мы работаем над аналогичной проблемой, пытаясь выяснить, как заставить выпадающий div исчезать при нажатии на него. Наш вопрос немного отличается от вопроса первоначального постера, потому что мы не хотели уходить от другого компонента или директивы , а просто за пределами конкретного div.

В итоге мы решили ее с помощью обработчика событий (window: mouseup).

Шаги:
1.) Мы дали всему раскрывающемуся меню div уникальное имя класса.

2.) В самом внутреннем раскрывающемся меню (единственной части, в которой мы хотели, чтобы щелчки НЕ закрывали меню), мы добавили обработчик событий (window: mouseup) и передали в событие $.

ПРИМЕЧАНИЕ. Это невозможно сделать с помощью обычного обработчика щелчка, поскольку он конфликтует с родительским обработчиком щелчка.

3.) В нашем контроллере мы создали метод, который должен вызываться при событии щелчка мышью, и мы используем event.closest ( здесь документы ), чтобы узнать, находится ли место, на которое щелкнули, в пределах нашего div целевого класса.

 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>


"window: mouseup" следует использовать в декораторе хоста.
Шивам

@ Shivam - Я не совсем понимаю, что вы имеете в виду под "должно использоваться в декораторе хоста". Не могли бы вы объяснить дальше? Благодарность!
Paige Bolduc

Я имею в виду, что вместо прямого использования объекта "окно" вы должны использовать свойство "host" декоратора компонента / декоратора "HostListener" компонента. Это стандартная практика при работе с объектом «окно» или «документ» в angular 2.
Шивам

2
Просто следите за совместимостью браузера, .closest()на сегодняшний день не поддерживается в IE / Edge ( caniuse )
superjos

5

Вы можете создать элемент-брат для раскрывающегося списка, охватывающий весь экран, который будет невидимым и будет использоваться только для захвата событий щелчка. Затем вы можете обнаруживать щелчки по этому элементу и закрывать раскрывающийся список при щелчке по нему. Допустим, этот элемент относится к классу шелкографии, вот какой стиль для него:

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

Z-index должен быть достаточно высоким, чтобы размещать его над всем, кроме раскрывающегося списка. В этом случае мое раскрывающееся меню будет b z-index 2.

Другие ответы в некоторых случаях работали для меня, за исключением случаев, когда мой раскрывающийся список закрывался, когда я взаимодействовал с элементами внутри него, а я этого не хотел. У меня были динамически добавленные элементы, которые не содержались в моем компоненте, в соответствии с целью события, как я и ожидал. Я решил, что вместо того, чтобы разбираться с этим беспорядком, просто попробую сделать это методом шелкографии.


5

Я не нашел обходного пути. Я только что прикрепил документ: нажмите на мою функцию переключения, как показано ниже:

    @Directive ({
      селектор: '[appDropDown]'
    })
    класс экспорта DropdownDirective реализует OnInit {

      @HostBinding ('class.open') isOpen: логический;

      конструктор (частный elemRef: ElementRef) {}

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

      @HostListener ('документ: щелчок', ['$ event'])
      @HostListener ('документ: touchstart', ['$ event'])
      toggle (событие) {
        if (this.elemRef.nativeElement.contains (event.target)) {
          this.isOpen =! this.isOpen;
        } else {
          this.isOpen = false;
      }
    }

Итак, когда я выхожу за пределы своей директивы, я закрываю раскрывающийся список.


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

СНИЖЕНИЯ: - Два прослушивателя событий щелчка для каждого из этих компонентов на странице. Не используйте это для компонентов, которые появляются на странице сотни раз.


Нет, я использовал его только в браузере рабочего стола.
Alex Egli

3

Я хотел бы дополнить ответ @Tony, поскольку событие не удаляется после щелчка за пределами компонента. Полная квитанция:

  • Отметьте свой основной элемент с помощью #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);
    
  • На интерактивном элементе используйте:

    (click)="dropstatus=true"
    

Теперь вы можете контролировать свое раскрывающееся состояние с помощью переменной dropstatus и применять соответствующие классы с помощью [ngClass] ...


3

Вы можете написать директиву:

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


}

В вашем компоненте:

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

Эта директива генерирует событие, когда элемент html содержится в DOM и когда свойство input [clickOut] имеет значение «true». Он прослушивает событие mousedown для обработки события до того, как элемент будет удален из DOM.

И одно примечание: firefox не содержит свойство path, если вы можете использовать функцию для создания пути:

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

Поэтому вам следует изменить обработчик событий в директиве: event.path следует заменить getEventPath (event)

Этот модуль может помочь. https://www.npmjs.com/package/ngx-clickout Он содержит ту же логику, но также обрабатывает событие esc в исходном элементе html.


3

При правильном ответе есть проблема: если у вас есть компонент clicakble в вашем всплывающем окне, элемент больше не будет использоваться в containметоде и закроется, на основе @ JuHarm89 я создал свой собственный:

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

Спасибо за помощь!


2

Лучшая версия для отличного решения @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");

        }
    }
}

В файле css: // НЕ требуется, если вы используете раскрывающийся список начальной загрузки.

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

2

Вы должны проверить, нажимаете ли вы вместо этого модальное наложение, намного проще.

Ваш шаблон:

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

И способ:

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


  }

1

Если вы используете Bootstrap, вы можете сделать это напрямую с помощью способа начальной загрузки через раскрывающиеся списки (компонент 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>

Теперь можно положить что- (click)="clickButton()"нибудь на кнопку. http://getbootstrap.com/javascript/#dropdowns


1

Я также сделал небольшое собственное обходное решение.

Я создал событие (dropdownOpen), которое я прослушиваю в своем компоненте элемента ng-select, и вызываю функцию, которая закроет все другие открытые компоненты SelectComponent, кроме открытого в данный момент SelectComponent.

Я изменил одну функцию внутри файла select.ts, как показано ниже, чтобы генерировать событие:

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

В HTML я добавил прослушиватель событий для (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>

Это моя вызывающая функция для триггера события внутри компонента, имеющего тег 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();
    })
}

1

НОТА: Для тех, кто хочет использовать веб-воркеров, и вам нужно избегать использования document и nativeElement, это сработает.

Я ответил на тот же вопрос здесь: /programming/47571144

Скопируйте / Вставьте из приведенной выше ссылки:

У меня была такая же проблема, когда я создавал раскрывающееся меню и диалоговое окно подтверждения, которое я хотел отклонить при нажатии снаружи.

Моя последняя реализация работает отлично, но требует некоторой анимации и стиля css3.

ПРИМЕЧАНИЕ : я не тестировал приведенный ниже код, могут быть некоторые проблемы с синтаксисом, которые необходимо устранить, а также очевидные корректировки для вашего собственного проекта!

Что я сделал:

Я сделал отдельный фиксированный div с высотой 100%, шириной 100% и transform: scale (0), это, по сути, фон, вы можете стилизовать его с помощью background-color: rgba (0, 0, 0, 0.466); чтобы было видно, что меню открыто, а фон закрывается щелчком. Меню получает z-индекс выше, чем все остальное, тогда фоновый div получает z-индекс ниже, чем у меню, но также выше, чем все остальное. Затем на фоне появляется событие щелчка, закрывающее раскрывающийся список.

Вот он с вашим 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>

Вот CSS3, для которого нужны простые анимации.

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

Если вам не нужно ничего визуального, вы можете просто использовать такой переход

.dropdownbackground{
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 2;
    transform: scale(0);
    opacity: 0;
    transition all 0.1s;
}

.dropdownbackground.showbackground{
     transform: scale(1);
}

1

Я сделал директиву для решения этой похожей проблемы и использую Bootstrap. Но в моем случае, вместо того, чтобы ждать события щелчка вне элемента, чтобы закрыть текущее открытое раскрывающееся меню, я думаю, что лучше, если мы будем следить за событием mouseleave, чтобы автоматически закрыть меню.

Вот мое решение:

Директива

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>

1

Я наткнулся на другое решение, вдохновленное примерами с событием focus / blur.

Итак, если вы хотите достичь той же функциональности без подключения глобального прослушивателя документов, вы можете считать допустимым следующий пример. Он также работает в Safari и Firefox на OSx, несмотря на то, что у них есть другая обработка события фокусировки кнопки: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus

Рабочий пример на stackbiz с angular 8: https://stackblitz.com/edit/angular-sv4tbi?file=src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts

Разметка 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>

Директива будет выглядеть так:

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

1

Вы можете использовать mouseleaveв своем представлении вот так

Протестируйте с angular 8 и отлично работайте

<ul (mouseleave)="closeDropdown()"> </ul>

Это закроет контейнер, когда мышь покинет, но в любом случае спасибо за то, что поделились, поскольку я не знал о его существовании.
Бен Хейворд,

0

САМЫЙ ЭЛЕГАНТНЫЙ МЕТОД: D

Есть один самый простой способ сделать это, для этого не нужны никакие директивы.

"element-that-toggle-your-dropdown" должен быть тегом кнопки. Используйте любой метод в атрибуте (blur). Вот и все.

<button class="element-that-toggle-your-dropdown"
               (blur)="isDropdownOpen = false"
               (click)="isDropdownOpen = !isDropdownOpen">
</button>

Это не сработает, если вы хотите, чтобы раскрывающийся список оставался открытым при нажатии, например, пользователь может пропустить нажатие кнопки
Да, когда
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.