我有一个父组件(侧边栏)和一个子组件(菜单)

我有一个定制指令,它检测是否在元素外部进行了单击:

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

@Directive({
  selector: '[appClickedOutside]',
  standalone: true,
})
export class ClickedOutsideDirective {
  constructor(private el: ElementRef) {}

  @Output() public clickedOutside = new EventEmitter();

  @HostListener('document:click', ['$event'])
  public onClick(event: any) {
    if (!this.el.nativeElement.contains(event.target)) {
      this.clickedOutside.emit(true);
    }
  }

  @HostListener('document:keydown.escape', ['$event'])
  onEscapeKeydownHandler(event: KeyboardEvent) {
    this.clickedOutside.emit(true);
  }
}

在菜单组件中,我正在发出一个事件:

 @Output() menuClosed = new EventEmitter<boolean>();

在菜单模板中,我将指令应用于菜单的div,并在外部单击时发出true.

<div
  appClickedOutside
  (clickedOutside)="menuClosed.emit(true)"
></div>

在侧边栏组件(父)中,我正在接收此消息并关闭菜单:

onMenuClosed(isClosed: boolean) {
    if (isClosed && this.isMenuOpen) {
      this.isMenuOpen = false;
    }
  }

然而,当我单击打开菜单的父级中的按钮时,它不再起作用:

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

toggleMenu是:

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

这个实现出了什么问题?

推荐答案

最新情况:

在用户分享了最小可重现堆叠闪电战后,我在按钮上添加了类ignore-click,然后在指令中忽略了按钮点击!

    ...
    if (
     !(
        this.el.nativeElement?.contains(event.target) ||
        event.target?.classList?.contains('ignore-click')
      )
    ) {
      this.clickedOutside.emit(true);
    }
    ...

完整代码

双手

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { MenuComponent } from './app/menu/menu.component';
import { ClickOutsideDirective } from './app/click-outside.指令';

@Component({
  selector: 'app-root',
  imports: [MenuComponent, ClickOutsideDirective],
  standalone: true,
  template: `
  <div class="container">
    <div class="menu-container">
      <button (click)="toggleMenu()" class="ignore-click">Toggle menu</button>
      @if(isMenuOpen) {
      <app-menu appClickOutside (clickedOutside)="isMenuOpen=false;"/>
    }
    </div>
</div>
  `,
  styles: `
    .menu-container {
      position: relative
    }

    .container {
      display: flex;
      justify-content: center;
      align-items: center;
      margin-top: 100px;
    }
  `,
})
export class App {
  isMenuOpen = false;

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

bootstrapApplication(App);

指令

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

@Directive({
  selector: '[appClickOutside]',
  standalone: true,
})
export class ClickOutsideDirective {
  constructor(private el: ElementRef) {}

  @Output() public clickedOutside = new EventEmitter();

  @HostListener('document:click', ['$event'])
  public onClick(event: any) {
    if (
      !(
        this.el.nativeElement?.contains(event.target) ||
        event.target?.classList?.contains('ignore-click')
      )
    ) {
      this.clickedOutside.emit(true);
    }
  }

  @HostListener('document:keydown.escape', ['$event'])
  onEscapeKeydownHandler(event: KeyboardEvent) {
    this.clickedOutside.emit(true);
  }
}

Working Stackblitz


它的发生,因为按钮点击也将被视为外部点击!!

Please pass in the button ref also to the 指令 and ignore the click if it originates from the button!

<button #buttonRef> this opens the menu!</button>
<div
  appClickedOutside
  [buttonRef]="buttonRef"
  (clickedOutside)="menuClosed.emit(true)"
></div>

Then the 指令 can be changed to

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

@Directive({
  selector: '[appClickedOutside]',
  standalone: true,
})
export class ClickedOutsideDirective {
  @Input() buttonRef: ElementRef<any>;
  constructor(private el: ElementRef) {}

  @Output() public clickedOutside = new EventEmitter();

  @HostListener('document:click', ['$event'])
  public onClick(event: any) {
    if (!(this.el.nativeElement.contains(event.target) &&
          this.el.nativeElement === this.buttonRef.nativeElement)) { // <-changed here
      this.clickedOutside.emit(true);
    }
  }

  @HostListener('document:keydown.escape', ['$event'])
  onEscapeKeydownHandler(event: KeyboardEvent) {
    this.clickedOutside.emit(true);
  }
}

因为没有stackblitz,所以很难调试这个问题.


甚至不需要传递按钮.你可以定义一个类,比如do-not-notice-this,然后判断事件目标是否没有这个类!

<button class="do-not-notice-this"> this opens the menu!</button>
<div
  appClickedOutside
  (clickedOutside)="menuClosed.emit(true)"
></div>

the 指令 can be

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

@Directive({
  selector: '[appClickedOutside]',
  standalone: true,
})
export class ClickedOutsideDirective {
  constructor(private el: ElementRef) {}

  @Output() public clickedOutside = new EventEmitter();

  @HostListener('document:click', ['$event'])
  public onClick(event: any) {
    if (!(this.el.nativeElement.contains(event.target) &&
          this.el.nativeElement.classList.contains('do-not-notice-this')) { // <-changed here
      this.clickedOutside.emit(true);
    }
  }

  @HostListener('document:keydown.escape', ['$event'])
  onEscapeKeydownHandler(event: KeyboardEvent) {
    this.clickedOutside.emit(true);
  }
}

Angular相关问答推荐

NG构建后多余的国旗

元素上的Angular 管道链接不起作用

Angular-每2个流式传输多个HTTP调用

Angular *ngIf 表达式中这么多冒号的作用是什么?

如何检测 Angular Universal 预渲染版本?

使用Dragula时,ReactiveForm元素在拖动副本中显示不同的