【问题标题】:Angular Click Outside Directive issue with an extra click event带有额外点击事件的 Angular Click Outside Directive 问题
【发布时间】:2018-09-18 04:10:44
【问题描述】:

我已经实现了一个自定义的 Click Outside 指令,该指令用于关闭模式对话框、通知、弹出窗口等(为简单起见,我们称它们为“弹出窗口”)。但是,我正在努力为通过鼠标单击(即按钮单击)打开的弹出窗口和通过其他操作(即鼠标悬停)打开的弹出窗口实现通用解决方案。

这是我面临的问题的Plunker example:鼠标悬停事件产生的弹出窗口需要单击两次才能关闭,而“单击我”按钮产生的相同弹出窗口只需要单击即可关闭.

这是指令的代码:

@Directive({
  selector: '[clickOutside]'
})
export class ClickOutsideDirective {
  @Output('clickOutside') clickOutsideEvent = new EventEmitter();

  private globalClick: Subscription;

  constructor(private elRef: ElementRef) { }

  ngOnInit() {
    this.globalClick = Observable
      .fromEvent(document, 'click')
      .skip(1)
      .subscribe((event: MouseEvent) => {
        this.clicked(event);
      });
  }

  ngOnDestroy() {
    this.globalClick.unsubscribe();
  }

  private clicked(event: MouseEvent) {
    let clickedInside = this.elRef.nativeElement.contains(event.target);

    if (!clickedInside) {
      this.clickOutsideEvent.emit();
    }
  }
}

在 ngOnInit() 内部,我初始化了一个 observable,它侦听文档上的点击并将事件传递给 clicked() 函数,该函数将检查点击是否在外部进行,如果是则引发事件。 observable 使用跳过运算符来过滤第一次点击事件。这是一种解决方法,因为如果您单击“单击我”按钮,则该原始单击事件是我要跳过的事件,因为它在指令中对我没有用。如果我不跳过它,弹出窗口会自行关闭,因为它会认为外面有点击。但是,此解决方法有一个副作用,即现在需要单击两次才能关闭以其他方式触发的弹出窗口,而不是通过单击(Plunker 中的鼠标悬停示例演示了此问题)。

我正在努力想出一个优雅而通用的解决方案来解决这个问题。我可以想到多种“hacky”解决方案:

黑客解决方案 1: 我可以在“单击我”按钮处停止事件传播,这意味着 ClickOutsideDirective 将不会接收初始单击事件,并且我可以从可观察到的文档中删除 .skip 运算符。理论上应该可行(没有尝试过),但我不喜欢的是指令的使用者现在需要知道他们需要停止事件传播,如果他们不这样做,指令将无法按设计工作.

黑客解决方案 2: 将布尔标志传递给可用于确定是否需要跳过第一次点击的指令。很容易做到,但是 API 的用户现在需要知道他们需要设置这个标志。如果他们忘记了,该指令将无法按预期工作。

黑客解决方案 3: 跳过指令初始化后短时间内(比如说 100 毫秒)发生的第一个事件。这也很容易做到,这个解决方案的优点是 API 的用户不需要向指令提供任何额外的信息(或停止任何事件传播)并且指令将“正常工作”。这里的问题是要找出在所有浏览器和硬件上都可以使用的延迟。延迟必须不小于指令实例化后事件到达所需的时间,但如果用户决定在弹出窗口显示后立即点击离开,则延迟不能大到跳过合法用户点击。

谁能想出一个更好、更简单、更优雅的解决方案来解决这个问题?

【问题讨论】:

    标签: javascript angular typescript


    【解决方案1】:

    与其将逻辑保存在单独的指令中,不如将其保存在 popup 组件中,因为它负责关闭。

    更改组件,以便有一个带有点击侦听器的固定位置 div,位于弹出窗口的后面。然后,无论何时单击它,都发送clickOutside 事件。

    import {Component, Output, EventEmitter} from '@angular/core';
    
    @Component({
      selector: 'popup',
      styles: [`
          .background {
        z-index: 1;
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        backgrouc
          }
    
          .modal-content {
        left: 0;
        right: 0;
        z-index: 3;
          }
      `],
      template: `
        <div class="background" (click)="close()">
          <div class="modal-content">
        Click outside to close me!
          </div>
        </div>
      `,
    })
    export class PopupComponent {
    
      @Output()
      clickOutside = new EventEmitter();
    
      close() {
        this.clickOutside.emit();
      }
    }
    

    这是一个有效的 plunkr:https://plnkr.co/edit/9QBJ0PXOg2GAUpBacgQV?p=preview

    【讨论】:

    • 谢谢,这在这个例子中可以工作,但我正在寻找一个通用的解决方案,这样我就不需要修改所有现有的组件。您的回答给我的一个想法是动态添加一个额外的“背景”div,它将包装指令附加到的组件,然后专门在该“背景”div 上监听点击事件,而不是像我现在所做的那样监听全局点击。
    【解决方案2】:

    所以我终于找到了答案。该解决方案并不像我希望的那样干净,但基本上我添加了一个可观察的计时器(具有零值),它将触发 .fromEvent(document, 'click') 可观察的。我已经在所有现代浏览器(包括 iPad 和 Android Chrome)中对此进行了测试,它似乎在任何地方都可以正常工作。

    ngOnInit() {
       this.globalClick = Observable
          .timer(0)
          .switchMap(() => {
              return Observable.fromEvent(this.document, 'click');
          })
          .subscribe((event: MouseEvent) => {
            this.clicked(event);
          });
    }
    

    【讨论】:

      猜你喜欢
      • 2014-07-02
      • 2016-09-27
      • 1970-01-01
      • 2020-03-21
      • 1970-01-01
      • 2020-08-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多