angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.32k stars 6.73k forks source link

docs(menu): add example of nested menus using dynamic data #7434

Open Splaktar opened 6 years ago

Splaktar commented 6 years ago

Bug, feature request, or proposal:

Feature Request - Menu Docs

What is the expected behavior?

The docs would include an example of building a menu from a data structure similar to the radio button docs.

What is the current behavior?

The menu docs only include a nested menu example using static HTML.

What are the steps to reproduce?

Working on a Stackblitz demo atm...

What is the use-case or motivation for changing an existing behavior?

This is a common use case for building application menus, especially for business applications. The data may come from a config file, a REST API, or some other source but it is very often not static and does not align with a static HTML example.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Angular 4.4.3 TypeScript 2.3.4 Chrome 61 OS X El Capitan

Is there anything else we should know?

Thank you for your hard work on this library!

Splaktar commented 6 years ago

https://github.com/angular/material2/issues/6765 was likely to block this use case, but the fix has been merged into master.

Splaktar commented 6 years ago

There is an example of creating md-menu's dynamically here but it doesn't demonstrate nesting.

Splaktar commented 6 years ago

I've finally got something working on the latest snapshots. It's not possible in beta.11 due to the bugs related to ngIf/ngFor and menus. I'm working on distilling it down to a simple docs-ready example atm.

cpboyd commented 6 years ago

@Splaktar I'm interested to see what you find...

I just filed #7542 because it's not currently possible to use a recursive ng-template for creating a nested menu.

Mayocampo commented 6 years ago

hey everyone... im currently trying to achieve the same thing... it doesnt seem to be possible... any progress? i'm kinda new to Angular and i'm having a hard time tryin to figure this out. Any help would be more than welcome

Splaktar commented 6 years ago

I will publish what I got working next week. It is quite a different approach to what @cpboyd details in #7542.

Splaktar commented 6 years ago

Here's a Stackblitz of a working dynamic, nested menu: https://stackblitz.com/edit/dynamic-nested-menus?file=app%2Fapp.component.ts.

Note that the routing isn't actually hooked up.

Mayocampo commented 6 years ago

@Splaktar just awesome man.... great work. Question: If a need to bind the actual route, it must be hard coded on rounting.ts right? ... you saved my life :)

Splaktar commented 6 years ago

@Mayocampo Glad that it could help, thanks for the feedback :)

Yeah, I didn't hook up the routing to be generated dynamically. That's another topic that I haven't dug into just yet :) So yes, for now I just make sure that the app-routing.module.ts includes the routes that are specified in my config.

Here's another Stackblitz with routing hooked up plus a top level menu button bar: https://stackblitz.com/edit/dynamic-nested-topnav-menu?file=app%2Fapp.component.html

I'll be presenting on this at AngularMix on Thursday. Then I'll share my slides and some more examples (including a dynamic SideNav menu).

Hopefully I'll have time to put together the docs as well, but that will have to wait until later in November most likely.

cpboyd commented 6 years ago

@Splaktar I could swear I tried a similar approach using a custom components, but for some reason mine didn't propagate the "hover/expand" properly.

In any case, thanks for a working example!

Mayocampo commented 6 years ago

@Splaktar hey man... i've found an issue trying to implement this solution... i don't know if i'm doing something wrong. I'll try to explain:

  1. I took your code and extend interface with some other properties.
  2. I already have a model wich i fill with data from an API
  3. I map that model into the structure of the interface you made

So, when i have a pretty simple one-level-menu it all works ok... but, here's the thing:

i made a service to make all the mapping (cause its kinda complex due to structure of my API data) use that service to fill navItems (this.navItems = this._myService.MapElements(myListOfAPIData)) No compilation errors Page load ok... But when i try to load the menu (on some logon event) everything blows up and got this error:
"ERROR TypeError: Cannot read property 'changes' of undefined at mdMenu.webpackJsonp.../../../material/esm5/menu.es5.js.MdMenu.hover "

Have no idea whats happening there...

Heres my final json structure:

