Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Sidenav] Open sidenav from another component #2936

Closed
marianharbist opened this issue Feb 4, 2017 · 30 comments
Closed

[Sidenav] Open sidenav from another component #2936

marianharbist opened this issue Feb 4, 2017 · 30 comments
Assignees
Labels
feature This issue represents a new feature or feature request rather than a bug or bug fix needs: discussion Further discussion with the team is needed before proceeding P5 The team acknowledges the request but does not plan to address it, it remains open for discussion

Comments

@marianharbist
Copy link

Is it possible to have sidenav in separated component?

I have one component where is some content and button which if I click it opens the sidenav that is in another component (just sidenav is there). I tried that to implement with event emitter but not successfuly and I see that when sidenav is opened, it is showing under the layer of my content in first component. Maybe it is because the content is not in md-sidenav-content? I tried to set z-index but nothing happend. Is it possible? thanks

I need to have sidenav code separated in another component because it is my bachelors thesis and I need to have nice code. Thanks

@DennisSmolek
Copy link

Not sure about the Z-Indexing without seeing your code but my guess is they are on different z-planes.

To trigger you can call the sidenav explicitly (which I'm guessing you're doing) like this:

<md-sidenav-container class="example-container">
  <md-sidenav #sidenav class="example-sidenav">
    Jolly good!
  </md-sidenav>

  <div class="example-sidenav-content">
    <button md-button (click)="sidenav.open()">
      Open sidenav
    </button>
  </div>

</md-sidenav-container>

BUT You can also use that selector and the @ViewChild() decorator to access the element from your typescript code.
Before your constructor:
@ViewChild('sidenav') public myNav: MdSidenav;

then you can fire this.myNav.open() off all kinds of stuff, other buttons, services, etc.. LMK if that helps!

@theobalkwill
Copy link

@DennisSmolek Hi there, could you be a bit more precise on the usage on @ViewChild() ?
In my case, my and are in my 'app.component.html' template but I need to call sidenav.open() from my 'header.component.html' template which is called inside 'home.component.html' template (basically I used transclusion and ng-content to get the appropriate navbar content for each of my views). Hope that's clear enough.

So should I add a viewChild decorator to my app component and to my home component?

Thanks

@grizzm0
Copy link
Contributor

grizzm0 commented Feb 9, 2017

I guess he's got the following structure.

  • md-sidenav-container
    • custom-sidenav
      • md-sidenav
    • custom-header
      • md-toolbar

The "problem" here is that md-sidenav-container captures md-sidenav to ng-content. Hence his custom-sidenav ending up in md-sidenav-content messing up the backdrop. A possible "fix" would be to change to select="md-sidenav, [md-sidenav]". This way we could create sidenav components with the attribute md-sidenav that contains the actual sidenav.

@craig-o-curtis
Copy link

craig-o-curtis commented Mar 14, 2017

I'm got a similar problem, though not deeply nested

- md-sidenav-container ( main.component.ts / .html)
    - custom-header with nav toggle (header-menu.component.ts / .html)
    - custom sidenav component (sidenav.component.ts / .html)
        - md-sidenav
  • md-sidenav inside the sidenav.component has sidenav.open() and sidenav.close(), but erroring out since 'custom sidenav doesn't have property sidenav or sidenav.open"

@DennisSmolek
Copy link

DennisSmolek commented Mar 14, 2017

@theobalkwill

@DennisSmolek Hi there, could you be a bit more precise on the usage on @ViewChild() ?
In my case, my and are in my 'app.component.html' template but I need to call sidenav.open() from my 'header.component.html' template which is called inside 'home.component.html' template (basically I used transclusion and ng-content to get the appropriate navbar content for each of my views). Hope that's clear enough.

So should I add a viewChild decorator to my app component and to my home component?

Thanks

So a local variable allows you to attach to the component and issue commands to it but your sidenav is actually nested within ANOTHER component.
So you've got
Main Component
--sidenav
-- headerComponent---- buttonToToggleHeader

right?

What I would do is use @Output on headerComponent

So:

