Angular Material – Navbar with Responsive Side Menu

Photo by Terje Sollie from Pexels

Overview

This post is an example of how to build an Angular Material navigation bar with a responsive side menu drawer. The side menu will also highlight the options based on the Router Path.

Source Code

Node-Sass

Warning about the code from GitHub: You may need to run the following NPM commands if you get a node-sass incompatibility error.

npm uninstall node-sass
npm install node-sass@4.14.1

ng s -o

Create Angular Application and Install Material

ng new material-layout-app
ng add @angular/material

Add a couple of components such as home and profile.

ng g c components/home-page
ng g c components/profile

In the GitHub code example there is a material.module.ts file which has all of the imports for Material.
Remember when creating a new component you have to add ng g c component-name –module=app when using a separate module like material.module.ts. Click here to see an example of a separate module for material imports.

Layout Component

Generate a component for the layout.

ng g c material-layout

In the material-layout.component.ts file import the following libraries.

import { Component, OnInit, HostBinding } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { Router } from '@angular/router';
import { MatSidenav } from '@angular/material/sidenav';
import { MatSnackBar } from '@angular/material/snack-bar';

OptionalThis example includes using a snackbar for small alerts such as picking a menu item.

In the example from GitHub, there is an environment variable set to show the side menu (drawer) open or closed.

In the gitHub example there is a material.module.ts file which has all of the imports for Material.
Remember when creating a new component you have to add ng g c component-name –module=app when using a separate module like material,module.ts.

Layout Component – TypeScript

All of the TypeScript for the material-layout.component.ts file:

import { Component, OnInit, HostBinding } from '@angular/core';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { Router } from '@angular/router';
import { MatSidenav } from '@angular/material/sidenav';
import { MatSnackBar } from '@angular/material/snack-bar';

@Component({
  selector: 'material-layout',
  templateUrl: './material-layout.component.html',
  styleUrls: ['./material-layout.component.scss']
})
export class MaterialLayoutComponent implements OnInit {
  public loading: boolean;
  public isAuthenticated: boolean;
  public title: string;

  public isBypass: boolean;
  public mobile: boolean;
  public isMenuInitOpen: boolean;

  constructor(private breakpointObserver: BreakpointObserver,
              private router: Router,
              private _snackBar: MatSnackBar) { }

    private sidenav: MatSidenav;

    public isMenuOpen = true;
    public contentMargin = 240;

    get isHandset(): boolean {
      return this.breakpointObserver.isMatched(Breakpoints.Handset);
  }


  ngOnInit() {
    this.isMenuOpen = true;  // Open side menu by default
    this.title = 'Material Layout Demo';
  }

  ngDoCheck() {
      if (this.isHandset) {
         this.isMenuOpen = false;
      } else {
         this.isMenuOpen = true;
      }
  }

  public openSnackBar(msg: string): void {
    this._snackBar.open(msg, 'X', {
      duration: 2000,
      horizontalPosition: 'center',
      verticalPosition: 'top',
      panelClass: 'notif-error'
    });
  }

  public onSelectOption(option: any): void {
    const msg = `Chose option ${option}`;
    this.openSnackBar(msg);

    /* To route to another page from here */
    // this.router.navigate(['/home']);
  }
}

Mobile Device Detection

This following function automatically collapses the side menu (drawer) in a mobile device. The code below determines if this is a mobile device. Just look at breakpointObserver and you will get back a lot of use information about the device that is viewing your site.

get isHandset(): boolean {
  return this.breakpointObserver.isMatched(Breakpoints.Handset);
}

Add an ngDoCheck() to collapse or expand the side menu if resized.

ngDoCheck() {
    if (this.isHandset) {
      this.isMenuOpen = false;
    } else {
      this.isMenuOpen = true;
    }      
}

To highlight the menu items using the routerLinkActive property in the anchor tag.

<mat-nav-list >
<a [routerLink]="['/home']" mat-list-item href="#" routerLinkActive="active">
<mat-icon routerLinkActive="active-icon">home</mat-icon><span class="ml-2" > Welcome </span></a>
<a [routerLink]="['/profile']" mat-list-item href="#" routerLinkActive="active">
<mat-icon routerLinkActive="active-icon">settings</mat-icon>
<span class="ml-2" > Profile Settings</span></a>
</mat-nav-list>

The corresponding CSS that is used to change the link from black to dark red.


.active {
  font-weight: bold;
  background-color: #CFCFCF;
  color: darkred;
}

.active:active {
  font-weight: bold;
  background-color: lightpink;
}

Layout Component – CSS

Add the following to layout CSS material-layout.component.html component

.main {
  overflow-y: hidden;
  overflow-x: hidden;
}

.sidenav-container {
  height: 92%;
  position: relative;
}

.sidenav-content {
  width: 100%;
}

.sidenav {
  width: 250px;
}

