Open/Close sidenav from another component

AngularTypescriptAngular Material

Angular Problem Overview


I use angular (latest version) and angular material.

There are 3 components:

  • header.component, in which there is a control button for right-sidenav
  • rigth-sidenav.component, in which is the right-sidenav
  • sidenav.component (this is the left main menu), this component calls header.component, right-sidenav.component and content

How to open / close sidenav from another component ? (in my case, the button is in header.component).

Tried the following option (but got the error: TypeError: this.RightSidenavComponent.rightSidenav is undefined):

header.component.html

<button mat-button (click)="toggleRightSidenav()">Toggle side nav</button>

header.component.ts

import { Component } from '@angular/core';
import { RightSidenavComponent } from '../right-sidenav/right-sidenav.component';
@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss']
})
export class HeaderComponent {

	constructor(public RightSidenavComponent:RightSidenavComponent) {	}
	toggleRightSidenav() {
		this.RightSidenavComponent.rightSidenav.toggle();
	}
	
}

sidenav.component.html

<mat-sidenav-container class="sidenav-container">
	<mat-sidenav #sidenav mode="side" opened="true" class="sidenav"
				 [fixedInViewport]="true"> Sidenav	</mat-sidenav>
	<mat-sidenav-content>
		<app-header></app-header>
		<router-outlet></router-outlet>
		<app-right-sidenav #rSidenav></app-right-sidenav>
	</mat-sidenav-content>
</mat-sidenav-container>

sidenav.component.ts

import { Component, Directive, ViewChild } from '@angular/core';
import { RightSidenavComponent } from '../right-sidenav/right-sidenav.component';

@Component({
  selector: 'app-sidenav',
  templateUrl: './sidenav.component.html',
  styleUrls: ['./sidenav.component.scss']
})

export class SidenavComponent {
	
	@ViewChild('rSidenav') public rSidenav;
	constructor(public RightSidenavComponent: RightSidenavComponent) {
		this.RightSidenavComponent.rightSidenav = this.rSidenav;
	}
	
}

right-sidenav.component.html

<mat-sidenav #rightSidenav mode="side" opened="true" class="rightSidenav"
			 [fixedInViewport]="true" [fixedTopGap]="250">
	Sidenav
</mat-sidenav>

right-sidenav.component.ts

import { Component, Injectable } from '@angular/core';

@Component({
  selector: 'app-right-sidenav',
  templateUrl: './right-sidenav.component.html',
  styleUrls: ['./right-sidenav.component.scss']
})

@Injectable()
export class RightSidenavComponent {
	
	public rightSidenav: any;

	constructor() { }

}

Non-working sample with code stackblitz

Angular Solutions


Solution 1 - Angular

I had the same problem using. I resolved it like this.

SidenavService

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

@Injectable()
export class SidenavService {
    private sidenav: MatSidenav;


    public setSidenav(sidenav: MatSidenav) {
        this.sidenav = sidenav;
    }

    public open() {
        return this.sidenav.open();
    }


    public close() {
        return this.sidenav.close();
    }

    public toggle(): void {
    this.sidenav.toggle();
   }

Your Component

constructor(
private sidenav: SidenavService) { }

toggleRightSidenav() {
   this.sidenav.toggle();
}

> Bind your html toggle() based on your requirement.

App component.

@ViewChild('sidenav') public sidenav: MatSidenav;

constructor(private sidenavService: SidenavService) {
}

ngAfterViewInit(): void {
  this.sidenavService.setSidenav(this.sidenav);
}

App Module

providers: [YourServices , SidenavService],

Working sample with code stackblitz

Angular 9+ Update

Per this answer on SO, "Components can no longer be imported through @angular/material. Use the individual secondary entry-points, such as @angular/material/button." As such, make sure to import MatSidenav like so:

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

Solution 2 - Angular

I used @Input() inputSideNav: MatSideNav in parent\ another component to pass the sideNav object as target property from child component. It works as expected. By the way, I liked the service implementation by @Eldho :)

  1. It is simple to plugin in your current implementation
  2. It is scalable to as many sideNav or other UI controls passed as reference into the component definition
  3. No cluttering in code and plain as it looks.
  4. Vote up if you get what we mean :)

Child.component.html

<app-layout-header [inputSideNav]="sideNav"></app-layout-header>
<mat-sidenav-container>
  <mat-sidenav #sideNav (click)="sideNav.toggle()" mode="side">
    <a routerLink="/">List Products</a>
  </mat-sidenav>
  <mat-sidenav-content>
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>

layout-header.component.html

<section>
  <mat-toolbar>
    <span (click)="inputSideNav.toggle()">Menu</span>
  </mat-toolbar>