template: '<a (click)="navOpen()">Toggle Da Nav!</a>'

export class HeaderComponent {
  @Output() navToggle = new EventEmitter<boolean>();
  navOpen() {
    this.navToggle.emit(true);
  }
}

Then in mainComponent.html

<md-sidenav-container>
  <md-sidenav #mainNav>
    <!-- sidenav content -->
  </md-sidenav>

  <!-- primary content -->
</md-sidenav-container>
<headerComponent (navToggle)="mainNav.toggle()"></headerComponent>

Or if you want to have more drastic communication between components I prefer the service method

template: '<a (click)="toggleNav()">Toggle Da Nav!</a>'

export class HeaderComponent {
  constructor(public mySuperService:MySuperService) {
  
  }
  toggleNav() {
    this.mySuperService.sidenav.toggle();
  }
}

Then in mainComponent.html

<md-sidenav-container>
  <md-sidenav #mainNav>
    <!-- sidenav content -->
  </md-sidenav>

  <!-- primary content -->
</md-sidenav-container>
<headerComponent></headerComponent>

`mainComponent.ts`
@ViewChild('mainNav') public mainNav;
constructor(public mySuperService: MySuperService) {
    this.mySuperService.sidenav = this.mainNav;
}

`mySuperService`
@Injectable()
export class MySuperService {
    public sidenav: any;

}

There is some great reading here in the ngx on @ViewChild and here on component comms

@SirLants
Copy link

@DennisSmolek I'm a bit of a rookie here when it comes to constructors and ngOnInit(), I was getting an error until I added the "this.mySuperService.sidenav = this.mainNav" code into my ngOnInit instead of my constructor. Does ngOnInit() override predefined values that have been set in the constructor or something?

@DennisSmolek
Copy link

@DennisSmolek I'm a bit of a rookie here when it comes to constructors and ngOnInit(), I was getting an error until I added the "this.mySuperService.sidenav = this.mainNav" code into my ngOnInit instead of my constructor. Does ngOnInit() override predefined values that have been set in the constructor or something?

No, @Inject takes over a class constructor for most ng2 classes which means you can't really declare anything in the constructor. Plus ngOnInit()'s a lifecycle hook.
You could bind to any of the other events. It's standard practice now to use ngOnInit() where you would expect to do the actual "work" of a constructor.

The part that's confusing you I think is with visibility in the constructor.

When you define the visibility you remove the need to declare it before the constructor.
You could do this:

export class MyComponent {
    // vars
    private _MySuperService: MySuperService;

    constructor(mySuperService: MySuperService) {
        // without declaring in the constructor, mySuperService is only local, like this variable
        let localOnly = 'whatever';
        this._mySuperService = mySuperService;
    }

    otherFunction() {
        console.log(this._mySuperService.whatever);
     }
}

So thats a pain, Typescript lets you do this instead

export class MyComponent {

    constructor(private _mySuperService: MySuperService) {}

    otherFunction() {
        console.log(this._mySuperService.whatever);
     }
}

But if you do that, remember you've already declared it bound to this:

constructor(private _myService: MyService) {
    // THIS WILL FAIL AND THROW ERRORS
    let whatever = _myService.whatever;
    
    //This wont
    let whateverTwo = this._myService.whatever;
}

}

@tarlepp
Copy link

tarlepp commented Mar 23, 2017

Hmm, and if I have following structure on my app.component.html

<md-sidenav-container fxFlex fxFlexFill>
  <md-sidenav #sidenav>
    Drawer content
  </md-sidenav>

  <div fxLayout="column" fxLayoutAlign="space-between stretch" fxFlexFill>
    <header>
      <router-outlet name="header" ></router-outlet>
    </header>

    <article fxFlex>
      <router-outlet></router-outlet>
    </article>

    <footer>
      <router-outlet name="footer"></router-outlet>
    </footer>
  </div>
</md-sidenav-container>

And I want to toggle that sidenav on any component that is rendered inside header router-outlet?

@flamusdiu
Copy link

@tarlepp A service would work. The catch is you need to assign the element in ngAfterViewInit(). I just spent about an hour trying to figure out why nothing worked.

