【发布时间】:2017-02-27 15:42:29
【问题描述】:
如何在 Angular 中检测外部组件的点击?
【问题讨论】:
标签: html events angular typescript
如何在 Angular 中检测外部组件的点击?
【问题讨论】:
标签: html events angular typescript
获取所有父母
var paths = event['path'] as Array<any>;
检查是否有父组件是组件
var inComponent = false;
paths.forEach(path => {
if (path.tagName != undefined) {
var tagName = path.tagName.toString().toLowerCase();
if (tagName == 'app-component')
inComponent = true;
}
});
如果您将组件作为父组件,则在组件内部单击
if (inComponent) {
console.log('clicked inside');
}else{
console.log('clicked outside');
}
@HostListener('document:click', ['$event'])
clickout(event: PointerEvent) {
var paths = event['path'] as Array<any>;
var inComponent = false;
paths.forEach(path => {
if (path.tagName != undefined) {
var tagName = path.tagName.toString().toLowerCase();
if (tagName == 'app-component')
inComponent = true;
}
});
if (inComponent) {
console.log('clicked inside');
}else{
console.log('clicked outside');
}
}
【讨论】:
MVP的替代方案,只需要关注Event
@HostListener('focusout', ['$event'])
protected onFocusOut(event: FocusEvent): void {
console.log(
'click away from component? :',
event.currentTarget && event.relatedTarget
);
}
【讨论】:
使用 event.stopPropagation()
的另一种可能的解决方案【讨论】:
改进@J。科学怪人的回答
@HostListener('click')
clickInside($event) {
this.text = "clicked inside";
$event.stopPropagation();
}
@HostListener('document:click')
clickOutside() {
this.text = "clicked outside";
}
【讨论】:
stopPropagation 可能会影响组件外部的逻辑:谷歌分析、关闭另一个组件等
你可以调用像 (focusout) 或 (blur) 这样的事件函数,然后你输入你的代码
<div tabindex=0 (blur)="outsideClick()">raw data </div>
outsideClick() {
alert('put your condition here');
}
【讨论】:
ginalx's answer 应该设置为 imo 的默认值:此方法允许进行许多优化。
问题
假设我们有一个项目列表,并且我们希望在每个项目上都包含一个需要切换的菜单。我们在一个按钮上添加了一个切换按钮,该按钮在其自身(click)="toggle()" 上侦听click 事件,但我们也希望在用户点击菜单外部时切换菜单。如果项目列表增加并且我们在每个菜单上附加@HostListener('document:click'),那么项目中加载的每个菜单都将开始监听整个文档的点击,即使菜单被关闭。除了明显的性能问题外,这是不必要的。
例如,您可以在弹出窗口通过点击切换时订阅,然后才开始监听“外部点击”。
isActive: boolean = false;
// to prevent memory leaks and improve efficiency, the menu
// gets loaded only when the toggle gets clicked
private _toggleMenuSubject$: BehaviorSubject<boolean>;
private _toggleMenu$: Observable<boolean>;
private _toggleMenuSub: Subscription;
private _clickSub: Subscription = null;
constructor(
...
private _utilitiesService: UtilitiesService,
private _elementRef: ElementRef,
){
...
this._toggleMenuSubject$ = new BehaviorSubject(false);
this._toggleMenu$ = this._toggleMenuSubject$.asObservable();
}
ngOnInit() {
this._toggleMenuSub = this._toggleMenu$.pipe(
tap(isActive => {
logger.debug('Label Menu is active', isActive)
this.isActive = isActive;
// subscribe to the click event only if the menu is Active
// otherwise unsubscribe and save memory
if(isActive === true){
this._clickSub = this._utilitiesService.documentClickedTarget
.subscribe(target => this._documentClickListener(target));
}else if(isActive === false && this._clickSub !== null){
this._clickSub.unsubscribe();
}
}),
// other observable logic
...
).subscribe();
}
toggle() {
this._toggleMenuSubject$.next(!this.isActive);
}
private _documentClickListener(targetElement: HTMLElement): void {
const clickedInside = this._elementRef.nativeElement.contains(targetElement);
if (!clickedInside) {
this._toggleMenuSubject$.next(false);
}
}
ngOnDestroy(){
this._toggleMenuSub.unsubscribe();
}
并且,在*.component.html:
<button (click)="toggle()">Toggle the menu</button>
【讨论】:
tap 运算符中。相反,请使用skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false))。这样你就可以利用更多的 RxJs 并跳过一些订阅。
通过@Hostlistener 绑定到文档点击成本很高。如果您过度使用(例如,在构建自定义下拉组件并且您在表单中创建了多个实例时),它可以并且将会产生明显的性能影响。
我建议在您的主应用程序组件内仅向文档单击事件添加一次 @Hostlistener()。该事件应将单击的目标元素的值推送到存储在全局实用程序服务中的公共主题内。
@Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>'
})
export class AppComponent {
constructor(private utilitiesService: UtilitiesService) {}
@HostListener('document:click', ['$event'])
documentClick(event: any): void {
this.utilitiesService.documentClickedTarget.next(event.target)
}
}
@Injectable({ providedIn: 'root' })
export class UtilitiesService {
documentClickedTarget: Subject<HTMLElement> = new Subject<HTMLElement>()
}
任何对点击的目标元素感兴趣的人都应该订阅我们实用服务的公共主题,并在组件被销毁时取消订阅。
export class AnotherComponent implements OnInit {
@ViewChild('somePopup', { read: ElementRef, static: false }) somePopup: ElementRef
constructor(private utilitiesService: UtilitiesService) { }
ngOnInit() {
this.utilitiesService.documentClickedTarget
.subscribe(target => this.documentClickListener(target))
}
documentClickListener(target: any): void {
if (this.somePopup.nativeElement.contains(target))
// Clicked inside
else
// Clicked outside
}
【讨论】:
上面提到的答案是正确的,但是如果您在失去相关组件的焦点后正在执行繁重的过程怎么办。为此,我提出了一个带有两个标志的解决方案,其中只有在失去相关组件的焦点时才会发生聚焦事件过程。
isFocusInsideComponent = false;
isComponentClicked = false;
@HostListener('click')
clickInside() {
this.isFocusInsideComponent = true;
this.isComponentClicked = true;
}
@HostListener('document:click')
clickout() {
if (!this.isFocusInsideComponent && this.isComponentClicked) {
// do the heavy process
this.isComponentClicked = false;
}
this.isFocusInsideComponent = false;
}
希望这会对您有所帮助。纠正我如果错过了什么。
【讨论】:
import { Component, ElementRef, HostListener, Input } from '@angular/core';
@Component({
selector: 'selector',
template: `
<div>
{{text}}
</div>
`
})
export class AnotherComponent {
public text: String;
@HostListener('document:click', ['$event'])
clickout(event) {
if(this.eRef.nativeElement.contains(event.target)) {
this.text = "clicked inside";
} else {
this.text = "clicked outside";
}
}
constructor(private eRef: ElementRef) {
this.text = 'no clicks yet';
}
}
【讨论】:
你可以使用https://www.npmjs.com/package/ng-click-outside包中的clickOutside()方法
【讨论】:
AMagyar 答案的替代方案。当您单击使用 ngIf 从 DOM 中删除的元素时,此版本有效。
http://plnkr.co/edit/4mrn4GjM95uvSbQtxrAS?p=preview
private wasInside = false;
@HostListener('click')
clickInside() {
this.text = "clicked inside";
this.wasInside = true;
}
@HostListener('document:click')
clickout() {
if (!this.wasInside) {
this.text = "clicked outside";
}
this.wasInside = false;
}
【讨论】: