John Woodruff
John Woodruff

Reputation: 1666

How to create a dynamic menu using the Angular CDK Menu

I'm working on building an application menubar-style menu using the new Angular CDK Menu. I want to dynamically build this menu from a given JSON structure rather than hardcoding the menu and each submenu as shown in the docs examples. Since I want to dynamically build a menu, I'm not sure how to do it since I can't dynamically create template reference variables that I can then reference with the menu trigger.

I've tried doing a recursive component method, but that doesn't fully work, as the menus don't close when mousing to the next menu. I believe it's because once they're inside the component, the menu items are no longer direct siblings which makes it so they don't properly register in terms of auto closing when a sibling opens. I have a StackBlitz I've been working on that has my attempt and it shows the behavior of not closing the previous menu when mousing to another menu item. I've read the menu docs multiple times trying to see if I missed anything, and the CDK Menu is so new (just released a few weeks ago) that there's not really anything on the internet in the way of example usage.

Basically, what the StackBlitz shows, I have my menubar definition in the AppComponent defining the cdkMenuBar, like so:

<div class="titlebar" cdkMenuBar>
  <app-menu *ngFor="let item of menu" [item]="item" [isRoot]="true"></app-menu>
</div>

Then I have the actual menu component which is used recursively, like so:

<button
  class="menu-item-button"
  cdkMenuItem
  [cdkMenuTriggerFor]="menu"
  [class.menubar-button]="isRoot"
>
  <div>{{ item.label }}</div>
  <div *ngIf="!isRoot && item.type === 'submenu'">&gt;</div>
</button>

<ng-template #menu>
  <div class="menu-dropdown" cdkMenu>
    <ng-container *ngFor="let subItem of item.submenu">
      <ng-container *ngIf="subItem.type !== 'separator'; else separator">
        <app-menu
          [item]="subItem"
          *ngIf="subItem.type === 'submenu'; else menuItem"
        ></app-menu>
        <ng-template #menuItem>
          <button class="menu-item-button" cdkMenuItem>
            <div>{{ subItem.label }}</div>
            <div class="text-color-tertiary">
              {{ subItem.accelerator }}
            </div>
          </button>
        </ng-template>
      </ng-container>
      <ng-template #separator>
        <hr />
      </ng-template>
    </ng-container>
  </div>
</ng-template>

That basically iterates through the "submenu" items in the menu template and then for each one invokes the menu component recursively. Eventually there is no submenu and the recursion ends there.

As I mentioned above, I believe the cause may be related to the menu items not being direct siblings which possibly makes it so the Angular CDK doesn't know to close those ones. I am hoping there's a way to make this dynamic usecase work.

Upvotes: 3

Views: 6474

Answers (1)

Rens Jaspers
Rens Jaspers

Reputation: 1962

If you don't exactly follow the ng-template[#menu] > [cdkMenu] > [cdkMenuItem] hierarchy, the menu won't work as expected.

So you can't wrap [cdkMenu] or [cdkMenuItem] with app-menu without taking some steps to restore the required DOM structure.

You could solve the nesting issue by directly referencing menuComponent.menu in the parent component's [cdkMenuTriggerFor]:

app.component.html

<button [cdkMenuTriggerFor]="menuComponent.menu">menu</button>

<app-menu #menuComponent [items]="menuItems"></app-menu>

menu.component.html

<ng-template #menu>
  <div cdkMenu>
    <ng-container *ngFor="let item of items">
      <ng-container *ngIf="hasChildren(item); else leafNode">
        <button cdkMenuItem [cdkMenuTriggerFor]="subMenu" *ngIf="menuComponent.menu as subMenu">
              <div>{{ item.label }}</div>
              <div *ngIf="item.children">▸</div>
            </button>
        <app-menu #menuComponent [items]="item.children!"></app-menu>
      </ng-container>
      <ng-template #leafNode>
        <button cdkMenuItem>
              {{ item.label }}
            </button>
      </ng-template>
    </ng-container>
  </div>
</ng-template>

Working demo: https://stackblitz.com/github/rensjaspers/cdk-dynamic-menu-demo?file=src/app/app.component.html

Upvotes: 3

Related Questions