@tarlepp
Copy link

tarlepp commented Apr 21, 2017

@flamusdiu yeah got that working on my simple app almost like that with following setup:

app.component.html
app.component.ts
header.component.html
header.component.ts
sidenav.service.ts

I hope this helps others too

@lllbllllb
Copy link

lllbllllb commented Jun 12, 2017

@tarlepp thanks a lot!
It's really cool, flexible solution for any case

@mrusful
Copy link

mrusful commented Jun 22, 2017

Another one solution for discussion.

sidenav-layout is some abstraction above of MdSidenavModule that can be deleted.

Actually i implemented this functionality with Directive because I thought that it is possible to add more then one sidenav component on a page but it is not.

I wanted to pass sidenav's template reference into Directive with @Input property and decide which sidenav should be toggled. But since only one sidenav component can be added on a page this is not so valuable.

One thing what i don't like is that service is singleton for whole app. Maybe it is possible to add providers only within components.

@vovikdrg
Copy link

vovikdrg commented Jul 19, 2017

In my case i have structure

  • selectorComponent
    • countrySelectComponent
      • CountryFormComponent
        • Side nav
    • CitySelectComponent
      • CityFormComponent
        • Side nav

And few more forms, idea is that as soon as i drom FormComponent to any place it has everything i need. Now it means that md-sidenav must me child of md-sidenav-container

@ravivit9
Copy link

ravivit9 commented Aug 13, 2017

Hi, When I implement @flamusdiu solution, I am getting undefined for sidenav in this.sidenav.toggle(isOpen) of sidenav.service.ts

Despite setting the sidenav in app component it's undefined when clicking the burger menu from header component.

I believe the issue is with the below line.
public sidenav: MdSidenav;
if we declare sidenav without an explicit default type, the "this" keyword not taking the sidenav varaible.

If we declare in the following way this keywork works but unable to find toggle() function in it.
public sidenav: any = MdSidenav;

@codemental
Copy link

codemental commented Aug 19, 2017

Hi guys,

Here is an example of how to pass the MdSidenav reference from a parent component to its children using @Input().

Parent component HTML:

<md-sidenav-container style="height: 100%; width: auto">
 
 <md-sidenav #searchSidenav style="width: 500px;">  <!-- create the reference to the sidenav -->
    <my-search [sidenavRef]="searchSidenav"></my-search>  <!-- pass the reference to the 'sidenavRef' input of the child element -->
 </md-sidenav>

  <div class="row" style="height: 100%">

    <div class="col-xs-1 col-md-1 col-lg-1 left-column">
      
      ...

    </div>
    <div class="col-xs-11 col-md-11 col-lg-11">
      <router-outlet></router-outlet>
    </div>
  </div>
</md-sidenav-container>

SearchComponent HTML:

<div class="col">
  <div class="row">
      ...
  </div>
  <div class="row">
    <div class="col">
      <button md-mini-fab
              (click)="sidenavRef.close()">  <!-- use the 'sidenavRef' variable inside the SearchComponent.ts -->
        <md-icon>arrow_back</md-icon>
      </button>
    </div>
    <div class="col">
      <form [formGroup]="searchForm">
        <md-input-container floatPlaceholder="never"
                            style="width:100%">
          <input mdInput
                 formControlName="search"
                 placeholder="{{'HEADER.SEARCH' | translate}}">
        </md-input-container>
      </form>
    </div>
  </div>
</div>

SearchComponent.ts:

export class SearchComponent {

  @Input() private sidenavRef: MdSidenav;

   ...
}

The SearchComponent has a variable that holds the reference to the MdSidenav from the parent. It gets the reference through the @Input() parameter inside the parent HTML. You can basically apply this to any number of child elements and they will get the same instance. One to rule them all :)
No extra service or complicated event handling. I hope this helps.

@dushkostanoeski
Copy link

dushkostanoeski commented Sep 7, 2017

I get a this.layoutService.rightMenu.toggle is not a function error

I'm using the following code:

The Layout Service

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

