Как сфокусировать угловой клик-аут

Это div будет динамически отображаться на странице как восточная панель, когда open panel кнопка нажата. Бул showEastPanel Переменная - это то, что используется для открытия и закрытия восточной панели. Я пытаюсь использовать (clickoutside) закрыть панель (настройка showEastPanel значение false) однако открытая панель запускается сначала на угловом крючке, и для панели устанавливается значение true, затем false, и панель не отображается. Есть ли какой-нибудь способ "clickoutside", чтобы не включать кнопку?

<div [ngClass]="{'d-none': !showEastPanel, 'east-panel-container': showEastPanel}" (clickOutside)="ClosePanel()">
<div id="east-panel">
  <ng-template #eastPanel></ng-template>
</div>

<button (click)="ShowPanel()">Open Panel</button>

11 ответов

Решение

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

При таком подходе нужно вводить Renderer2 в ваш конструктор компонента, который Renderer2 позволяет нам listen чтобы вызвать события элегантно. Это просто берет элемент, который вы собираетесь слушать, в качестве первого аргумента, который может быть window, document, body или любой другой элемент ссылки. Для второго аргумента требуется событие, которое мы собираемся прослушать, которое в данном случае click и третий аргумент на самом деле является функцией обратного вызова, которую мы делаем с помощью функции стрелки.