.sidenav .mat-toolbar {
  background: #fff;
  color: #fff;
}

.toolbar-icon {
  padding: 0 0px;
}

.sidenavContainer {
  height: 100%;
}

.toolbar-spacer {
  flex: 1 1 auto;
}

.active {
  font-weight: bold;
  background-color: #CFCFCF;
  color: darkred;
}

.active:active {
  font-weight: bold;
  background-color: lightpink;
}

.active-icon {
  color: red;
}

Layout Component – HTML

Open material-layout.component.html and copy/paste the following:

<mat-toolbar color="primary">
  <mat-toolbar-row>

    <button mat-icon-button  (click)="drawer.toggle();"><mat-icon routerLinkActive="active-icon">menu</mat-icon></button>
    <span>{{title}}</span>

    <span class="toolbar-spacer "></span>

    </mat-toolbar-row>

  </mat-toolbar>

<mat-sidenav-container class="sidenav-container" >
  <mat-sidenav
  #drawer
  class="sidenav"
  [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
  [mode]="(isHandset$ | async) ? 'over' : 'side'"
  [opened]="isMenuOpen">

    <mat-nav-list >
      <a [routerLink]="['/home']" mat-list-item href="#" routerLinkActive="active"><mat-icon routerLinkActive="active-icon">home</mat-icon><span class="ml-2" > Welcome </span></a>
      <a [routerLink]="['/profile']" mat-list-item href="#" routerLinkActive="active"><mat-icon routerLinkActive="active-icon">settings</mat-icon><span class="ml-2" > Profile Settings</span></a>
    </mat-nav-list>

  </mat-sidenav>

  <mat-sidenav-content  [ngStyle]="{ 'margin-left.px': contentMargin }" >
    <div class="main">
      <router-outlet #outlet="outlet"></router-outlet>
    </div>

  </mat-sidenav-content>
</mat-sidenav-container>

The router <router-outlet> is wrapped with the Material side navigation tag. <mat-sidenav-container></mat-sidenav-container>. Since it is inside the container tags, the content will be responsive to a resize of the menu or change in the drawer state being open or closed.

<mat-sidenav-content  [ngStyle]="{ 'margin-left.px': contentMargin }" >
    <div class="main">
      <router-outlet #outlet="outlet"></router-outlet>
    </div>
</mat-sidenav-content>

Finally – Render the Layout in app.component.ts

Open app.component.html

Remove the html that is in the new Angular App and add the following

<app-material-layout></app-material-layout>

Optional – Adding a SnackBar to Display Chosen Menu Item

In the GitHub demo there are a couple of extras such as a drop down side menu on the upper right that triggers a Material Snackbar and has the router set up.

Import the snack bar and declare it in the constructor.

Import Libraries

import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';

Updates to the Constructor

Also add the router in addition to the snackbar and Breakpoint observer.

  constructor(private breakpointObserver: BreakpointObserver,
              private router: Router,
              private _snackBar: MatSnackBar) { }

Inside the material-layout.component.ts add the following below the ngDoCheck(),

The router has been disabled, but I left it there just in case you want to use the menu options to navigate to another page.

  public openSnackBar(msg: string): void {
    this._snackBar.open(msg, 'X', {
      duration: 2000,
      horizontalPosition: 'center',
      verticalPosition: 'top',
      panelClass: 'notif-error'
    });
  }

  public onSelectOption(option: any): void {
    const msg = `Chose option ${option}`;
    this.openSnackBar(msg);
    /* To route to another page from here */
    // this.router.navigate(['/home']);
  }

Updates to material-layout.component.html

The following html goes between the <mat-toolbar-row> tags after the <span>{{title}}</span>. The <span class=”toolbar-spacer “></span> uses CSS defined in the material-layout.component.scss file pushes everything after to the right side.

<span class="toolbar-spacer "></span>

    <button mat-icon-button [matMenuTriggerFor]="menu" aria-label="Example icon-button with a menu">
      <mat-icon>more_vert</mat-icon>
    </button>
    <mat-menu #menu="matMenu">
      <button mat-menu-item (click)="onSelectOption('1')">
        <mat-icon>wb_sunny</mat-icon>
        <span>Option 1</span>
      </button>
      <button mat-menu-item (click)="onSelectOption('2')">
        <mat-icon>lens</mat-icon>
        <span>Option 2</span>
      </button>
      <button mat-menu-item (click)="onSelectOption('3')">
        <mat-icon>brightness_3</mat-icon>
        <span>Option 3</span>
      </button>
    </mat-menu>

Adding the Routes

Setup the router in app-routing.module.ts.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './components/home-page/home-page.component';
import { ProfileComponent } from './components/profile/profile.component';

const routes: Routes = [
  {
    path: '',
    component: HomePageComponent
  },
  {
    path: 'home',
    component: HomePageComponent
  },
  {
    path: 'profile',
    component: ProfileComponent
  },

];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }