在Angular 2中处理click和clickOutside事件执行优先级的最佳方法是什么?

时间:2016-11-17 02:02:54

标签: javascript angularjs events angular

我是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)事件的处理程序类似地切换到编辑模式。

只读组件

@Component({
  selector: 'read-only',
  template: `
    <div 
      (click)="setEditable()"
    >
     {{lineData.name}}
    </div>
  `,
   inputs['lineData']
})

export class ReadOnly {

 lineData:any;
 constructor(){

 }  

 setEditable(){
  this.lineData.edit=true;
 }
}

修改组件

@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处理程序还会选择切换到编辑模式的click事件。在内部,clickOutside处理程序由click事件触发,并测试编辑组件的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()as detailed here,但没有成功。我注意到原生事件可能以这种方式绑定,但我无法绑定到自定义指令的输出。

我在这里附上Plunk来证明这个问题。使用控制台向上单击元素演示了clickOutside事件如何从创建编辑组件的相同单击事件中触发(最后) - 立即将组件恢复为只读模式。

处理这种情况的最简洁方法是什么?理想情况下,我可以动态绑定clickOutside事件但尚未找到成功的方法。

2 个答案:

答案 0 :(得分:0)

部分解决方案(取决于DOM)

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

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

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

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

与这种情况下使用的简单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 真正通用且模块化。

有什么想法吗?

答案 1 :(得分:0)

更好的解决方案 - 独立于DOM

该问题的进一步改进解决方案。与以前的解决方案相比的重大改进:

  • 不进行DOM元素查询
  • 清除初始点击事件的处理(如果主机组件是通过点击实例化的)
  • 清理动态事件处理程序 移至ngOnDestroy()
  • 将click事件对象与(clickOutside)事件一起传递,以便侦听器更好地对(clickOutside)事件进行条件处理。

修改动态地向指令添加一个额外的(单击)事件处理程序,该指令捕获主机组件的事件对象(而不是全局文档范围)。这与listenGlobal()方法生成的事件对象进行比较,如果两者都是等效的,则假设在内部进行单击。如果没有,则发出(clickOutside)事件并传递click事件对象。

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

  • 在listenGlobal()处理程序
  • 中设置“ready”标志
  • 动态注册 我们用来捕获我们用于的本地事件对象的click处理程序 测试clickInside时的比较。

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)"

更新了工作插件示例here