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

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

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

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

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

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

  • Каждый раз, когда этот компонент становится видимым (например, когда пользователь нажимает на кнопку, чтобы отобразить его), он подписывается на "глобальный" субъект-пользователь rxjs, хранящийся в 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.
    • Событие click всплывает до компонента приложения и попадает
    • Компонент приложения отправляет событие на тему userMenu
    • Компонент раскрывающегося списка перехватывает это действие в userMenu и скрывает раскрывающийся список.
    • В конце выпадающий никогда не отображается.

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

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

26 ответов

Решение

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

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

ЭЛЕГАНТНЫЙ МЕТОД: Я нашел это clickOut директива: https://github.com/chliebel/angular2-click-outside

Я проверяю это, и это работает хорошо (я только копирую clickOutside.directive.ts в мой проект). Вы можете использовать это следующим образом:

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

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

БОНУС:

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

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

Я сделал это таким образом.

Добавлен прослушиватель событий в документ 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";
        }
    }
}

Я думаю, что Сасса принял ответ работает для большинства людей. Однако у меня возникла ситуация, когда содержимое Элемента, которое должно прослушивать события отсутствия щелчка, динамически изменялось. Таким образом, Elements native Element не содержал 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 к цели) события. Таким образом, можно обрабатывать динамически созданные элементы.

Если вы делаете это на 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();
    }
  }

}

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

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

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

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

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

3.) В нашем контроллере мы создали метод, который мы хотим вызывать для события click out, и мы используем 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>

Вы можете использовать mouseleave на ваш взгляд, как это

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

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

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

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

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

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

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

      @HostListener('document:click', ['$event'])
      @HostListener('document:touchstart', ['$event'])
      переключатель (событие) {
        if (this.elemRef.nativeElement.contains(event.target)) {
          this.isOpen =!this.isOpen;
        } еще {
          this.isOpen = false;
      }
    }

Поэтому, когда я не в своей директиве, я закрываю выпадающий список.

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

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

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

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

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

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

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

Ваш шаблон:

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


  }

Я хотел бы дополнить ответ @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]...

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

@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 и когда входное свойство [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-элемента.

Правильный ответ имеет проблему, если у вас есть всплывающий компонент в вашем поповере, элемент больше не будет на 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();
    }
  }
}

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

Я не думал, что ответов достаточно, поэтому хочу внести свой вклад. Вот что я сделал

component.ts

      @Component({
    selector: 'app-issue',
    templateUrl: './issue.component.html',
    styleUrls: ['./issue.component.sass'],
})
export class IssueComponent {
    @Input() issue: IIssue;
    @ViewChild('issueRef') issueRef;
    
    public dropdownHidden = true;
    
    constructor(private ref: ElementRef) {}

    public toggleDropdown($event) {
        this.dropdownHidden = !this.dropdownHidden;
    }
    
    @HostListener('document:click', ['$event'])
    public hideDropdown(event: any) {
        if (!this.dropdownHidden && !this.issueRef.nativeElement.contains(event.target)) {
            this.dropdownHidden = true;
        }
    }
}

component.html

      <div #issueRef (click)="toggleDropdown()">
    <div class="card card-body">
        <p class="card-text truncate">{{ issue.fields.summary }}</p>
        <div class="d-flex justify-content-between">
            <img
                *ngIf="issue.fields.assignee; else unassigned"
                class="rounded"
                [src]="issue.fields.assignee.avatarUrls['32x32']"
                [alt]="issue.fields.assignee.displayName"
            />
            <ng-template #unassigned>
                <img
                    class="rounded"
                    src="https://img.icons8.com/pastel-glyph/2x/person-male--v2.png"
                    alt="Unassigned"
                />
            </ng-template>
            <img
                *ngIf="issue.fields.priority"
                class="rounded mt-auto priority"
                [src]="issue.fields.priority.iconUrl"
                [alt]="issue.fields.priority.name"
            />
        </div>
    </div>
    <div *ngIf="!dropdownHidden" class="list-group context-menu">
        <a href="#" class="list-group-item list-group-item-action active" aria-current="true">
            The current link item
        </a>
        <a href="#" class="list-group-item list-group-item-action">A second link item</a>
        <a href="#" class="list-group-item list-group-item-action">A third link item</a>
        <a href="#" class="list-group-item list-group-item-action">A fourth link item</a>
        <a
            href="#"
            class="list-group-item list-group-item-action disabled"
            tabindex="-1"
            aria-disabled="true"
            >A disabled link item</a
        >
    </div>
