16
votes

I am using angular 4 in combination with angular material.

I am using a sidenav container a router and a custom generated menu:

<mat-sidenav-container>
<mat-sidenav #sidenav>
    <app-menu></app-menu>
</mat-sidenav>
<router-outlet></router-outlet>
</mat-sidenav-container>

Inside the menu component I am using default angular 4 router links:

<ul>
  <li *ngFor="let hero of heroes">
    <a mat-button [routerLink]="['/hero/', hero.id]" queryParamsHandling="merge">
      {{hero.name}}
    </a>
  </li>
</ul>

What I would like to do is to close the Material sidenav when clicking on one of those router links.

I can assure you that without the click event routing works fine, it just does not close my sidenav, because the router-outlet is inside the side navigation.

However, when I add a (click)="sidenav.close()" to the a my full page suddenly refreshed, instead of just following the router link.

I have tried many things, but I can't seem to figure it out, hope someone can help!

8

8 Answers

33
votes

Here's a complete solution based off of Angular Material's schematics generated sidenav layout component. This answer improves on the previous ones by taking into consideration whether we are on a handset device or not, and making sure the sidenav only closes on a handset device.

A few points:

  1. This component was generated by using Angular Materials navigation schematics.
  2. I called my component my-menu however, you may use any other name.
  3. The sidenav will only close if we are not on a handset device, this is done using the rxjs operator withLatestFrom combined with the isHandset$ stream which was generated by the schematics.
import { Component, ViewChild } from '@angular/core';
import { BreakpointObserver, Breakpoints, BreakpointState } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
import { map, filter, withLatestFrom } from 'rxjs/operators';
import { Router, NavigationEnd } from '@angular/router';
import { MatSidenav } from '@angular/material';

@Component({
  selector: 'app-my-menu',
  templateUrl: './my-menu.component.html',
  styleUrls: ['./my-menu.component.css']
})
export class MyMenuComponent {
  @ViewChild('drawer') drawer: MatSidenav;

  isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset)
    .pipe(
      map(result => result.matches)
    );

  constructor(private breakpointObserver: BreakpointObserver,
    router: Router) {
    router.events.pipe(
      withLatestFrom(this.isHandset$),
      filter(([a, b]) => b && a instanceof NavigationEnd)
    ).subscribe(_ => this.drawer.close());
  }
}
17
votes

I created a service so that the sidenav could be controlled from various components. I followed this example.

Then it's a matter of watching the router for events and closing as needed. In app.component.ts:

this.router.events.subscribe(event => {
  // close sidenav on routing
  this.sidenavService.close();
});

This doesn't work if you want to close the sidenav on only specific links, but the service itself would facilitate that.

8
votes

You don't need to make a service for this. This is a simple solution.

<mat-sidenav-container [hasBackdrop]="true">
                <mat-sidenav #sidenav mode="side" class="sideNavigation">
                        <mat-nav-list>
                                <a mat-list-item routerLink="" (click)="sidenav.close()"> bar</a>
                                <a mat-list-item routerLink="/bar" (click)="sidenav.close()">foo</a>
                                <a mat-list-item routerLink="/foo" (click)="sidenav.close()">whatever</a>
                                <a mat-list-item routerLink="/something1" (click)="sidenav.close()">SomePage</a>
                                <a mat-list-item routerLink="/something2" (click)="sidenav.close()">Some Page</a>
                        </mat-nav-list>
                </mat-sidenav>
                <mat-sidenav-content>
                        <mat-toolbar color="accent">
                                <mat-icon (click)="sidenav.toggle()">menu</mat-icon> 
                                <div class="divider"></div>
                                <img class="img" src="../assets/img/logo.png" alt="logo" (click)="goHome()" />
                        </mat-toolbar>
                        <router-outlet></router-outlet>
                </mat-sidenav-content>
        </mat-sidenav-container>

All you do is add (click)="sidenav.close()" along with the routerLink="/whatever/path/here" works. This will close the sidenav on directing to a page.

5
votes

Inspired on @isherwood's answer I came up with a solution:

I created the following service:

import { Injectable } from '@angular/core';
import { MatSidenav } from '@angular/material';

@Injectable()
export class SidenavService {

  public sideNav:MatSidenav;
  constructor() { }
}

I changed my app component to set the sideNav property in the above service.

export class AppComponent implements OnInit {
  title = 'app';

  @ViewChild('sidenav') public sideNav:MatSidenav;
  constructor(private sidenavService: SidenavService) {      
  }

  ngOnInit() {
    this.sidenavService.sideNav = this.sideNav;
  }
}

Please note that it's very important to implement OnInit and to set the sidenavService.sideNav in the ngOnInit function.

You can then use your sideNavService anywhere you want. In my case I use it only in the components that I actually link in my side navigation menu.

Example:

 export class HeroComponent implements OnInit {

  constructor(private sidenavService: SidenavService) {      
  }

  ngOnInit() {
    this.sidenavService.sideNav.close();
  }
}

You'll probably want to do it on change of a parameter instead of on init, but you get the point.

3
votes

Just additional small info, if you want to close sidenav only on mobile viewport, just use isHandset$ function check for the drawer state before you call the function:

import { ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';

@ViewChild('snav', { static: false }) usuarioMenu: MatSidenav;

isHandset$: Observable<boolean> = 
   this.breakpointObserver.observe(Breakpoints.Handset)
    .pipe(
      map(result => result.matches),
      shareReplay()
    );

constructor(
    private breakpointObserver: BreakpointObserver) { }

  closeAllSidenav() {
    this.isHandset$.subscribe(isVisible => {
      if(isVisible) {
        this.myDrawer.close();
      }
    });
  }
3
votes

add (click)="sidenav.close()" to mat-sidenav

<mat-sidenav #sidenav mode="side" class="app-sidenav" (click)="sidenav.close()">
1
votes

I tested another solution in case it is necessary to call a method, it seemed easier and more practical using @ViewChild, I'll leave it here in case anyone wants to.

app.component.html:

<mat-sidenav #snav mode="over" fixedTopGap="56">
  <mat-nav-list class="pt-0">
    <a mat-list-item (click)="closeAllSidenav()">
      Navigation Item
    </a>
  </mat-nav-list>
</mat-sidenav>

app.component.ts:

import { ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';

@ViewChild('snav', { static: false }) usuarioMenu: MatSidenav;

closeAllSidenav() {
  this.usuarioMenu.close();
}
0
votes

another way to close sideNav by clicking element

component html

<mat-sidenav-container>
<mat-sidenav #sidenav>
    <li *ngFor="let hero of heroes">
    <a mat-list-item [routerLink]="['/hero/', hero.id]" 
 queryParamsHandling="merge">
      {{hero.name}}
    </a>
  </li>
</mat-sidenav>
<router-outlet></router-outlet>
</mat-sidenav-container>

component ts

import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements AfterViewInit {



  @ViewChild('sidenav') matSideNav: MatSidenav;

  constructor(
    private elementRef: ElementRef,
  ) { }


  ngAfterViewInit(): void {
    this.elementRef.nativeElement.querySelectorAll('.mat-list-item[href]')
      .forEach((el: HTMLElement) => {
        el.addEventListener('click', () => {
          this.matSideNav.close()
        })
      })
  }

}