1

I'm playing around with Angular 2. Currently I wonder what is the best way to implement a dynamic menu component. The idea is to have a global menu component that receives menu items for every other content component in the app.

One idea was, to have a menu.component, a menu.service and other components, that use the service. The service is referenced globally in app.module. The menu.components subscribes to an observable and dynamically add items that get pasted in. I think this is not the best solution.

Another idea is, that I put the menu selector/tag in every components template and paste data to the menu component directly. Currently I stuck in how to get items to the menu.component, so that the ngFor generates the menu items after it was initialized.

So the question is: Are there any "best practices" or common way to do this?

Guess I need some more basics in Angular 2, but it would be nice if you can lead me on the right path. It's just a learning project :)

Here is some code of what I've tried:

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HTTP_PROVIDERS } from '@angular/http';

import { AppComponent } from './app.component';
import { appRouterProviders } from './app.routes';
import { MenuComponent } from './menu.component';

import { HomeComponent } from './home.component';

@NgModule({
    imports: [BrowserModule],
    declarations:
    [
        AppComponent

    ],
    bootstrap: [AppComponent],
    providers:
    [
        MenuComponent,
        appRouterProviders,
        HTTP_PROVIDERS
    ]
})
export class AppModule { }

menu.component.ts

import { Component } from '@angular/core';
import { ROUTER_DIRECTIVES } from '@angular/router';

export class MenuItem {
    name: string;
    path: string;
}

@Component({
    selector: 'component-menu',
    template: `
    <ul>
    <li *ngFor="let item of menuItems"><a [routerLink]="[item.path]">{{item.name}}</a></li>
    </ul>
    `
})
export class MenuComponent {

    public menuItems: Array<MenuItem>;

    constructor() {
    }

}

home.component.ts

import { Component, OnInit } from '@angular/core';
import { MenuComponent, MenuItem } from './menu.component';

@Component({
    selector: 'app-home',
    directives: [],
    template: `<h2>Home Component</h2>
    <component-menu></component-menu>`
})
export class HomeComponent implements OnInit{
    private menuItems:Array<MenuItem>;

    constructor(private menuComponent:MenuComponent) {
        console.log('Home component ctor');
        this.menuItems = [
            {name:'Home', path: '/home'},
            {name:'Content', path: '/content'}
        ];

        this.menuComponent.menuItems = this.menuItems;


    }

    ngOnInit(){

    }
}
5
  • "Dynamic Menu" is quite generic. You didn't provide any requirements for the menu. This question is also about opinions which is discouraged on SO. Commented Aug 18, 2016 at 8:55
  • OK - the idea is that there is one separated menu component and every "content"-component has its own menu items that get transferred to this menu component. Sounds really simple. I think this is a common use case. Commented Aug 18, 2016 at 8:59
  • Please edit your question and post the code that demonstrates what you try to accomplish and where you failed. Commented Aug 18, 2016 at 9:03
  • It is more about the question what is the best way to implement such a global menu component than getting help with a specific code. Commented Aug 18, 2016 at 9:08
  • But specific code makes it easier to grasp what you actually try to accomplish. Please check the help menu about what questions (not) to ask and how to ask good questions. SO is about concrete programming problems. Good questions usually contain code that demonstrate what was tried and where one failed and if applicable concrete error messages. For discussions other channels are more appropriate. Commented Aug 18, 2016 at 9:12

2 Answers 2

8

I have implemented exactly what you want in my project. Here's how I did it:

1- I created a service that I called ContextMenuService that is responsible for instantiating the context menu

2- I created a ContextMenu component, which is the actual menu to be displayed.

3- The service dynamically creates the component, and whenever there is a click outside the menu, the service destroys the menu component.

Here's the service:

@Injectable()

export class ContextMenuService {

  private _menuAlreadyOn: boolean = false;

  private _currentContextMenu: ComponentRef<any>;

  viewContainerRef: ViewContainerRef

