在Angular 2中处理click和clickOutside事件执行优先级的最佳方式

Best way to handle click and clickOutside event execution precedence in Angular 2?

本文关键字:执行 事件 优先级 方式 最佳 clickOutside Angular 处理 click      更新时间:2023-09-26

我是Angular 2的新手,正在构建一个应用程序,它在父宿主组件中生成同一子组件的多个实例。单击其中一个组件将使其进入编辑模式(显示表单输入),并且在活动编辑组件外单击应将编辑组件替换为默认的只读版本。

@Component({
  selector: 'my-app',
  template: `
    <div *ngFor="let line of lines">
    <ng-container *ngIf="!line.edit">
      <read-only 
        [lineData]="line"
      ></read-only>
    </ng-container>
    <ng-container *ngIf="line.edit">
      <edit 
        [lineData]="line"
      ></edit>
    </ng-container>    
    </div>
  `,
})
export class App {
  name:string;
  lines:any[];
  constructor() {
    this.name = 'Angular2';
    this.lines = [{name:'apple'},{name:'pear'},{name'banana'}];
  }
}

通过只读组件上的(click)处理程序将项置于编辑模式,类似地,通过自定义指令中定义的(clickOutside)事件附加的处理程序将项切换出编辑模式。

<<p> 只读组件/strong>
@Component({
  selector: 'read-only',
  template: `
    <div 
      (click)="setEditable()"
    >
     {{lineData.name}}
    </div>
  `,
   inputs['lineData']
})
export class ReadOnly {
 lineData:any;
 constructor(){
 }  
 setEditable(){
  this.lineData.edit=true;
 }
}
<<p> 编辑组件/strong>
@Component({
  selector: 'edit',
  template: `
    <div style="
      background-color:#cccc00;
      border-width:medium;
      border-color:#6677ff;
      border-style:solid"
      (clickOutside)="releaseEdit()"
      >
    {{lineData.name}}
  `,
  inputs['lineData']
})
export class Edit {
 lineData:any;
 constructor(){
 } 
 releaseEdit(){
   console.log('Releasing edit mode for '+this.lineData.name);
   delete this.lineData.edit;
 }
}

问题是切换到编辑模式的单击事件也被clickOutside处理程序拾取。在内部,clickOutside处理程序由单击事件触发,并测试编辑组件的nativeElement是否包含单击目标—如果不包含,则会发出clickOutside事件。

ClickOutside指令

@Directive({
    selector: '[clickOutside]'
})
export class ClickOutsideDirective {
    ready: boolean;

    constructor(private _elementRef: ElementRef, private renderer: Renderer) {
    }
    @Output()
    public clickOutside = new EventEmitter<MouseEvent>();
    @HostListener('document:click', ['$event', '$event.target'])

    public onClick(event: MouseEvent, targetElement: HTMLElement): void {
        if (!targetElement) {
            return;
        }
        const clickedInside = this._elementRef.nativeElement.contains(targetElement);
        if (!clickedInside) {
            this.clickOutside.emit(event);
        }
    }
}

我已经尝试动态绑定(clickOutside)事件到编辑组件内的ngAfterContentInit()和ngAfterContentChecked()生命周期钩子和使用Renderer listen()详细在这里,但没有成功。我注意到本地事件可以用这种方式绑定,但是我不能绑定到自定义指令的输出。

我在这里附加了一个Plunk来演示这个问题。在打开控制台的情况下单击元素,演示了clickOutside事件是如何在创建编辑组件的相同单击事件中触发的(最后一次)——立即将组件恢复到只读模式。 处理这种情况最干净的方法是什么?理想情况下,我可以动态绑定clickOutside事件,但还没有找到一个成功的方法。

部分解(DOM依赖)

原谅回复自己的帖子,但希望这将对在类似方式中挣扎的人有用。

上面代码的主要问题是,在指令定义(clickOutside)中设置的clickedInside总是测试为false。

const clickedInside = this._elementRef.nativeElement.contains(targetElement);