[
{
"idNav":0, "displayName":"Inicio", "iconName":"home", "disabled":false, "route":"home" }, {
"idNav":1, "disabled":true, "displayName":"Gestión de Elementos", "iconName":"list", "children":[
{
"idNav":2, "disabled":true, "displayName":"Gestión de Menús", "iconName":"view_headline", "route":"menuManagement", "parentId":1 }, {
"idNav":3, "disabled":true, "displayName":"Gestión de Roles", "iconName":"assignment", "route":"roleManagement", "parentId":1 }, {
"idNav":4, "disabled":true, "displayName":"Gestión de Usuarios", "iconName":"supervisor_account", "route":"userManagement", "parentId":1 }, {
"idNav":5, "disabled":true, "displayName":"Menú Hijo/Padre", "children":[
{
"idNav":7, "disabled":true, "displayName":"Menú de 2do Nivel Hijo/Padre", "iconName":"supervisor_account", "children":[
{
"idNav":8, "disabled":true, "displayName":"Menú hijo de 3er Nivel", "iconName":"supervisor_account", "route":"userManagement", "parentId":7 } ], "parentId":5 } ], "parentId":1 } ] }, {
"idNav":6, "disabled":true, "displayName":"Menú Standalone", "iconName":"warning" } ]

Hope you can bring some light on this... don't know if this has something to do with a bad behaviour of @ViewChild ... Any help would be gladly received.

Thank u so much (and my apologizes if this is not the way to comunicate this issues)

Splaktar commented 6 years ago

@Mayocampo I am at AngularMix this week and I'm prepping for my talk on Thursday, so I don't have a ton of time to review your specific implementation issue. However, if you have an example in Stackblitz or Plunker, I can take a quick look and try to debug it. Another option would be posting to StackOverflow (and linking that question here).

Btw, I have run into this error and I forget the exact solution. I think it was related to missing fields in the config or needing a check in the template for something like *ngIf="child && child.items.length". It's hard for me to answer your specific case though w/o running the code.

ghost commented 6 years ago

I experienced the same error "ERROR TypeError: Cannot read property 'changes' of undefined at mdMenu.webpackJsonp.../../../material/esm5/menu.es5.js.MdMenu.hover ", related to "hover/expand", you can fix it by upgrading to the latest version of Angular Material. After upgrading my application the error went away and the "hover/expand" behavior now works as expected and demonstrated in the above stackblitz. Yes, it took me a few minutes to change all occurrences of the "md-" prefix to "mat-" prefix throughout my application and confirm no breaking changes exist.

Splaktar commented 6 years ago

Ah yes thank you @ebduhon! Beta.12 of the CDK and Angular Material are required as well as Angular 4.4.4.

Smitha-Patil commented 6 years ago

Dear Materials team, Kindly add this example as this is one of the most wanted use case. @Splaktar: Thank you for the example code.

Swoox commented 6 years ago

@Splaktar cheers this works on Angular 5 to. Would be nice if this feature would be implemented.

d-damien commented 6 years ago

I have that weird problem where submenus appear only on click (not hover) and don't autoclose. It is all reset if I close root menu. See image below.

What's frustrating is it works with documentation code ; it seems to be the dynamic generation / loop that creates problem.

Here's my code. I have tried replacing <a> links with <buttons> and removing all routerLink or specific functions. What did I miss @Splaktar ?

Angular 5.2.1, cdk 5.1, Chromium 64.0.3282.119.

<nav fxShow fxHide.gt-md class="spin-menu">
  <mat-menu #appMenu>
    <ng-container *ngFor="let panel of panels">
      <ng-container [ngSwitch]="panel.type">
        <ng-container *ngSwitchCase="'submenu'">
          <button
            [matMenuTriggerFor]="submenu"
            [ngClass]="{ 'active': hasSubpanelSelected(panel) }"
            mat-menu-item>
              {{ panel.title }}
              <i *ngIf="panel.new" class="new">NEW</i>
              <i *ngIf="panel.beta" class="beta">BETA</i>
          </button>
          <mat-menu #submenu>
            <a *ngFor="let subpanel of panel.subpanels"
              routerLink="{{ base }}/base/dashboard/{{ subpanel.value }}"
              (click)="reloadPage($event)"
              [ngClass]="{ 'active': selectedPanel == subpanel.value }"
              mat-menu-item>
                {{ subpanel.title }}
                <i *ngIf="subpanel.new" class="new">NEW</i>
                <i *ngIf="subpanel.beta" class="beta">BETA</i>
            </a>
          </mat-menu>
        </ng-container>

        <a *ngSwitchDefault
          routerLink="{{ base }}base/dashboard/{{ panel.value }}"
          (click)="reloadPage($event)"
          [ngClass]="{ 'active': selectedPanel == panel.value }"
          mat-menu-item>
            {{ panel.title }}
            <i *ngIf="panel.new" class="new">NEW</i>
            <i *ngIf="panel.beta" class="beta">BETA</i>
        </a>
      </ng-container>
    </ng-container>
  </mat-menu>

  <button [matMenuTriggerFor]="appMenu" mat-button>
    <mat-icon>menu</mat-icon>
  </button>
</nav>

image

d-damien commented 6 years ago

Found it ! <mat-menu> does not like ngSwitch, you have to replace them with ngIf.

irowbin commented 6 years ago

Hi @d-damien! My submenu disappeared when hovering. I just copied your code from above like this.

<mat-menu #appMenu>
    <ng-container *ngFor="let panel of panels">

        <ng-container *ngIf="panel.sub.length>0">
            <button [matMenuTriggerFor]="submenu" mat-menu-item>
                {{ panel.text }}
            </button>
            <mat-menu #submenu>
                <a *ngFor="let subpanel of panel.sub" mat-menu-item>
                    {{ subpanel.text }}
                </a>
            </mat-menu>
        </ng-container>

        <a *ngIf="!(panel.sub.length>0)" mat-menu-item>
            {{ panel.text }}
        </a>

    </ng-container>
</mat-menu>

<button [matMenuTriggerFor]="appMenu" mat-button>
    <mat-icon>menu</mat-icon>
</button>
  panels: any[] = [
        {
            text: 'parent',
            sub: [{
                text: 'sub item'
            }]
        }
    ];

Take a look: video_001 1

d-damien commented 6 years ago

Hello @irowbin. This is all really weird. Non-reproducible bugs are the strangest. My diff indicates ngSwitch -> ngIf to be the only difference.

What I did to find a solution is to copy paste a working example (the one in the doc at "nested menu" section), then slightly modify it step by step until it works for me. Hope you can do the same. Here's my code as of now FYI :

<nav fxShow fxHide.gt-md class="spin-menu">
  <mat-menu #appMenu>
    <ng-container *ngFor="let panel of panels">
      <ng-container *ngIf="panel.type == 'submenu'">
        <button
          [matMenuTriggerFor]="submenu"
          [ngClass]="{ 'active': hasSubpanelSelected(panel) }"
          mat-menu-item>
            {{ panel.title }}
            <i *ngIf="panel.new" class="new">NEW</i>
            <i *ngIf="panel.beta" class="beta">BETA</i>
        </button>
        <mat-menu #submenu>
          <a *ngFor="let subpanel of panel.subpanels"
            routerLink="{{ base }}/base/dashboard/{{ subpanel.value }}"
            (click)="reloadPage($event)"
            [ngClass]="{ 'active': selectedPanel == subpanel.value }"
            mat-menu-item>
              {{ subpanel.title }}
              <i *ngIf="subpanel.new" class="new">NEW</i>
              <i *ngIf="subpanel.beta" class="beta">BETA</i>
          </a>
        </mat-menu>
      </ng-container>

      <a *ngIf="panel.type != 'submenu'"
        routerLink="{{ base }}base/dashboard/{{ panel.value }}"
        (click)="reloadPage($event)"
        [ngClass]="{ 'active': selectedPanel == panel.value }"
        mat-menu-item>
          {{ panel.title }}
          <i *ngIf="panel.new" class="new">NEW</i>
          <i *ngIf="panel.beta" class="beta">BETA</i>
      </a>
    </ng-container>
  </mat-menu>

  <button [matMenuTriggerFor]="appMenu" mat-button>
    <mat-icon>menu</mat-icon>
    Menu
  </button>
</nav>
d-damien commented 6 years ago

Data looks like this :

