angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.36k stars 6.74k forks source link

[Sidenav] Open sidenav from another component #2936

Closed marianharbist closed 6 years ago

marianharbist commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

I guess he's got the following structure.

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 commented 7 years ago

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
DennisSmolek commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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

mrusful commented 7 years ago

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 commented 7 years ago

In my case i have structure

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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:

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 commented 7 years ago

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 commented 7 years ago

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.

dman777 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

That did it, thanks!

jelbourn commented 6 years ago

Closing this since I believe it is obsolete.

ghost commented 5 years ago

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[bot] commented 5 years ago

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.