原因是this._elementRef.nativeElement引用了编辑组件的DOM元素,而targetElement引用了最初被点击的DOM元素(即只读组件),所以条件总是测试为假,触发clickOutside事件。

对于这种情况下使用的简单DOM元素,一个解决方法是在每个属性内的修剪过的HTML上测试字符串是否相等。

const name = this.elementRef.nativeElement.children[0].innerHTML.trim();
const clickedInside = name == $event.target.innerHTML.trim();

第二个问题是,事件处理程序的定义将持续存在,并导致问题,所以我们将改变点击处理程序中的ClickOutsideDirective指令,使其在构造函数中动态创建。

export class ClickOutsideDirective {
@Output() public clickOutside = new EventEmitter<MouseEvent>()
globalListenFunc:Function; 
 constructor(private elementRef:ElementRef, private renderer:Renderer){
  // assign ref to event handler destroyer method returned
  // by listenGlobal() method here 
  this.globalListenFunc = renderer.listenGlobal('document','click',($event)=>{
    const name = this.elementRef.nativeElement.innerHTML.trim();
    const clickedInside = name == $event.target.innerHTML.trim();
    if(!clickedInside){
        this.clickOutside.emit(event);
        this.globalListenFunction(); //destroy event handler
    }
  });
 } 
}

TODO

这是可行的,但是它很脆弱并且依赖于DOM。最好找到一个独立于DOM的解决方案——也许使用@View..或@Content输入。理想情况下,它还将提供一种定义clickOutside测试条件的方法,以便ClickOutsideDirective真正通用和模块化。

任何想法?

更好的解决方案- DOM独立

对问题的进一步改进的解决方案。相对于以前的解决方案的主要改进:

  • 不做DOM元素查询
  • 初始单击事件的更清洁处理(如果主机组件从单击实例化)
  • 清理动态事件处理程序移动到ngOnDestroy()
  • 传递带有(clickOutside)事件的click事件对象,以便其侦听器更好地有条件地处理(clickOutside)事件。

这个修改动态地为指令添加了一个额外的(click)事件处理程序,它为宿主组件捕获一个事件对象(而不是全局文档作用域)。这将与listenGlobal()方法生成的事件对象进行比较,如果两者相等,则假设内部发生了单击。如果没有,则触发(clickOutside)事件并传递click事件对象。

为了防止指令在第一次调用时接收和保存任何在指令初始化时(例如,如果使用该指令的主机是通过鼠标点击来初始化的)范围内的点击事件对象,我们:

  • 在listenGlobal()处理程序中设置一个"ready"标志
  • 动态注册用于捕获用于的本地事件对象的click处理程序

clickOutside()指令的完整更新代码

@Directive({
    selector: '[clickOutside]'
})
export class ClickOutsideDirective {
@Output() public clickOutside = new EventEmitter<MouseEvent>()
localevent:any;
lineData:any;
globalListenFunc:Function;
localListenFunc:Function;
ready:boolean;
 constructor(private elementRef:ElementRef, private renderer:Renderer){
      this._initClickOutside();
 }  

_initHandlers(){
  this.localListenFunc = this.renderer.listen(this.elementRef.nativeElement,'click',($event)=>{ 
    this.localevent = $event;
  });
}
_initClickOutside(){
  this.globalListenFunc = this.renderer.listenGlobal('document','click',($event)=>{
    if(!this.ready){
      this.ready = true;
      return;
    }
  if(!this.localListenFunc) this._initHandlers();
    const clickedInside = ($event == this.localevent);
    if(!clickedInside){
        this.clickOutside.emit($event);
        delete this.localevent;
    }
  });
}
ngOnDestroy() {
    // remove event handlers to prevent memory leaks etc.
    this.globalListenFunc();
    this.localListenFunc();
}
}

要使用,请将该指令与处理器和事件参数附加到宿主组件的模板中,如下所示:

(clickOutside) = "doSomething($event)"

此处更新了Plunk示例