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