  showContextMenu(event: MouseEvent, options: ContextMenuOption[]) : boolean {

    event.stopPropagation();

    if (this._menuAlreadyOn) {
      this._currentContextMenu.destroy();
      this._menuAlreadyOn = false;
    }

    let componentRef = this.viewContainerRef.createComponent(this._cfr.resolveComponentFactory(ContextMenuComponent))

    componentRef.instance.options = options;
    componentRef.location.nativeElement.getElementsByTagName('div')[0].style.left = event.clientX
    componentRef.location.nativeElement.getElementsByTagName('div')[0].style.top = event.clientY

    this._currentContextMenu = componentRef;
    this._menuAlreadyOn = true;

    let listener = this._eventManager.addGlobalEventListener('document', 'click',  () => {

      this._currentContextMenu.destroy();
      this._menuAlreadyOn = false;
      listener();
    })

    return false;
  }

  constructor(
    private _cfr: ComponentFactoryResolver,
    private _eventManager: EventManager
  ) { }   
}

Now, I have to inject this service into the app module, using the providers array, but also, I need to provide ContextMenuComponent to app module in the list of entryComponents, entryComponents is a list for all components that are dynamically created:

@NgModule({
  declarations: [
    AppComponent,
    ContextMenuComponent
  ],
  providers: [
    ContextMenuService,
  ],
  bootstrap: [AppComponent],
  entryComponents: [ContextMenuComponent]
})
export class AppModule{}

Finally, in the constructor of the main app component, I tell it that the ViewContainerRef of the context menu service, is the view container ref of the app component:

@Component ({
  selector: 'saugo-viewer',
  templateUrl: 'app/views/app.component.html',
  styleUrls: ['app/css/app.component.css'],
 }
)

export class AppComponent {
  constructor(
    private contextMenuService: ContextMenuService,
    viewContainer: ViewContainerRef
) {
    contextMenuService.viewContainerRef = viewContainer;
  }
}

My ContextMenuComponent simply looks like this:

export interface ContextMenuOption {
  text: string;
  action?: () => void;
  icon?: string;
  disabled?: boolean;
}

@Component({
  selector: 'context-menu',
  templateUrl: 'app/views/helpers/context-menu.service.html',
  styleUrls: ['app/css/helpers/context-menu.service.css']
})

export class ContextMenuComponent {

  options: ContextMenuOption[] = [];

  itemClicked(i: number) {
    if (this.options[i].action) {
      this.options[i].action();
    }
  }
}

With this template:

<div id="context-menu"class="custom-menu">
  <ul>
    <li *ngFor="let option of options; let i = index"
    [ngClass]="{'active-item': !option.disabled}" (click)="itemClicked(i)">
      <span [class]="'glyphicon ' + option.icon"></span>
      <a href="#" [ngClass]="{'disabled-text': option.disabled}">{{option.text}}</a>
    </li>
  </ul>
</div>

I hope this could help you to create your own custom context menu

Sign up to request clarification or add additional context in comments.

Comments

1

First thing that comes to my mind after reading this question is routes. You can use nested routes to make menu like that. After reloading the page you will be in the same page. Additionally you can generate different menu items based on the current route if you need such functionality.

routes documentation

For changing routes dynamically you can use resetConfig method. resetConfig

for parent component use prefix type of pathMatch

The other possible pathMatch value is 'prefix' which tells the router to match the redirect route when the remaining URL begins with the redirect route's prefix path.


To sync data between components use service that is provided by main app file.


If you need more dynamic approach then use Compiler to inject your components to the view. Compiler documentation

constructor(
    private compiler:Compiler,
    private viewContainerRef:ViewContainerRef){}

...

this.compiler.compileComponentAsync(yourComponentClass).then((cmpFactory)=>{
      const injector = this.viewContainerRef.injector;
      this.viewContainerRef.createComponent(cmpFactory, 0,  injector);
    });

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.