我是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示例