@Injectable()
export class LayoutService {
    leftMenu: MdSidenav;
    rightMenu: MdSidenav;
}

The layout component

<app-header></app-header> // got two buttons that toggle the sidenavs
<md-sidenav-container class="sidenav-container">
    <router-outlet></router-outlet>   // I load a different left menu depending on the route
    <app-right-menu></app-right-menu> // the right menu
    <app-footer></app-footer>
</md-sidenav-container>
<app-spinner></app-spinner>

The right menu html and component

<md-sidenav #rightMenu mode="side" opened="true" position="end">
    <md-tab-group>
       // iterating trough the tabs
    </md-tab-group>
</md-sidenav>

@Component({
    selector: 'app-right-menu',
    templateUrl: 'right-menu.component.html'
})
export class RightMenuComponent implements AfterViewInit {

    @ViewChild('rightMenu')
    rightMenu: MdSidenav;

    constructor(public layoutService: LayoutService) {
    }

    ngAfterViewInit(): void {
        console.log(this.rightMenu);
        this.layoutService.rightMenu = this.rightMenu;
    } 
}

In the console I get an ElementRef {nativeElement: md-sidenav}, not a MdSidenav object. and I think that that is the problem. Any ideas why?

@dushkostanoeski
Copy link

The sidenav component is in my shared module, but I fetch and use the sidenav in the main app module. Can't remember which one (I'm pretty sure it was the appmodule), but one of the modules didn't implement the MdSidenavModule and that was causing all the trouble.

If the sidenav is created in one module, but manipulated in another, both should implement the MdSidenavModule.

@tsaarikivi
Copy link

In my opinion the sidenav is hard to use from external components.

Md-sidenav-container is a weird strategy. Why is the side-nav not with a fixed/absolute property or something but with a parent container? Am I missing something cool?

Also the opened property does not work as expected.

Here is a good implementation which you can stick anywhere without any need of parent components. https://material-ui-1dab0.firebaseapp.com/demos/drawers
Notice the "onRequestClose" and "open" properties.

This can actually be hooked to a state management system:

  • set open => open=true
  • set close => open=false
  • if onRequestClose => open=false

Wanted outcome:
The sidenav component should be extractable 100% like the component architecture strategy states:

app.component.html:
app-topbar></app-topbar
app-sidenav></app-sidenav
router-outlet></router-outlet

NOT:
<sidenav-container

app-topbar></app-topbar
sidenav></sidenav
router-outlet></router-outlet

sidenav-container>

@walakulu
Copy link

walakulu commented Nov 2, 2017

Step 01:In your Sub component,put the custom event as below,

<button mat-raised-button color="accent" (click)="navOpen()">

Step 02:Change your typescript file as fallows,

import {Component} from '@angular/core';
import {EventEmitter} from '@angular/core';
@component({
selector:'sub-component',
styleUrls: ['./sub.component.css'],
templateUrl: './sub.component.html',
outputs:['navToggle']
})

export class SubComponent{
navToggle=new EventEmitter();

navOpen(){
    this.navToggle.emit(true);
}

}

Step 03: In your main component you can put ,

<sub-component (navToggle)="sidenav.open()">

