我是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'}];
  }
}


项由只读组件上的(单击)处理程序置于编辑模式,并且类似地,通过附加到自定义指令中定义的(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是否包含click目标-如果不包含,则它发出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来演示此问题。在具有控制台功能的情况下单击元素,演示了如何触发(最后一次)与创建编辑组件的同一click事件之间的clickOutside事件-立即将其恢复为只读模式。

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

最佳答案

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

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

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

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


原因是this._elementRef.nativeElement引用了edit组件的DOM元素,而targetElement引用了最初单击的DOM元素(即只读组件),因此条件始终测试为false,从而触发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
    }
  });

 }

}




去做

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

有任何想法吗?

07-26 01:03