isaacplmann / ngx-contextmenu

An Angular component to show a context menu on an arbitrary component
MIT License
248 stars 91 forks source link

Dynamically add menu items #147

Open AlexBokhankovich opened 5 years ago

AlexBokhankovich commented 5 years ago

I need to add custom context menus for some components In my app. I woud like to use ComponentFactory for this. How can I add menu items in my dynamically added ContextMenuComponent? Please see the below code for reference

createContextMenu(): any {
        if (!this.contextMenuItems || this.contextMenuItems.length === 0) {
            return;
        }
        const contextMenuComponentFactory = this.componentFactoryResolver.resolveComponentFactory(ContextMenuComponent);
        const contextMenuCompRef = this.viewContainerRef.createComponent(contextMenuComponentFactory);
        const contextMenuComp = contextMenuCompRef.instance;
        this.contextMenuItems.forEach(contextMenuItem => {
              // loop over all context menu records and add them to context menu component
        });
    }
isaacplmann commented 5 years ago

That's an interesting problem. I've never had to do that before.

You could try doing this:

<ng-template contextMenuItem></ng-template>
<!-- as many of these as you need, or use an *ngFor -->
@ViewChild(ContextMenuItemDirective) contextMenuItemDirectives: QueryList<ContextMenuItemDirective>;

createContextMenu(): any {
        if (!this.contextMenuItems || this.contextMenuItems.length === 0) {
            return;
        }
        const contextMenuComponentFactory = this.componentFactoryResolver.resolveComponentFactory(ContextMenuComponent);
        const contextMenuCompRef = this.viewContainerRef.createComponent(contextMenuComponentFactory);
        const contextMenuComp = contextMenuCompRef.instance;
        contextMenuComp.menuItems = this.contextMenuItemDirectives;
    }

I'm not sure if that will work.

I also found a more generic way to add ng-content to a dynamically created component: https://blog.ng-book.com/dynamic-components-with-content-projection-in-angular/

I'd use the Handling Templates strategy in that article. And have the template look like this:

<ng-template #contextMenuItems>
  <ng-template *ngFor="let action of contextMenuItems" contextMenuItem let-item
    [visible]="action.visible" [enabled]="action.enabled" [divider]="action.divider"
    (execute)="action.click($event.item)">
    {{ action.html($event.item) }}
  </ng-template>
</ng-template>

Good luck! Let me know if you get it working. I'm curious.

AlexBokhankovich commented 5 years ago

Thanks a lot for your assistance but this would not work in my case since I have a complex components that extends each other and I need to add this logic at the most low level component, that is extended by about 20 components. As angular components doesn't not support template inheritance, I can not use any template code. That is why I'm trying to do this programmatically. So, now I see two possible solutions: add template markup to each of my components or somehow inject menus dynamically. I've already use dynamically created components widely in my app. In every component I expose ViewContainerRef so other components can use it to dynamically inject needed components. In your component I see 2 things that could porevent from injecting it dynamically I a way I did before: -it doesn't expose ViewContainerRef -it uses ng-template and directive for menu items.

I think addind public ViewContainerRef to ContextMenuComponent and adding support for menu items components (not a directives) would let use dynamic injection. So, in this case we can dynamically create ContextMenuComponent, and using its ViewContainerRef inject as many MenuItemsComponents as we need.

isaacplmann commented 5 years ago

You could try passing the TemplateRef down as an Input to whatever component needs it.

I’m open to making a public ViewContainerRef, but I don’t want to change ContextMenuItems from directives to components.  It’s not worth making a breaking change for a minority use case. On Dec 7, 2018, 1:01 AM -0500, Alex Bokhankovich notifications@github.com, wrote:

Thanks a lot for your assistance but this would not work in my case since I have a complex components that extends each other and I need to add this logic at the most low level component, that is extended by about 20 components. As angular components doesn't not support template inheritance, I can not use any template code. That is why I'm trying to do this programmatically. So, now I see two possible solutions: add template markup to each of my components or somehow inject menus dynamically. I've already use dynamically created components widely in my app. In every component I expose ViewContainerRef so other components can use it to dynamically inject needed components. In your component I see 2 things that could porevent from injecting it dynamically I a way I did before: -it doesn't expose ViewContainerRef -it uses ng-template and directive for menu items. I think addind public ViewContainerRef to ContextMenuComponent and adding support for menu items components (not a directives) would let use dynamic injection. So, in this case we can dynamically create ContextMenuComponent, and using its ViewContainerRef inject as many MenuItemsComponents as we need. — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

AlexBokhankovich commented 5 years ago

I've solved this by creating component, that I inject in every place where I need custom context menu

import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { ContextMenuComponent } from 'ngx-contextmenu';
import { TypeChecker } from '../../animation/type-checker';
import { Point } from '../../models/point';
import { ReceptorAction } from '../actions/receptor-action';
import { ContextMenuItem } from './context-menu-item';

@Component({
    selector: 'app-context-menu-receptor',
    template: '<context-menu>
    <ng-template contextMenuItem
        *ngFor="let menuItem of menuItems"
        (execute)="execReceptorAction($event.item, menuItem.action)">
        {{menuItem.name}}
    </ng-template>
</context-menu>',
    styleUrls: ['./context-menu-receptor.component.scss']
})
export class ContextMenuReceptorComponent implements OnInit {
    @ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
    @Output() receptorTriggered: EventEmitter < ReceptorAction > = new EventEmitter();

    menuItems: ContextMenuItem[];
    constructor() {}

    ngOnInit() {}

    execReceptorAction(position: Point, receptorAction: ReceptorAction) {

        if (TypeChecker.instanceOfWindowOpenAction(receptorAction)) {
            receptorAction.position = position;
        }

        this.receptorTriggered.emit(receptorAction)
    }
}

and in my component I inject this and set it's menu items

     createContextMenu(): any {
        if (!this.contextMenuItems || this.contextMenuItems.length === 0) {
            return;
        }

        const contextMenuComponentFactory = this.componentFactoryResolver.resolveComponentFactory(ContextMenuReceptorComponent);
        const contextMenuCompRef = this.viewContainerRef.createComponent(contextMenuComponentFactory);
        const contextMenuComp = contextMenuCompRef.instance;

        contextMenuComp.menuItems = this.contextMenuItems;

        this.contextMenu = contextMenuComp.contextMenu;
        contextMenuComp.receptorTriggered.subscribe((action) => {

            this.receptorTriggered.emit(action);
        })
        contextMenuComp.ngOnInit();

        this.viewContainerRef.element.nativeElement.addEventListener('contextmenu', ($event, item) => {
            this.onContextMenu($event, this.contextMenuItems);
        });        
    }

private onContextMenu($event: MouseEvent, item: any): void {
        console.log($event, item);
        const x = ($event.x ? $event.x : 0);
        const y = ($event.y ? $event.y : 0);
        const point = new Point(x, y);

        this.contextMenuService.show.next({
            // Optional - if unspecified, all context menu components will open
            contextMenu: this.contextMenu,
            event: $event,
            item: point,
        });
        $event.preventDefault();
        $event.stopPropagation();
    }