</section>

layout-header.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { MatSidenav } from '@angular/material';

@Component({
  selector: 'app-layout-header',
  templateUrl: './layout-header.component.html',
  styleUrls: ['./layout-header.component.css']
})
export class LayoutHeaderComponent implements OnInit {
  @Input() inputSideNav: MatSidenav;

  constructor() { }

  ngOnInit() {
  }
}

Solution 3 - Angular

If you are working in Angular 9+ Remember add param of static: true in @ViewChild like:

@ViewChild('rightSidenav', {static: true}) sidenav: MatSidenav;

You can see my code working on Angular 9 here:

https://stackblitz.com/edit/angular-kwfinn-matsidenav-angular9

Solution 4 - Angular

For an easier solution just use an @ouput() event emitter.

In your parent component

 <mat-sidenav
    #sidenav
    class="sidenav"
    fixedInViewport
    [opened]="opened"
    [mode]="mode"
  >
// catch the emitted output from child component 
<childcomponent (sidenav)="sidenav.toggle()"></childcomponent>
....
</mat-sidenav>

child component

<mat-toolbar>
 <div>
  <button mat-button (click)="toggle()">
   <mat-icon>menu</mat-icon>
  </button>
 </div>
</mat-toolbar>

child component.ts

import {Component, EventEmitter, Output} from '@angular/core';

@Component({
  selector: 'childcomponent',
  templateUrl: './childcomponent.component.html',
  styleUrls: ['./childcomponent.component.scss'],
})
export class ChildComponent {
 @Output() sidenav: EventEmitter<any> = new EventEmitter();

toggle() {
 this.sidenav.emit();
 }
}

Solution 5 - Angular

Though people above has already answered, but I believe my solution is more simple when I solved for myself.

You need to have a service in middle to support both components.

Let's say, such service is NavService, below code goes into NavService:

public toggleNav: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

  public toggle(): void {
    this.toggleNav.next(!this.toggleNav.value);
  }

Then you have a component from where you would like to open or close your SideNav. In my case, I created the button in the Navbar as:

button *ngIf="this.loginService.tokenSubject.value"  mat-mini-fab color="warn" (click)="onToggle()"><mat-icon>menu</mat-icon></button>

Then in the SideNavComponent that you would have created for SideNav, put this in the mat-sidenav tag:

[opened]="this.sideNavService.toggleNav.value"

Of course, I injected the NavService as sideNavService in the constructor of this component.

Solution 6 - Angular

First of all, you don't need to toggle the right sidenav in the sidenav component. Your toggle button is in the header component, so you can do this in your header component.

header.component.ts

import { Component } from '@angular/core';
import { RightSidenavComponent } from '../right-sidenav/right-sidenav.component';
@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss']
})
export class HeaderComponent {
  rightSidenav: boolean;
  constructor(public RightSidenavComponent:RightSidenavComponent) {	}

  toggleRightSidenav() {
    this.rightSidenav = !this.rightSidenav;
  }
}

And in your header.component.html, use this code:

<app-right-sidenav *ngIf="rightSideNav"></app-right-sidenav>

This will make your work easy.

Solution 7 - Angular

Just to add to Eldho's answer, you can disable animations for a true hiding effect.

    <mat-sidenav #leftbar opened mode="side" class="ng-sidenav" [@.disabled]="true">

Solution 8 - Angular

to expanded on eldho service response I had an error when clicking this.sidenav is undefined' I fixed it by making sure my Side nav was tagged with #sidenav to match the selector on the app.component.ts file.

Also mat-drawer-container and mat-drawer seems to be the new name for sidenav

<mat-drawer-container autosize>
   <mat-drawer #sidenav class="example-sidenav" mode="side" position="end">
    
   </mat-drawer>

   <mat-drawer-content autosize="true">
   
   </mat-drawer-content>
</mat-drawer-container>

the app.component.ts

@ViewChild('sidenav') public sidenav: MatSidenav;

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDenisView Question on Stackoverflow
Solution 1 - AngularEldhoView Answer on Stackoverflow
Solution 2 - AngularAshokan SivapragasamView Answer on Stackoverflow
Solution 3 - AngularAntonio MolinaView Answer on Stackoverflow
Solution 4 - AngularDuncan FaulknerView Answer on Stackoverflow
Solution 5 - AngularImran FaruqiView Answer on Stackoverflow
Solution 6 - AngularAarshView Answer on Stackoverflow
Solution 7 - AngularJonathanView Answer on Stackoverflow
Solution 8 - AngularMatthew BeckerView Answer on Stackoverflow