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

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

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

17 ответов


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

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


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

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

ЭЛЕГАНТНЫЙ МЕТОД: я нашел это


Я думаю, что Sasxa принятый ответ работает для большинства людей. Однако у меня была ситуация, когда содержимое элемента, которое должно прослушивать события off-click, динамически менялось. Поэтому nativeElement элементы не содержат событие.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 событие.цель, я проверяю, находится ли elementRef в пути (путь DOM к цели) события. Это можно обрабатывать динамически создаваемые элементы.


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

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

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

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

Примечание: это не может быть сделано с типичным обработчиком "щелчка", потому что это противоречит родительскому обработчику щелчка.

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

Если вы делаете это на iOS, используйте touchstart мероприятие:

по состоянию на угловой 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();
    }
  }

}

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

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

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

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


Я хотел бы дополнить ответ @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);
    
  • в элементе clickable используйте:

    (click)="dropstatus=true"
    

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


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

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


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


  }

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


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


    @Directive({
      selector: '[appDropDown]'
    })
    export class DropdownDirective implements OnInit {

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

      constructor(private 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;
      }
    }

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


лучшая версия для @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: / / не требуется, если вы используете раскрывающийся список bootstrap.

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

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

@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] input свойство "true". Он прослушивает событие mousedown для обработки события, прежде чем элемент будет удален из DOM.

и одно замечание: firefox не содержит свойства "path" в событии, которое вы можете использовать для создания 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;
};

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

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


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

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

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

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

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

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

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

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

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

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

что я сделал:

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

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