I'm implementing an Angular 2 attribute directive to allow me to add a custom context menu to an element like this:
<p context-menu="myItems">Hello world</p>
That directive adds a mouse event handler to catch a right-click, and the idea is to then construct a context menu, add it to the DOM, and then destroy it when the user finishes with it.
I have a component that implements the context menu itself. I'd like to construct that component, call a method on it to set the item list, and then add it to the DOM.
It looks like I might be able to do this with AppViewManager.createHostViewInContainer. Is this an appropriate way to do this? And if so, is there a way to construct/get an ElementRef to document.body
so that I can tell createHostViewInContainer to construct the component there? Obviously I don't want my menu to be clipped inside the element I'm adding the context menu to.
Here is what I think is a good way to do it.
You need 1 service, 1 component and 1 directive.
Here is a plunker
Explanation:
The service
ContextMenuService
:
{event:MouseEvent,obj:any[]}
to be
subscribed to by ContextMenuHolderComponent
, and to receive values
from ContextMenuDirective
Code:
import {Injectable} from 'angular2/core';
import {Subject} from 'rxjs/Rx';
@Injectable()
export class ContextMenuService{
public show:Subject<{event:MouseEvent,obj:any[]}> = new Subject<{event:MouseEvent,obj:any[]}>();
}
And add it to the list of providers in bootstrap()
bootstrap(AppComponent,[ContextMenuService]);
The Component
ContextMenuHolderComponent
:
AppComponent
and it has a fixed
position.It subscribes to the subject
in ContextMenuService
to receive:
{title:string,subject:Subject}[]
, the subject is used to send the clicked on value inside the menuIt has a (document:click)
event listener to close the menu on clicks outside the menu.
code:
@Component({
selector:'context-menu-holder',
styles:[
'.container{width:150px;background-color:#eee}',
'.link{}','.link:hover{background-color:#abc}',
'ul{margin:0px;padding:0px;list-style-type: none}'
],
host:{
'(document:click)':'clickedOutside()'
},
template:
`<div [ngStyle]="locationCss" class="container">
<ul>
<li (click)="link.subject.next(link.title)" class="link" *ngFor="#link of links">
{{link.title}}
</li>
</ul>
</div>
`
})
class ContextMenuHolderComponent{
links = [];
isShown = false;
private mouseLocation :{left:number,top:number} = {left:0;top:0};
constructor(private _contextMenuService:ContextMenuService){
_contextMenuService.show.subscribe(e => this.showMenu(e.event,e.obj));
}
// the css for the container div
get locationCss(){
return {
'position':'fixed',
'display':this.isShown ? 'block':'none',
left:this.mouseLocation.left + 'px',
top:this.mouseLocation.top + 'px',
};
}
clickedOutside(){
this.isShown= false; // hide the menu
}
// show the menu and set the location of the mouse
showMenu(event,links){
this.isShown = true;
this.links = links;
this.mouseLocation = {
left:event.clientX,
top:event.clientY
}
}
}
And add it to the root component:
@Component({
selector: 'my-app',
directives:[ContextMenuHolderComponent,ChildComponent],
template: `
<context-menu-holder></context-menu-holder>
<div>Whatever contents</div>
<child-component></child-component>
`
})
export class AppComponent { }
The last one,
ContextMenuDirective
:
contextmenu
event to the host element.ContextMenuHolderComponent
.Code:
@Directive({
selector:'[context-menu]',
host:{'(contextmenu)':'rightClicked($event)'}
})
class ContextMenuDirective{
@Input('context-menu') links;
constructor(private _contextMenuService:ContextMenuService){
}
rightClicked(event:MouseEvent){
this._contextMenuService.show.next({event:event,obj:this.links});
event.preventDefault(); // to prevent the browser contextmenu
}
}
That's it. All you need to do now is attach the [context-menu]
directive to an element and bind it to a list of items. For example:
@Component({
selector:'child-component',
directives:[ContextMenuDirective],
template:`
<div [context-menu]="links" >right click here ... {{firstRightClick}}</div>
<div [context-menu]="anotherLinks">Also right click here...{{secondRightClick}}</div>
`
})
class ChildComponent{
firstRightClick; secondRightClick;
links;
anotherLinks;
constructor(){
this.links = [
{title:'a',subject:new Subject()},
{title:'b',subject:new Subject()},
{title:'b',subject:new Subject()}
];
this.anotherLinks = [
{title:'link 1',subject:new Subject()},
{title:'link 2',subject:new Subject()},
{title:'link 3',subject:new Subject()}
];
}
// subscribe to subjects
ngOnInit(){
this.links.forEach(l => l.subject.subscribe(val=> this.firstCallback(val)));
this.anotherLinks.forEach(l => l.subject.subscribe(val=> this.secondCallback(val)))
}
firstCallback(val){
this.firstRightClick = val;
}
secondCallback(val){
this.secondRightClick = val;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With