DiPix
DiPix

Reputation: 6083

How to create collapse menu using Angular Material?

In documentation of Angular Material there is cool feature. Menu with subcategory which can be expanded/collapsed.

Is it possible to create it using some component? Or I have to do it myself from scratch? Or maybe you can suggest me some package that save my time.

enter image description here

Upvotes: 7

Views: 23636

Answers (4)

Mark Lagendijk
Mark Lagendijk

Reputation: 6883

You can combine mat-sidenav with mat-tree to achieve this. This can be done without any 'hacky' fixes, which are needed if you try to use mat-accordion instead.

I created this Stackblitz example that shows the result.

HTML:

<mat-nav-list>
  <mat-tree [dataSource]="menuItems" [treeControl]="menuTreeControl">
    <mat-tree-node *matTreeNodeDef="let menuItem">
      <a
        [routerLink]="menuItem.link"
        mat-list-item
        routerLinkActive="mdc-list-item--activated"
      >
        {{ menuItem.name }}
      </a>
    </mat-tree-node>

    <mat-nested-tree-node *matTreeNodeDef="let menuItem; when: hasChild">
      <a mat-list-item matTreeNodeToggle>
        <span matListItemTitle
          >{{ menuItem.name }}
          <mat-icon>
            {{
              menuTreeControl.isExpanded(menuItem)
                ? "expand_more"
                : "expand_less"
            }}
          </mat-icon>
        </span>
      </a>
      <div *ngIf="menuTreeControl.isExpanded(menuItem)" role="group">
        <ng-container matTreeNodeOutlet></ng-container>
      </div>
    </mat-nested-tree-node>
  </mat-tree>
</mat-nav-list>

SCSS:

mat-nav-list {
  div[role="group"] {
    padding-left: 20px;
  }

  mat-icon {
    position: absolute;
    right: 20px;
  }
}

Component:

export class NavMenuComponent {
  previewEnabled = Cookies.get("previewEnabled") === "true";
  title$: Observable<string>;

  menuTreeControl = new NestedTreeControl<MenuItem>(
    (menuItem) => menuItem.children
  );

  menuItems: MenuItem[] = [
    { type: "link", link: "link-1", name: "Item 1"},
    {
      type: "section",
      name: "Parent Item",
      children: [
        { type: "link", link: "link-2", name: "Item 2" },
        { type: "link", link: "link-3", name: "Item 3" },
      ],
    },
  ];

  constructor(
    private router: Router,
  ) {
    router.events
      .pipe(filter((event) => event instanceof NavigationEnd))
      .subscribe(() => this.openActiveRouteParentMenuItem());
  }

  openActiveRouteParentMenuItem() {
    _.forEach(this.menuItems, (parentMenuItem) => {
      const childMenuItems = this.menuTreeControl.getChildren(
        parentMenuItem
      ) as MenuItem[];
      _.forEach(childMenuItems, (childMenuItem) => {
        if (
          this.router.isActive(childMenuItem.link as string, {
            paths: "subset",
            queryParams: "subset",
            fragment: "ignored",
            matrixParams: "ignored",
          })
        ) {
          this.menuTreeControl.expand(parentMenuItem);
        }
      });
    });
  }
  
  hasChild = (_: number, menuItem: MenuItem) => !!menuItem.children?.length;
}

interface MenuItem {
  type: "link" | "section";
  name: string;
  link?: string;
  children?: MenuItem[];
}

Upvotes: 1

Trevor Karjanis
Trevor Karjanis

Reputation: 1724

Place mat-expansion-panel's in a mat-accordion and mat-nav-list's in the expansion panels. Then create a global style for the new navigation accordion.

StackzBlitz

<mat-accordion class="app-nav-accordion">
  <mat-expansion-panel class="mat-elevation-z0">
    <mat-expansion-panel-header>
      <mat-panel-title>Section Two</mat-panel-title>
    </mat-expansion-panel-header>
    <mat-nav-list>
      <a mat-list-item>Item Three</a>
        <a mat-list-item>Item Four</a>
    </mat-nav-list>
  </mat-expansion-panel>
</mat-accordion>
.app-nav-accordion {
  .mat-expansion-panel {
    border-radius: 0px !important;
    box-shadow: none !important;

    &.mat-expansion-panel-spacing {
      margin: 0px;
    }

    .mat-expansion-panel-body {
      padding: 0px;
    }

    .mat-expansion-panel-header {
      height: 40px;
      padding: 0px 24px 0px 16px;
    }

    .mat-expansion-panel-header-title {
      color: rgba(0,0,0,0.54);
      font-size: 14px;
      font-weight: 500;
    }
  }
}

Upvotes: 4

felixosle
felixosle

Reputation: 81

I tried to achieve this myself with a side-nav. Maybe it can save you some time.

The nav-items follows a simple interface:

interface NavItem {
displayName: string;
disabled?: boolean;
iconName: string;
route?: string;
children?: NavItem[];}

And then in the component.html the main idea is to add the nav-items recursively (*ngFor). Use mat-accordions for each category and then use another *ngFor to get the children:

<mat-nav-list>
<span *ngFor="let item of menu">
  <span *ngIf="item.children && item.children.length > 0">
    <mat-accordion>
      <mat-expansion-panel>
        <mat-expansion-panel-header>
          <mat-panel-title>
            <!-- Cabeceras del submenu -->
            <div fxLayout="row" fxLayoutAlign="space-between center" > 
              <mat-icon>{{item.iconName}}</mat-icon>
              {{item.displayName}}
            </div>
          </mat-panel-title>
        </mat-expansion-panel-header>
        <span *ngFor="let child of item.children">
          <mat-list-item routerLink="[child.route]">
            <!-- Entradas de cada submenú  -->
            <div fxLayout="row" fxLayoutAlign="space-between center" > 
              <mat-icon>{{child.iconName}}</mat-icon>
              {{child.displayName}}
            </div>
          </mat-list-item>
        </span>
      </mat-expansion-panel>
    </mat-accordion>
  </span>
  <span *ngIf="!item.children || item.children.length === 0">
    <mat-list-item routerLink="[item.route]">
      <!-- Entradas principales -->
      <div fxLayout="row" fxLayoutAlign="space-between center">
        <mat-icon>{{item.iconName}}</mat-icon>
        {{item.displayName}}
      </div>
    </mat-list-item>
  </span>
</span>

Have a look: https://stackblitz.com/edit/angular-side-nav-dynamic-expansive-menu

Upvotes: 8

ibenjelloun
ibenjelloun

Reputation: 7723

Using the mat-expansion-panel would be a good start. The default example uses accordion to have only one expanded panel, you can find an example of how to expand multiple panels at the same time here :

1- You should remove the mat-accordion to enable multiple expanded panels.

2- Use the expanded parameter to change multiple states at the same time.

Upvotes: 0

Related Questions