我是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是真正的通用模块。
有任何想法吗?