58

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

Mel
  • 5,837
  • 10
  • 37
  • 42
Denis
  • 583
  • 1
  • 4
  • 7
  • in right-sidenav.component.ts try changing `public rightSidenav: any; ` to `@ViewChild('rightSidenav') public rightSidenav: any;` – Dhyey Jan 03 '18 at 08:02
  • I think if you implement like this you need only one component for your right and left side nav (side nav) – Eldho Jan 03 '18 at 11:23
  • i would also suggest you to format your question. The code snippet only works if its pure html , java script or css . check if its works in angular 2. So i could suggest you to remove and have proper formatting. – Eldho Jan 03 '18 at 11:24

8 Answers8

106

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';
Kurtis Jungersen
  • 2,204
  • 1
  • 26
  • 31
Eldho
  • 7,795
  • 5
  • 40
  • 77
  • Sorry. Did not quite understand. SidenavService is needed to operate the right-sidenav from header.component? – Denis Jan 03 '18 at 20:34
  • Sorry denis i missed to update the set side nav part; i have updated the code to include. The code is manuly typed from phne so please excuse for typo. – Eldho Jan 03 '18 at 20:47
  • @Denis I have updated the answer. Please take a look & let me know ur feedback. If you have any suggestion to improve this or any new ideas happy to learn. – Eldho Jan 04 '18 at 08:56
  • I do not know much Angular, so I did not succeed. Error: StaticInjectorError[SidenavService] – Denis Jan 04 '18 at 11:46
  • 1
    you have to add the service in module, @Denis I have updated the `app.module.ts` where you need to register the every service. Try now – Eldho Jan 04 '18 at 11:46
  • really, I forgot about it, but now another problem.If I add your code "Your Component" in the component where the control button is located, there is an error when clicking 'this.sidenav is undefined'. If I add your code "Your Component" in the component where is located right-sidenav: "TypeError: _co.toggleRightSidenav is not a function" – Denis Jan 04 '18 at 12:04
  • You dont need to add "Your Component" in to the code. Just use the `toggle()` in your header component. – Eldho Jan 04 '18 at 12:07
  • You `toggleRightSidenav()` in your header component. Replace with the `sideNavservice.toggle()` it will work – Eldho Jan 04 '18 at 12:08
  • Sorry, I do not understand how it works. Inserted and now the next error: TypeError: sidenav_service_1.SidenavService.toggle is not a function. – Denis Jan 04 '18 at 12:42
  • what is this `sidenav_service_1`? . – Eldho Jan 04 '18 at 12:44
  • @ViewChild('sidenav') public sidenav: MatSidenav; in appComponent, but which one sidenav? I just have a two sidenav – Denis Jan 04 '18 at 12:44
  • give a unique name for your sidenav1 like `@ViewChild('sidenav1')`. Try one for now. – Eldho Jan 04 '18 at 12:45
  • yes, I did so. "what is this sidenav_service_1?" - it appeared after I toggleRightSidenav() { this.sidenav.toggle(); } replaced on toggleRightSidenav() { SidenavService.toggle(); } – Denis Jan 04 '18 at 12:52
  • 3
    Can you create a project here https://stackblitz.com/. So may be i can help you by editing it – Eldho Jan 04 '18 at 12:53
  • 1
    https://stackblitz.com/edit/angular-tku8zp please look this. I had implemented the basic way you can tweak it – Eldho Jan 04 '18 at 13:45
  • 1
    Please mark as answer if its helped you. I would attach the sample to the answer. – Eldho Jan 04 '18 at 14:15
  • If you could add your sample to question it will be easy for other people – Eldho Jan 04 '18 at 14:17
  • How would you go about unit testing this by creating a mock MatSidenav? – bmd May 03 '18 at 13:28
  • I've used type assertion and a spy object : spyobject service["sidenav"] = > jasmine.createSpyObj("sidenav", ["open", "close", "toggle"]); – bmd May 03 '18 at 13:39
  • I'm not sure about the unit test. But ideally your spyobject should able to detect the changes when you toggle from the component. I haven't done unit test in ang. – Eldho May 03 '18 at 14:01
  • 1
    Thanks, Eldho. That's correct. I assigned a spy object to sidenav property of the SidenavService class. Thanks for your initial answer too - it was exactly what I was after. – bmd May 03 '18 at 14:24
  • Great solution! – Nathan Beck Nov 02 '18 at 18:26
  • @NathanBeck thanks if you have any suggestions to improve pls let me know – Eldho Nov 02 '18 at 18:28
  • 2
    The only one that comes to mind is using `ngAfterViewInit()` instead of `ngOnInit()` - from this article https://blog.angular-university.io/angular-viewchild/ initializing the viewChild ensures "the references injected by @ViewChild are present". – Nathan Beck Nov 02 '18 at 18:31
  • @Eldho What is if i have 5 Sidenavs in different components? isnt there any way without registering something in app.component.ts? – Budi Aug 14 '19 at 19:15
  • @Budi How do you plan to use the sidenav components – Eldho Aug 15 '19 at 06:20
  • I have alot `side-nav-container``s in my app... maybe 8? And all of them contains a sidenav. i need to trigger them from everywhere... I struggle for years with sidenav toggleing... I cant write in every component 10 lines or more of code just for a simple toggle... – Budi Aug 16 '19 at 07:52
  • Can you try to create a global side nav service and register all in one service and use it. Like rightsidenav.toggle() – Eldho Aug 16 '19 at 10:13
  • 3
    As @NathanBeck mentions, if you get 'thissidenav is undefined' try using ngAfterViewInit instead of ngOnInit. This worked for me. – tif Feb 18 '20 at 12:22
  • @tif i have updated the answer too `ngAfterViewInit` – Eldho Feb 18 '20 at 12:50
37

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() {
  }
}
Ashokan Sivapragasam
  • 2,033
  • 2
  • 18
  • 39
  • 3
    I think this solution is simpler – Harrison O Nov 14 '19 at 13:20
  • 1
    I liked this solution but it does not work with my app component structure. The header component and sidenav component are sibling components, i.e. I call both app-header and app-sidenav under my app.component. This means the '#sidenav', which is inside my app-sidenave, cannot be passed to my app-header, any ideas? – tif Feb 18 '20 at 08:45
  • @tif, I could not imagine your case. If possible, please post your code to stackblitz. – Ashokan Sivapragasam Feb 18 '20 at 14:46
  • @tif, It looks like you fixed that problem. Could you please share your experience? – Ashokan Sivapragasam Feb 19 '20 at 06:08
  • My case is like this: My app-component looks like this: As you can see, 'sideNav is not exposed from app-sidenav. I actually solved using a service (from the answer marked for this question). Thanks for replying – tif Feb 20 '20 at 12:10
  • @tif, but as you see this solution is exporting the #variable to parent component. Apparently, IMO, it is supposed to be available for siblings as well. – Ashokan Sivapragasam Feb 20 '20 at 15:11
  • I've implemented this idea in Angular8, which works as I'm not using sibling components. But I am running into an ExpressionChangedAfterItHasBeenCheckedError. Previous: 'sidenav: undefined', current value: 'sidenav: [object Object]'. Was this an issue for you @AshokanSivapragasam? Usually I'd deal with this in the component class, but as it's injected in the template I'm not sure where or how to address it. – nospi Feb 28 '20 at 02:42
  • @nospi, could you please post your code to stackblitz? – Ashokan Sivapragasam Feb 29 '20 at 06:49
  • 1
    @AshokanSivapragasam I'm unable to reproduce the error in a simplified StackBlitz so it must be something I've introduced. I'll post back if I find the cause. Cheers! – nospi Mar 01 '20 at 12:42
  • 1
    @AshokanSivapragasam Found it: I was passing a ViewChild reference via the component controller, instead of the #variable directly through the template. – nospi Mar 01 '20 at 12:48
  • 1
    If this style is applied, it is simpler to implement without altering your design – Zephania Mwando Jun 15 '20 at 07:48
  • 1
    In angular10+ I get "sideNav has no initializer and is not definitely assigned in the constructor."Anyway to initialize the sideNav? – wwjdm Apr 26 '21 at 16:53
  • I found this article. Please check the solutions provided here and see if it helps by changing the severity of TS Lint. https://stackoverflow.com/questions/49699067/property-has-no-initializer-and-is-not-definitely-assigned-in-the-construc – Ashokan Sivapragasam Apr 27 '21 at 07:08
  • 1
    SIMPLE AND SMART SOLUTION – Swapnil Dalvi Aug 09 '21 at 14:32
  • 1
    @wwjdm Latest version of typescript has 'strictPropertyInitialization' enabled by default. You can either disable it or add an ! as postfix to the variable name. For the example provided in the answer, it would be @Input() inputSideNav!: MatSidenav . Refer https://stackoverflow.com/questions/49699067/property-has-no-initializer-and-is-not-definitely-assigned-in-the-construc – daedalus_hamlet Aug 17 '21 at 10:24
9

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

5

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();
 }
}
Duncan Faulkner
  • 124
  • 1
  • 3
1

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.

Imran Faruqi
  • 663
  • 9
  • 19
  • 1
    Although this works, a BS should really be used publish to many (potential) subscribers. Since there is only ever one sidenav, having a common layout service with explicit open/close methods may be better than a BS that will only be subscribe to once. – steve Nov 28 '22 at 17:38
0

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.

Edric
  • 24,639
  • 13
  • 81
  • 91
Aarsh
  • 2,555
  • 1
  • 14
  • 32
  • 1
    One thing to keep in mind is that with this solution the markup for the sidenav will not be rendered. This can impact SEO for the site, since the code for the navigation will not be found by the search enginges. – tif Feb 18 '20 at 08:41
0

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">
Jonathan
  • 3,893
  • 5
  • 46
  • 77
0

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;