Denis
Denis

Reputation: 613

Open/Close sidenav from another component

I use angular (latest version) and angular material.

There are 3 components:

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

Upvotes: 61

Views: 99083

Answers (10)

elobuho
elobuho

Reputation: 131

Angular 17 version ...but the idea is for basically any previous one

  1. Set a template reference to the sidenav component
    <mat-sidenav-container>
      <mat-sidenav #sidenav mode="side">I'm a sidenav</mat-sidenav>
      <mat-sidenav-content>Some content</mat-sidenav-content>
    </mat-sidenav-container>
  1. Expose a toggle method
@Component({
  selector: 'elo-admin',
  standalone: true,
  imports: [CommonModule, MatSidenavModule, MatFormFieldModule, MatSelectModule, MatButtonModule],
  templateUrl: './admin.component.html',
  styleUrl: './admin.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdminComponent {
  @ViewChild('sidenav') sidenav!: MatSidenav;

  private cdr = inject(ChangeDetectorRef);

  toggle() {
    this.sidenav.toggle();
    this.cdr.detectChanges();
  }

}
  1. Place the nav component into parent, set a reference, get access to the public method of the referenced component
<mat-toolbar color="primary">
  <h1>{{title}}</h1>
  <button (click)="admin.toggle()">toggle</button>
</mat-toolbar>
<div class="content-wrapper">
  <elo-admin #admin />
  <mat-card>
    <mat-card-content>
      <router-outlet />
    </mat-card-content>
  </mat-card>
</div>

NOTE: I've been stuck for a while due to change detection, you need to trigger it manually after toggle().

Upvotes: 0

Nasser Alkhateeb
Nasser Alkhateeb

Reputation: 133

For a versatile and adaptable solution applicable to various scenarios, including but not limited to side-nav event management, is using NgEventBus.

Steps:

  1. Component That Has Access to the SideNav:

    This component is responsible for toggling the MatSidenav based on a specific event.

    import { Component, ViewChild } from '@angular/core';
    import { MatSidenav } from '@angular/material/sidenav';
    import { NgEventBus } from 'ng-event-bus';
    
    @Component({
      ...
    })
    export class ComponentThatHasAccessToTheSideNav {
      @ViewChild(MatSidenav) matSidenav: MatSidenav;
    
      constructor(private eventBus: NgEventBus) {}
    
      ngOnInit() {
        this.eventBus.on('appBarMenuButtonClickedEvent').subscribe(() => this.matSidenav.toggle());
      }
    }
    
  2. Component That Has the Toggle Button:

    This component triggers the event that the other component listens for, leading to the toggling of the MatSidenav.

    import { Component } from '@angular/core';
    import { NgEventBus } from 'ng-event-bus';
    
    @Component({
       ...
    })
    export class ComponentThatHasTheToggleButton {
      constructor(private eventBus: NgEventBus) {}
    
      menuClick() {
        this.eventBus.cast('appBarMenuButtonClickedEvent');
      }
    }
    

By adopting this approach, there's no need to create a new service for each similar use case. Moreover, multiple subscribers can listen to specific events.

Upvotes: 0

Duncan Faulkner
Duncan Faulkner

Reputation: 134

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();
 }
}

Upvotes: 6

Ashokan Sivapragasam
Ashokan Sivapragasam

Reputation: 2199

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() {
  }
}

Upvotes: 37

Matthew Becker
Matthew Becker

Reputation: 69

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;

Upvotes: 0

Imran Faruqi
Imran Faruqi

Reputation: 743

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.

Upvotes: 1

Antonio Molina
Antonio Molina

Reputation: 99

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

Upvotes: 9

Jonathan
Jonathan

Reputation: 4709

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">

Upvotes: 0

Eldho
Eldho

Reputation: 8301

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';

Upvotes: 107

Aarsh
Aarsh

Reputation: 2605

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.

Upvotes: 0

Related Questions