I'm fairly new to Angular 2 and am building an app which generates multiple instances of the same child component within the parent host component. Clicking on one of these components puts it into edit mode (display form inputs) and clicking outside of the active edit component should replace the edit component with the default read-only version.
@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'}];
}
}
An item is placed into edit mode by a (click) handler on the read-only component and similarly switched out of edit mode by a handler attached to a (clickOutside) event defined in a custom directive.
Read-only component
@Component({
selector: 'read-only',
template: `
<div
(click)="setEditable()"
>
{{lineData.name}}
</div>
`,
inputs['lineData']
})
export class ReadOnly {
lineData:any;
constructor(){
}
setEditable(){
this.lineData.edit=true;
}
}
Edit component
@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;
}
}
The problem is that the click event that switches to edit mode is also picked up by the clickOutside handler. Internally the clickOutside handler is triggered by a click event and tests whether the nativeElement of the edit component contains the click target - if not it emits a clickOutside event.
ClickOutside Directive
@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);
}
}
}
I've tried dynamically binding the (clickOutside) event to the edit component within ngAfterContentInit() and ngAfterContentChecked() lifecycle hooks and using Renderer listen() as detailed here but without success. I noticed that native events could be bound this way but I couldn't bind to an output of a custom directive.
I've attached a Plunk here demonstrating the issue. Clicking on the elements with console up demonstrates how the clickOutside event is fired (last) off the same click event that created the edit component - immediately reverting the component to read-only mode.
What's the cleanest way to handle this situation? Ideally I could dynamically bind the clickOutside event but haven't found a successful way to do so.