我有一个父组件(侧边栏)和一个子组件(菜单)
我有一个自定义指令,可以检测是否在元素外部进行了单击:
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;
}
这个实现有什么问题?
用户共享最小可重现的 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);
}
}
发生这种情况是因为按钮单击也将被视为外部单击!!
请将按钮引用也传递给指令,并忽略来自按钮的点击!
<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);
}
}