Angular 2:实现自定义上下文菜单

时间:2016-01-18 18:30:22

标签: angular

我实现了Angular 2属性指令,允许我向这样的元素添加自定义上下文菜单:

<p context-menu="myItems">Hello world</p>

该指令添加了一个鼠标事件处理程序来捕获右键单击,然后想法构建一个上下文菜单,将其添加到DOM,然后在用户完成时将其销毁。

我有一个实现上下文菜单本身的组件。我想构建该组件,在其上调用一个方法来设置项目列表,然后将其添加到DOM中。

看起来我可以用AppViewManager.createHostViewInContainer来做到这一点。这是一个合适的方法吗?如果是这样,有没有办法构造/获取ElementRef到document.body,以便我可以告诉createHostViewInContainer在那里构建组件?显然,我不希望我的菜单被剪切到我添加上下文菜单的元素中。

1 个答案:

答案 0 :(得分:26)

我认为这是一个很好的方法。
您需要1个服务,1个组件和1个指令。

Here is a plunker

说明:

  

服务ContextMenuService

  • 提供{event:MouseEvent,obj:any[]}类型的主题 由ContextMenuHolderComponent订阅,并接收值 来自ContextMenuDirective

代码:

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[]}>();
}

并将其添加到bootstrap()

中的提供商列表中
bootstrap(AppComponent,[ContextMenuService]);
  

组件ContextMenuHolderComponent

  • 此组件添加到根组件内。例如AppComponent并且它有一个fixed位置。
  • 订阅subject中的ContextMenuService即可获得:

    1. 类型为{title:string,subject:Subject}[]的菜单项,主题用于发送菜单中点击的值
    2. MouseEvent对象
  • 它有一个(document:click)事件监听器,用于关闭菜单外点击的菜单。

代码:

@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
    }
  }
}

并将其添加到根组件:

@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 { }
  

最后一个,ContextMenuDirective

  • 它向主机元素添加了contextmenu事件。
  • 接受要传递给ContextMenuHolderComponent的项目列表的输入。

代码:

@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
  }
}

就是这样。您现在需要做的就是将[context-menu]指令附加到元素并将其绑定到项列表。例如:

@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;
  }
}