this.renderer.listen('window', 'click',(e:Event)=>{ // your code here})

Остальное решение простое, вам просто нужно установить логический флаг, который сохраняет статус видимости меню (или панели), и нам нужно назначить false на этот флаг, когда он нажал за пределами меню.

Вот ссылка на демо Stackblitz

HTML

<button #toggleButton (click)="toggleMenu()"> Toggle Menu</button>

<div class="menu" *ngIf="isMenuOpen" #menu>
I'm the menu. Click outside to close me
</div>

app.component.ts

export class AppComponent {
  /**
   * This is the toogle button elemenbt, look at HTML and see its defination
   */
  @ViewChild('toggleButton') toggleButton: ElementRef;
  @ViewChild('menu') menu: ElementRef;

  constructor(private renderer: Renderer2) {
    /**
     * This events get called by all clicks on the page
     */
    this.renderer.listen('window', 'click',(e:Event)=>{
         /**
          * Only run when toggleButton is not clicked
          * If we don't check this, all clicks (even on the toggle button) gets into this
          * section which in the result we might never see the menu open!
          * And the menu itself is checked here, and it's where we check just outside of
          * the menu and button the condition abbove must close the menu
          */
        if(e.target !== this.toggleButton.nativeElement && e.target!==this.menu.nativeElement){
            this.isMenuOpen=false;
        }
    });
  }

  isMenuOpen = false;

  toggleMenu() {
    this.isMenuOpen = !this.isMenuOpen;
  }
}

Вы можете сделать что-то вроде этого

  @HostListener('document:mousedown', ['$event'])
  onGlobalClick(event): void {
     if (!this.elementRef.nativeElement.contains(event.target)) {
        // clicked outside => close dropdown list
     this.isOpen = false;
     }
  }

и используйте *ngIf=isOpen для панели

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

При использовании встроенных элементов, и вы хотите обнаружить щелчок по родительскому элементу, event.target дает ссылку на базовый дочерний элемент.

HTML

<div #toggleButton (click)="toggleMenu()">
    <b>Toggle Menu</b>
    <span class="some-icon"></span>
</div>

<div #menu class="menu" *ngIf="isMenuOpen">
    <h1>I'm the menu.</h1>
    <div>
        I have some complex content containing multiple children.
        <i>Click outside to close me</i>
    </div>
</div>

Я нажимаю на текст "Переключить меню", event.target возвращает ссылку на элемент 'u' вместо #toggleButton div.

Для этого случая я использовал решение M98, включая Renderer2, но поменял условие на то, что было в ответе Суджая.

ToggleButton.nativeElement.contains (e.target) возвращает true, даже если цель события click находится в дочерних элементах nativeElement, что решает проблему.

component.ts

export class AppComponent {
/**
 * This is the toogle button element, look at HTML and see its definition
 */
    @ViewChild('toggleButton') toggleButton: ElementRef;
    @ViewChild('menu') menu: ElementRef;
    isMenuOpen = false;

    constructor(private renderer: Renderer2) {
    /**
     * This events get called by all clicks on the page
     */
        this.renderer.listen('window', 'click',(e:Event)=>{
            /**
             * Only run when toggleButton is not clicked
             * If we don't check this, all clicks (even on the toggle button) gets into this
             * section which in the result we might never see the menu open!
             * And the menu itself is checked here, and it's where we check just outside of
             * the menu and button the condition abbove must close the menu
             */
            if(!this.toggleButton.nativeElement.contains(e.target) && !this.menu.nativeElement.contains(e.target)) {
                this.isMenuOpen=false;
            }
        });
    }

    toggleMenu() {
        this.isMenuOpen = !this.isMenuOpen;
    }
}

Мне нравится ответ Суджая. Если вы хотите вместо этого создать директиву (для использования в нескольких компонентах). Вот как бы я это сделал.

import {
  Directive,
  EventEmitter,
  HostListener,
  Output,
  ElementRef,
} from '@angular/core';

@Directive({
  selector: '[outsideClick]',
})
export class OutsideClickDirective {
  @Output()
  outsideClick: EventEmitter<MouseEvent> = new EventEmitter();

  @HostListener('document:mousedown', ['$event'])
  onClick(event: MouseEvent): void {
    if (!this.elementRef.nativeElement.contains(event.target)) {
      this.outsideClick.emit(event);
    }
  }

  constructor(private elementRef: ElementRef) {}
}

Затем вы могли бы использовать эту директиву так:

<div class="menu" *ngIf="isMenuOpen" (outsideClick)="isMenuOpen = false" outsideClick #menu>
  I'm the menu. Click outside to close me
</div>

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

import { Directive, ElementRef, Optional, Inject, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { fromEvent, Subscription } from 'rxjs';
import { DOCUMENT } from '@angular/common';
import { filter } from 'rxjs/operators';

@Directive({
  selector: '[outsideClick]',
})
export class OutsideClickDirective implements OnInit, OnDestroy {
  @Output('outsideClick') outsideClick = new EventEmitter<MouseEvent>();

  private subscription: Subscription;

  constructor(private element: ElementRef, @Optional() @Inject(DOCUMENT) private document: any) {}

  ngOnInit() {
    setTimeout(() => {
      this.subscription = fromEvent<MouseEvent>(this.document, 'click')
        .pipe(
          filter(event => {
            const clickTarget = event.target as HTMLElement;
            return !this.isOrContainsClickTarget(this.element.nativeElement, clickTarget);
          }),
        )
        .subscribe(event => this.outsideClick.emit());
    }, 0);
  }

  private isOrContainsClickTarget(element: HTMLElement, clickTarget: HTMLElement) {
    return element == clickTarget || element.contains(clickTarget);
  }

  ngOnDestroy() {
    if (this.subscription) this.subscription.unsubscribe();
  }
}

Кредиты на https://github.com/ngez/platform, я получил большую часть логики из этого.

Чего мне не хватало, так это setTimeout(..., 0), который гарантирует, что проверка будет запланирована после того, как компонент, использующий директиву, будет отрисован.

Полезные ссылки:

More Simplified Code with demo on: StackBlitz

I have made a common function to close the menu on outside click and Prevent the closeing if click triggered on specific elements.

HTML

      <button (click)="toggleMenu(); preventCloseOnClick()">Toggle Menu</button>
<ul (click)="preventCloseOnClick()" *ngIf="menuOpen">
  <li>Menu 1</li>
  <li>Menu 2</li>
  <li>Menu 3</li>
  <li>Menu 4</li>
  <li>Menu 5</li>
</ul>

TS

      import { Component, VERSION, Renderer2 } from '@angular/core';

export class AppComponent {
  menuOpen: boolean = false;
  menuBtnClick: boolean = false;

  constructor(private renderer: Renderer2) {
    this.renderer.listen('window', 'click', (e: Event) => {
      if (!this.menuBtnClick) {
        this.menuOpen = false;
      }
      this.menuBtnClick = false;
    });
  }
  toggleMenu() {
    this.menuOpen = !this.menuOpen;
  }
  preventCloseOnClick() {
    this.menuBtnClick = true;
  }
}

спасибо Эмерика ng-click-outside работает отлично, это то, что мне было нужно, я тестировал свой модальный, но когда я нажимаю его, первый щелчок по кнопке, он убирает внешний щелчок, а затем не работал, чтобы установить модальный, но я добавил delayClickOutsideInit="true" из документов, и он работает очень хорошо, вот конечный результат:

      <button
  (click)="imageModal()"
>
<button/>

<div
  *ngIf="isMenuOpen"
>
  <div
    (clickOutside)="onClickedOutside($event)"
    delayClickOutsideInit="true"
  >
   Modal content
  </div>
</div>

а это моя составляющая

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

@Component({
  selector: 'app-modal-header',
  templateUrl: './modal-header.component.html',
  styleUrls: ['./modal-header.component.css'],
})
export class ModalHeaderComponent implements OnInit {
  public isMenuOpen = false;

  constructor() {}

  imageModal() {
    this.isMenuOpen = !this.isMenuOpen;
  }
  closeModal() {
//you can do an only close function click
    this.isMenuOpen = false;
  }
  onClickedOutside(e: Event) {
    this.isMenuOpen = false;
  }
}

Я сделал иначе, в отличие от предыдущих ответов.

я кладу mouseleave, mouseenter событие в раскрывающемся меню

<div
    class="dropdown-filter"
    (mouseleave)="onMouseOutFilter($event)"
    (mouseenter)="onMouseEnterFilter($event)"
  >
    <ng-container *ngIf="dropdownVisible">
      <input
        type="text"
        placeholder="search.."
        class="form-control"
        [(ngModel)]="keyword"
        id="myInput"
        (keyup)="onKeyUp($event)"
      />
    </ng-container>
    <ul
      class="dropdown-content"
      *ngIf="dropdownVisible"
    >
      <ng-container *ngFor="let item of filteredItems; let i = index">
        <li
          (click)="onClickItem($event, item)"
          [ngStyle]="listWidth && {width: listWidth + 'px'}"
        >
          <span>{{ item.label }}</span>
        </li>
      </ng-container>
    </ul>
  </div>
  constructor(private renderer: Renderer2) {
    /**
     * this.renderer instance would be shared with the other multiple same components
     * so you should have one more flag to divide the components
     * the only dropdown with mouseInFilter which is false should be close
     */
    this.renderer.listen('document', 'click', (e: Event) => {
      if (!this.mouseInFilter) {
        // this is the time to hide dropdownVisible
        this.dropdownVisible = false;
      }
    });
  }

  onMouseOutFilter(e) {
    this.mouseInFilter = false;
  }

  onMouseEnterFilter(e) {
    this.mouseInFilter = true;
  }

и убедитесь, что значение defaultValue для mouseInFilter равно false;

  ngOnInit() {
    this.mouseInFilter = false;
    this.dropdownVisible = false;
  }

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

  toggleDropDownVisible() {
    if (!this.dropdownVisible) {
      this.mouseInFilter = true;
    }
    this.dropdownVisible = !this.dropdownVisible;
  }

Вы можете использовать https://github.com/arkon/ng-click-outside, который довольно прост в использовании и имеет множество полезных функций:

      @Component({
  selector: 'app',
  template: `
    <div (clickOutside)="onClickedOutside($event)">Click outside this</div>
  `
})
export class AppComponent {
  onClickedOutside(e: Event) {
    console.log('Clicked outside:', e);
  }
}

Что касается производительности , библиотека использует ngOnDestroy чтобы удалить слушателя, когда директива неактивна (используйте clickOutsideEnabledproperty), что действительно важно, и большинство предлагаемых решений этого не делают. Смотрите исходный код здесь.

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

В HTML

 <div #menuIcon (click)="onMenuClick()">
  <a><i class="fa fa-reorder"></i></a>
 </div>
<div #menuPopup  *ngIf="showContainer">
   <!-- Something in the popup like menu -->
</div>

В ТС

  @ViewChild('menuIcon', { read: ElementRef, static: false })  menuIcon: ElementRef;
  @ViewChild('menuPopup', { read: ElementRef, static: false })  menuPopup: ElementRef;
   showContainer = false;

      constructor(private renderer2: Renderer2) {
      this.renderer2.listen('window', 'click', (e: Event) => {
        if (
         (this.menuPopup && this.menuPopup.nativeElement.contains(e.target)) ||
          (this.menuIcon && this.menuIcon.nativeElement.contains(e.target))
         ) {
              // Clicked inside plus preventing click on icon
             this.showContainer = true;
           } else {
             // Clicked outside
             this.showContainer = false;
         }
      });
    }

     onMenuClick() {
        this.isShowMegaMenu = true;
      }

если click() и clickOutside() запускаются одновременно, вы должны обратиться

https://github.com/arkon/ng-click-outside/issues/31

это решило мою проблему

Другие вопросы по тегам