在 Angular 中将指令事件从子组件传递到父组件

问题描述 投票:0回答:1

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

我有一个自定义指令,可以检测是否在元素外部进行了单击:

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() {
    this.isMenuOpen = !this.isMenuOpen;
  }

这个实现有什么问题?

angular angular-directive directive
1个回答
1
投票

更新:

用户共享最小可重现的 stackblitz 后,我在按钮上添加了类

ignore-click
,然后在指令上,以下条件忽略了按钮单击!

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

完整代码

main.ts

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.directive';

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

工作 Stackblitz


发生这种情况是因为按钮单击也将被视为外部单击!!

请将按钮引用也传递给指令,并忽略来自按钮的点击!

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

然后指令可以更改为

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>

该指令可以是

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);
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.