</div>

Лучшая версия для @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;
}

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

      document.addEventListener("mouseover", e => { 
  const isDropdownButton = e.target.matches("[data-dropdown-button]")
  if (!isDropdownButton && e.closest('[data-dropdown]') != null) return
  
  let currentDropDown
  if (isDropdownButton) {
    currentDropdown = e.target.closest('[data-dropdown]')
    currentDropdown.classList.toggle('active')
  }
  
  document.querySelectorAll("[data-dropdown].active").forEach(dropdown => {
    if (dropdown === currentDropdown) return
    dropdown.classList.remove("active")
  })
})

который работает хорошо, так как наведение мыши открывает раскрывающийся список и держит его открытым. Но не хватает двух функций.

  1. Когда я нажимаю в другом месте, раскрывающийся список не закрывается.
  2. Когда я нажимаю на раскрывающееся меню, перейдите на URL-адрес. Спасибо

ПРИМЕЧАНИЕ: для тех, кто хочет использовать веб-работников, и вам нужно избегать использования документа и nativeElement, это будет работать.

Я ответил на тот же вопрос здесь: https://stackru.com/questions/47571144

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

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

Моя финальная реализация работает отлично, но требует анимации и стиля CSS3.

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

Что я сделал:

Я сделал отдельный фиксированный div с высотой 100%, шириной 100% и трансформировал: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);
}

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

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

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

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

Я также сделал небольшой обходной путь самостоятельно.

Я создал событие (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();
    })
}

Если вы используете 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/

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

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

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

Рабочий пример на 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();
  }
}

Это пример кнопки Angular Bootstrap DropDowns с закрытием за пределами компонента.

без использования bootstrap.js

      // .html
<div class="mx-3 dropdown" [class.show]="isTestButton">
  <button class="btn dropdown-toggle"
          (click)="isTestButton = !isTestButton">
    <span>Month</span>
  </button>
  <div class="dropdown-menu" [class.show]="isTestButton">
    <button class="btn dropdown-item">Month</button>
    <button class="btn dropdown-item">Week</button>
  </div>
</div>

// .ts
import { Component, ElementRef, HostListener } from "@angular/core";

@Component({
  selector: "app-test",
  templateUrl: "./test.component.html",
  styleUrls: ["./test.component.scss"]
})
export class TestComponent {

  isTestButton = false;

  constructor(private eleRef: ElementRef) {
  }


  @HostListener("document:click", ["$event"])
  docEvent($e: MouseEvent) {
    if (!this.isTestButton) {
      return;
    }
    const paths: Array<HTMLElement> = $e["path"];
    if (!paths.some(p => p === this.eleRef.nativeElement)) {
      this.isTestButton = false;
    }
  }
}

Я решил опубликовать собственное решение на основе моего варианта использования. У меня есть href с событием (щелчок) в Angular 11. Это переключает компонент меню в главном app.ts на off/

      <li><a href="javascript:void(0)" id="menu-link" (click)="toggleMenu();" ><img id="menu-image" src="img/icons/menu-white.png" ></a></li>

Компонент меню (например, div) отображается (*ngIf) на основе логического значения isMenuVisible. И, конечно же, это может быть раскрывающийся список или любой компонент.

В app.ts у меня есть эта простая функция

      @HostListener('document:click', ['$event'])
onClick(event: Event) {

    const elementId = (event.target as Element).id;
    if (elementId.includes("menu")) {
        return;
    }

    this.isMenuVisble = false;

}

Это означает, что щелчок в любом месте за пределами «именованного» контекста закрывает / скрывает «именованный» компонент.

Вы можете использовать событие (щелчок) в документе и проверить, не находится ли цель события в раскрывающемся контейнере. Если это не так, вы можете закрыть раскрывающийся список. Вот пример:

      import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <div #container>
      <button (click)="toggleDropdown()">Toggle Dropdown</button>
      <div *ngIf="isDropdownOpen">
        Dropdown Content
      </div>
    </div>
  `,
  styles: []
})
export class AppComponent implements OnInit {
  @ViewChild('container', { static: true }) container: ElementRef;
  isDropdownOpen = false;

  ngOnInit() {
    document.addEventListener('click', (event) => {
      if (!this.container.nativeElement.contains(event.target)) {
        this.isDropdownOpen = false;
      }
    });
  }

  toggleDropdown() {
    this.isDropdownOpen = !this.isDropdownOpen;
  }
}
Другие вопросы по тегам