Dynamically nested angular material menu

11,400

Solution 1

As a result, it turned out, relying on several similar problems with others. The examples from HERE (dynamic nested menu example) and from HERE (the problem with mat-menu hides immediately on opening) helped to figure it out (in the last example it was enough just to update zone.js by npm)

Solution 2

Sorry for the late answer, but maybe you can still find it helpful.
I wrote a little library called ng-action-outlet that is doing that quite neatly in my opinion.


It looks like this:

group: ActionGroup;

constructor(private actionOutlet: ActionOutletFactory) {
    this.group = this.actionOutlet.createGroup();

    this.group.createButton().setIcon('home').fire$.subscribe(this.callback);
    this.group.createButton().setIcon('settings').fire$.subscribe(this.callback);
}
<ng-container *actionOutlet="group"></ng-container>

DEMO: https://stackblitz.com/edit/ng-action-outlet-demo?file=src/app/app.component.ts

Share:
11,400
titaniche
Author by

titaniche

Updated on June 08, 2022

Comments

  • titaniche
    titaniche about 2 years

    Please tell me how I can solve the following problem: I need to implement a dynamically created menu with different nesting levels depending on the data model object. At the moment, using recursion, we managed to create the menu as such, however, there is a problem of assigning the attribute [matMenuTriggerFor] for, directly, the submenu. The problem is that all subsequent submenus in fact refer to the very first, so when you hover over any of the submenus, it causes a "flip" to the original one (example on image: menu, which includes elements: Device, Extension, Queue, Queue member (with submenu elements)). Thus, for a fraction of seconds, I see the other submenu frame (example on image: submenu Grouped list), after which the very first becomes active. Of course, maybe I didn’t do everything right, so I’m turning here. Help me please. Thank you all.

    Dynamically nested material menu

    imenu-item.ts

    export interface IMenuItem {
      name: string | string[];
      link: string;
      subItems: IMenuItem[];
    }
    

    dynamic-menu.service.ts

    import {Inject, Injectable} from '@angular/core';
    import {APP_CONFIG_ROUTES} from '../../../config/routes/app.config.routes';
    import {IAppConfigRoutes} from '../../../config/routes/iapp.config.routes';
    import {IMenuItem} from './imenu-item';
    import {_} from '@biesbjerg/ngx-translate-extract/dist/utils/utils';
    
    @Injectable({
      providedIn: 'root'
    })
    export class DynamicMenuService {
      private readonly appConfig: any;
    
      constructor(@Inject(APP_CONFIG_ROUTES) appConfig: IAppConfigRoutes) {
        this.appConfig = appConfig;
      }
    
      getMenuItems(): IMenuItem[] {
        return [
          {
            name: _('labels.device'),
            link: '/' + this.appConfig.routes.device,
            subItems: null
          },
          {
            name: _('labels.extension'),
            link: '/' + this.appConfig.routes.extension,
            subItems: null
          },
          {
            name: _('labels.queue'),
            link: '/' + this.appConfig.routes.queue,
            subItems: null
          },
          {
            name: _('labels.queueMember'),
            link: null,
            subItems: [{
              name: _('labels.fullList'),
              link: '/' + this.appConfig.routes.queueMember.all,
              subItems: null
            }, {
              name: _('labels.groupedList'),
              link: '/' + this.appConfig.routes.queueMember.grouped,
              subItems: [{
                name: 'subName',
                link: 'subLink',
                subItems: [{
                  name: 'subSubName1',
                  link: 'subSubLink1',
                  subItems: null
                }, {
                  name: 'subSubName2',
                  link: 'subSubLink2',
                  subItems: null
                }]
              }]
            }]
          }
        ];
      }
    }
    

    dynamic-menu.component.ts

    import {Component, Input, OnInit} from '@angular/core';
    import {IMenuItem} from './imenu-item';
    
    @Component({
      selector: 'app-dynamic-menu',
      templateUrl: './dynamic-menu.component.html',
      styleUrls: ['./dynamic-menu.component.scss']
    })
    export class DynamicMenuComponent implements OnInit {
      dynamicMenuItemsData: IMenuItem[];
    
      constructor(private dynamicMenuService: DynamicMenuService) {
      }
    
      ngOnInit() {
       this.dynamicMenuItemsData = this.dynamicMenuService.getMenuItems();
      }
    }
    

    dynamic-menu.component.html

    <div>
      <ng-container [ngTemplateOutlet]="recursiveListMenuItems"
                    [ngTemplateOutletContext]="{$implicit: dynamicMenuItemsData}">
      </ng-container>
    </div>
    
    <ng-template #recursiveListMenuItems let-listMenuItems>
      <div *ngFor="let menuItem of listMenuItems">
        <ng-container [ngTemplateOutlet]="menuItem.subItems != null ? subMenuItem : simpleMenuItem"
                      [ngTemplateOutletContext]="{$implicit: menuItem}">
        </ng-container>
      </div>
    </ng-template>
    
    <ng-template #simpleMenuItem let-menuItemArg>
      <a class="mat-button"
         mat-menu-item
         routerLink="{{menuItemArg.link}}">
        <span>{{menuItemArg.name | translate}}</span>
      </a>
    </ng-template>
    
    <ng-template #subMenuItem let-menuItemArg>
      <a class="mat-button"
         mat-menu-item
         routerLink="{{menuItemArg.link}}"
         [matMenuTriggerFor]="subItemsMenu">
        <span>{{menuItemArg.name | translate}}</span>
        <mat-menu #subItemsMenu="matMenu"
                  [overlapTrigger]="false">
          <ng-container [ngTemplateOutlet]="recursiveListMenuItems"
                        [ngTemplateOutletContext]="{$implicit: menuItemArg.subItems}">
          </ng-container>
        </mat-menu>
      </a>
    </ng-template>
    
  • titaniche
    titaniche about 5 years
    Thanks for the answer. Now the question itself is not particularly relevant, but in the future I will try to focus on your option)