"panels": [
    {
      "value": "1",
      "title": "1"
    },
    {
      "title": "2",
      "type": "submenu",
      "subpanels": [
        {
          "value": "2.1",
          "title": "2.1"
        },
        {
          "value": "2.2",
          "title": "2.2"
        }
      ]
    },
qpi commented 6 years ago

I have the same hovering issue:

issue

Here it is in live:

https://qpi.github.io/sightseeing/route

The source is here:

https://github.com/qpi/sightseeing/blob/master/src/app/route/route.component.html

irowbin commented 6 years ago

@qpi https://github.com/angular/material2/issues/10081

CharlyRipp commented 6 years ago

Simplified working example.

Somewhat annoying to use ng-container and having to create the button twice - once with and once without the [matMenuTriggerFor].

let links = [
    { label: "Link 1", route: "/link1"},
    { label: "Link 2", route: "/link2", children: [
        { label: "Child Link 1", route: "/child1"},
        { label: "Child Link 2", route: "/child2"}
    ]}
]
<mat-menu #appMenu="matMenu">
    <ng-container *ngFor="let link of links">
        <!-- subMenu is created regardless as it causes an error even if *ngIf is found false -->
        <mat-menu #subMenu="matMenu">
          <button mat-menu-item
                  *ngFor="let childLink of link.children"
                  [routerLink]="childLink.route">{{childLink.label}}</button>
        </mat-menu>

        <!-- Button with submenu -->
        <button *ngIf="link.children" mat-menu-item
                [matMenuTriggerFor]="subMenu"
                [routerLink]="link.route">{{link.label}}</button>

        <!-- Button without submenu -->
        <button  *ngIf="!link.children" mat-menu-item
                [routerLink]="link.route">{link.label}}</button>
    </ng-container>
</mat-menu>

<button id="nav-toggle" mat-icon-button [matMenuTriggerFor]="appMenu">
    Menu
</button>

Just throwing this here for others -- a proper solution would be much nicer :)

sashagrunge commented 6 years ago

@Splaktar Thanks for the example. Are there any plan to support the hover event on latest version, so we don't have to stick to angular 4.4.4 for the solution?

Splaktar commented 6 years ago

@sashagrunge which version are you using? The hover issue was fixed in Angular Material 5.2.4.

irowbin commented 6 years ago

@Splaktar YEP, there is no issues. @sashagrunge You should update to latest version.

sashagrunge commented 6 years ago

@Splaktar @irowbin I am using the latest version 5.2.4, but I realised that hover doesn't work only if I add one more component to wrap the menu-item. Thank!

amoguilner commented 6 years ago

I have expanded this menu to use dynamic click function as well as render checkbox control. Do you know how do I bind that to a variable via ngModel to that checkbox or set initial value of checkbox that is dependent on the variable and then change variable when checkbox state changes?

Splaktar commented 6 years ago

@amoguilner I haven't played with that advanced use case yet. It sounds quite interesting. It would be helpful if you had a Stackblitz to look at. I would also guess that Reactive Forms would be more capable for something like this compared to Template Driven Forms.

@sashagrunge Please post a Stackblitz demo of what you are referring to.

alex-vas commented 4 years ago

It is appeared to be quite tricky to implement model driven recursive menu in angular material. Quite a few examples out there are not compatible with angular past 8.0. This issue is discussed here: #16457 in details.

For those looking for an example working with 9+ angular, @Harpush has pointed to this https://stackblitz.com/edit/dynamic-material-menu-angular-8 in his comment here.

nzbin commented 3 years ago

@Mayocampo Glad that it could help, thanks for the feedback :)

Yeah, I didn't hook up the routing to be generated dynamically. That's another topic that I haven't dug into just yet :) So yes, for now I just make sure that the app-routing.module.ts includes the routes that are specified in my config.

Here's another Stackblitz with routing hooked up plus a top level menu button bar: https://stackblitz.com/edit/dynamic-nested-topnav-menu?file=app%2Fapp.component.html

I'll be presenting on this at AngularMix on Thursday. Then I'll share my slides and some more examples (including a dynamic SideNav menu).

Hopefully I'll have time to put together the docs as well, but that will have to wait until later in November most likely.

Many thanks for your examples, in the recursive nested menu, routerLinkActive is not working, how can I add a active class?

nzbin commented 3 years ago

@Mayocampo Glad that it could help, thanks for the feedback :) Yeah, I didn't hook up the routing to be generated dynamically. That's another topic that I haven't dug into just yet :) So yes, for now I just make sure that the app-routing.module.ts includes the routes that are specified in my config. Here's another Stackblitz with routing hooked up plus a top level menu button bar: https://stackblitz.com/edit/dynamic-nested-topnav-menu?file=app%2Fapp.component.html I'll be presenting on this at AngularMix on Thursday. Then I'll share my slides and some more examples (including a dynamic SideNav menu). Hopefully I'll have time to put together the docs as well, but that will have to wait until later in November most likely.

Many thanks for your examples, in the recursive nested menu, routerLinkActive is not working, how can I add a active class?

I have solved my own problem, take a look at the link below. https://ng-matero.github.io/ng-matero/dashboard (switch top navigation to check the recursive nested menu)