(NOTE:Don't forget to add "#sidenav" to your mat-sidenav .)
Then it should work.

@tsaarikivi
Copy link

True, true. And it could be done through a service. A service can implement global state through Observable, Subject, BehaviorSubject or redux etc. I guess the missing piece would be the onClose callback.

@mmalerba mmalerba added feature This issue represents a new feature or feature request rather than a bug or bug fix P5 The team acknowledges the request but does not plan to address it, it remains open for discussion labels Nov 22, 2017
@dman777
Copy link

dman777 commented Dec 29, 2017

@DennisSmolek

Before your constructor:
@ViewChild('sidenav') public myNav: MdSidenav;

How did you know the type was MDSidenav? Is that in documentation?

EDIT:

actually, I am getting error TS2304: Cannot find name 'MdSidenav'. when I try that. Any ideas why?

@walakulu
Copy link

@dman777 Yes.You need to read the API documentation.My above answer related to angular material 2.I think you are using angular 1.x.
You can find material 2, Sidenav API from here. https://material.angular.io/components/sidenav/api.

@dman777
Copy link

dman777 commented Dec 30, 2017

@walakulu No, I am using Angular 5 and Angular Material 2. I can grab the element fine if I don't give it the MdSidenav type using this:

export class AppComponent implements AfterViewInit {
   @ViewChild('snav') public snav;

@EdricChan03
Copy link
Contributor

@dman777 It's a bad idea to mix Angular 5 and Angular Material 2 together. Either downgrade your Angular version or upgrade your Angular version.

The reason why you can't get the MdSidenav type might be because you didn't import it at all! Add this line at the top of your file:

import { MdSidenav } from '@angular/material';

@dman777
Copy link

dman777 commented Jan 2, 2018

@Chan4077 Thank you for the tip, but import { MdSidenav } from '@angular/material'; produced a error:
ERROR in src/app/app.component.ts(11,10): error TS2305: Module '"/home/one/github/diabetes-charts/node_modules/@angular/material/material"' has no exported member 'MdSidenav'.

Since Angular 5 is stable and production ready, I am pretty sure Angular Material supports it.

@walakulu I looked at the documentation and it saids:

Exported as: matSidenav

so, I tried:

export class AppComponent implements AfterViewInit {
   @ViewChild('snav') public snav: matSidenav;

but got ERROR in src/app/app.component.ts(19,36): error TS2304: Cannot find name 'matSidenav'.

Again, it works if I do not specify a type on @ViewChild('snav') public snav;, but I would like to learn how to make the type work if anyone could help, please.

@Bodeclas
Copy link

Bodeclas commented Jan 3, 2018

Hi @dman777, you need to import correctly, I recommend you use the import of your code editor, in my case I use Visual Studio Code.

Your typescript file

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

export class AppComponent implements AfterViewInit {
@ViewChild('snav') public snav: MatSidenav;

your template

 <mat-sidenav #snav>
 </mat-sidenav>

@dman777
Copy link

dman777 commented Jan 3, 2018

That did it, thanks!

@jelbourn
Copy link
Member

jelbourn commented Oct 5, 2018

Closing this since I believe it is obsolete.

@jelbourn jelbourn closed this as completed Oct 5, 2018
@ghost
Copy link

ghost commented Dec 18, 2018

And how I should unit test this component that has a sidenav as @input

location-toolbar.component.ts

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


@Component({
  selector: 'app-location-toolbar',
  templateUrl: './location-toolbar.component.html',
  styleUrls: ['./location-toolbar.component.scss']
})
export class LocationToolbarComponent implements OnInit {

  // Input
  @Input() sidenav: MatSidenav;
  @Input() hideButtons?: boolean;

  // Output
  @Output() onchange: EventEmitter<any> = new EventEmitter();

  constructor() { this.hideButtons = false; }

  ngOnInit() {}

}

location-toolbar.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

// Modules
import { MaterialModule } from '@app-global-modules/material.module';

// Components
import { LocationToolbarComponent } from './location-toolbar.component';

describe('LocationToolbarComponent', () => {
  let component: LocationToolbarComponent;
  let fixture: ComponentFixture<LocationToolbarComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [MaterialModule],
      declarations: [ LocationToolbarComponent ]
    })
    .compileComponents();
  }));

  beforeEach(async() => {
    fixture = TestBed.createComponent(LocationToolbarComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();

    //
    component.sidenav = null;
    component.hideButtons = false;

    component.ngOnInit();
    await fixture.whenStable();
    fixture.detectChanges();
  });


  fit('should create', () => {
    expect(component).toBeTruthy();
  });

});

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 10, 2019
@mmalerba mmalerba added the needs: discussion Further discussion with the team is needed before proceeding label Mar 3, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature This issue represents a new feature or feature request rather than a bug or bug fix needs: discussion Further discussion with the team is needed before proceeding P5 The team acknowledges the request but does not plan to address it, it remains open for discussion
Projects
None yet
Development

No branches or pull requests