DEV Community

Cover image for Angular Material Router Outlet
Alex Patterson for CodingCatDev

Posted on • Originally published at ajonp.com on

Angular Material Router Outlet

Original Post: https://ajonp.com/courses/angularmaterial/angular-material-router-outlet/

Angular Material Router Outlet

This lesson will start from a new Angular Project and walk through how to use Angular Material Sidenav using the Angular Router with Named Outlets. This will be the begining of building a app for publishing book reviews.

🌎 Demo: https://ajonp-lesson-9.firebaseapp.com/

Lesson Steps

  1. Angular Material Router Outlet
  2. Create Angular Project
  3. Serve Angular Project
  4. Angular Modules
  5. Angular Material Sidenav
  6. Lazy Loading Books Feature Module
  7. Lazy Loading Welcome Feature Module
  8. Using Router Link for Navigation
  9. Toolbar Updates
  10. Book Drawer as Named Outlet
  11. Final Thoughts

Create Angular Project

If you have never used the Angular CLI you will want to checkout the main page to get started.

ng new angular-material-router-outlet
Enter fullscreen mode Exit fullscreen mode

Please choose Yes for routing and SCSS.
NG Choices

Add Material to Angular Project

Make sure you have changed to the correct directory cd angular-material-router-outlet

We will now run an Angular schematic command, you can think of this as a workflow to help get your project up and running quicker. There are several schematics available and I would recommend reading Angular Blog about schematics and Angular Console.

ng add @angular/material
Enter fullscreen mode Exit fullscreen mode

For the selections please choose custom, as we will add these in our next lesson.
Angular Material Selections

Open Project

Now you can open your new Angular project, if using VSCode

cd angular-material-router-outlet && code .
Enter fullscreen mode Exit fullscreen mode

You should see the base angular structure, including a routing module app-routing.module.ts
Structure for Angular Project

package.json

{
  "name": "angular-material-router-outlet",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~7.1.0",
    "@angular/cdk": "~7.2.1",
    "@angular/common": "~7.1.0",
    "@angular/compiler": "~7.1.0",
    "@angular/core": "~7.1.0",
    "@angular/forms": "~7.1.0",
    "@angular/material": "^7.2.1",
    "@angular/platform-browser": "~7.1.0",
    "@angular/platform-browser-dynamic": "~7.1.0",
    "@angular/router": "~7.1.0",
    "core-js": "^2.5.4",
    "hammerjs": "^2.0.8",
    "rxjs": "~6.3.3",
    "tslib": "^1.9.0",
    "zone.js": "~0.8.26"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.11.0",
    "@angular/cli": "~7.1.3",
    "@angular/compiler-cli": "~7.1.0",
    "@angular/language-service": "~7.1.0",
    "@types/node": "~8.9.4",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~3.1.1",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~7.0.0",
    "tslint": "~5.11.0",
    "typescript": "~3.1.6"
  }
}
Enter fullscreen mode Exit fullscreen mode

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularMaterialRouterOutlet</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
  <app-root></app-root>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Serve Angular Project

In order to preview this base setup you will need to run the angular serve command.

ng serve
Enter fullscreen mode Exit fullscreen mode

Now on http://localhost:4200 you will see the default Angular page displayed.

Angular Base page

Angular Modules

In general a module is a way of packaging up several Angular based files that logically belong together. Direct from Angular's docs, "NgModules are containers for a cohesive block of code dedicated to an application domain, a workflow, or a closely related set of capabilities."

We will use both NgModule and Component extensively through this lesson (and any Angular project).

Many tutorials will have you start putting everything into app.component*, I like to keep the main app clean and load as much as possible after lazy loading. Creating a modules folder keeps things a little more concise, but do what you prefer most.

Angular Material Sidenav

The Sidenav consists of three main html elements <mat-sidenav-container>, <mat-sidenav>, and <mat-sidenav-content>. Visually these can be represented like

Material Sidenav Layout

Creating Sidenav Module

To create a module we can leverage the Angular CLI and run

ng g m modules/sidenav
Enter fullscreen mode Exit fullscreen mode

Then we will need a component to display the Angular Material Sidenav.

ng g c modules/sidenav
Enter fullscreen mode Exit fullscreen mode

The output of these commands should give you this structure.

Sidenav Module Structure

You can then replace any contents in app.component.html with

<app-sidenav></app-sidenav>
Enter fullscreen mode Exit fullscreen mode

Sidenav will be the main entrypoint for the entire application, so it will need to reside directly in app.component. If you are asking yourself where did app-sidenav come from, Great question! This is defined in sidenav.component.ts in the @Component decorator, in the property selector: app-sidenav. Now at this point app.component.ts still does not now how to find sidenav.component.ts so we must export it from sidenav.module.ts and import it into app.module.ts.

sidenav.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SidenavComponent } from './sidenav.component';
import { MatSidenavModule, MatToolbarModule, MatIconModule, MatButtonModule, MatListModule} from '@angular/material';
import { RouterModule } from '@angular/router';

@NgModule({
  declarations: [SidenavComponent],
  imports: [
    CommonModule,
    MatSidenavModule,
    MatToolbarModule,
    MatIconModule,
    MatButtonModule,
    RouterModule,
    MatListModule
  ],
  exports: [SidenavComponent]
})
export class SidenavModule { }
Enter fullscreen mode Exit fullscreen mode

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SidenavModule } from './modules/sidenav/sidenav.module';
import { OverlayContainer } from '@angular/cdk/overlay';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    SidenavModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor(overlayContainer: OverlayContainer){
    overlayContainer.getContainerElement().classList.add('angular-material-router-app-theme');
  }
 }
Enter fullscreen mode Exit fullscreen mode

Now our app can find the Sidenav module and can use it to show any of the exported components.
If you open the preview again http://localhost:4200, you should now see "sidenav works!"

I would recommend committing at this point.

git add . && git commit -m "Initial sidenav"
Enter fullscreen mode Exit fullscreen mode

Update sidenav.component*

Now that we know our component can be seen as plain text lets start using the Angular Material Sidenav component for styling our app. First we will need to tell sidenav.module.ts that we need to include this new component, by adding it to our imports from @angular/material.

import { MatSidenavModule} from '@angular/material';
...
  imports: [
    CommonModule,
    MatSidenavModule,
...
Enter fullscreen mode Exit fullscreen mode

Now we can will update sidenav.component.html to include the sidenav elements.

<mat-sidenav-container>
  <mat-sidenav>drawer</mat-sidenav>
  <mat-sidenav-content>content</mat-sidenav-content>
</mat-sidenav-container>
Enter fullscreen mode Exit fullscreen mode

If you were to preview the page now you will only see "content" as the drawer is automatically hidden.

Update mat-sidenav element to have the drawer open and beside content.

<mat-sidenav opened=false mode="over">
...
Enter fullscreen mode Exit fullscreen mode

Now you can preview again http://localhost:4200.

Add MatToolbar

We can make our site look like most by adding a toolbar to the top

<mat-sidenav-container>
  <mat-sidenav opened=false mode="over" #snav>
  drawer
  </mat-sidenav>
  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <button
      type="button"
      aria-label="Toggle sidenav"
      mat-icon-button
      (click)="snavToggle(snav)"
    >
      <mat-icon>menu</mat-icon>
    </button>
   content
  </mat-sidenav-content>
</mat-sidenav-container>
Enter fullscreen mode Exit fullscreen mode

Because we have added three new Angular Material elements mat-toolbar, mat-icon-button and mat-icon to our component, we will need to let sidenav.component.ts know where they are defined, so you need to import them in sidenav.module.ts.

@NgModule({
  declarations: [SidenavComponent],
  imports: [
    CommonModule,
    MatSidenavModule,
    MatToolbarModule,
    MatIconModule,
    MatButtonModule,
    ...
Enter fullscreen mode Exit fullscreen mode

Add Angular Router Outlet

The main content of our app needs a place to end up, this is what Angular's router-outlet accompishes. It is a placeholder that takes the markup from another component and places it on the page. For our app this will be the main outlet that other child outlets will nest under.

...
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>
Enter fullscreen mode Exit fullscreen mode

Also remember to add RouterModule to sidenav.module so that Angular understands the element <router-outlet>.

@NgModule({
  declarations: [SidenavComponent],
  imports: [
    CommonModule,
    MatSidenavModule,
    MatToolbarModule,
    MatIconModule,
    MatButtonModule,
    RouterModule,
    MatListModule
  ],
  exports: [SidenavComponent]
})
Enter fullscreen mode Exit fullscreen mode

This is a visual representation of what is happening in our code so far, mat-sidenav-content->router-outlet is where the reaminder of our app will live.

Sidenav with Toolbar

Lazy Loading Books Feature Module

The first child route that we will setup is a book route. This will require us to create a new module and component. This time we will use an optional parameter --routing which will also create a routing module.

Create Book Modules

ng g m modules/books --routing
Enter fullscreen mode Exit fullscreen mode

Create Book Component

ng g c modules/books
Enter fullscreen mode Exit fullscreen mode

Update App routing

We now need to configure the router so that the books feature module can be accessed. So we will go back to app-routing.module.ts and add a new route with path books. There is a special way to load modules in a lazy fashion, meaning they were not downloaded when first accessing the app but when first accessing the route. You can read more about Lazy Loading Modules in the Angular Guide.

const routes: Routes = [
  {
    path: 'books',
    loadChildren: './modules/books/books.module#BooksModule'
  }
];
Enter fullscreen mode Exit fullscreen mode

App Routing Default Route

If someone enters the app without a specified path we need to redirect that request over to books so that content will show up correctly.

Add to constant routes.

  {
    path: '',
    redirectTo: '/books',
    pathMatch: 'full'
  }
Enter fullscreen mode Exit fullscreen mode

Update Books Feature Module Route

Now that we have told the app router about a feature module we need to make sure that feature module knows which component it should load, so we will add an empty path.

const routes: Routes = [
  {
    path: '',
    component: BooksComponent,
  }
]
Enter fullscreen mode Exit fullscreen mode

You should now see in the live preview http://localhost/books a message that says "books works!".

Lazy Loading Welcome Feature Module

Many sites will often have a welcome or home module that you will route your traffic to incase there are notifications, logins, or basic info requirements. So we will switch our base path over to this new feature module and leave books on a seperate path. This will be the same setup as our Books Module.

Create Welcome Modules

ng g m modules/welcome --routing
Enter fullscreen mode Exit fullscreen mode

Create Welcome Component

ng g c modules/welcome
Enter fullscreen mode Exit fullscreen mode

Update App routing

const routes: Routes = [
  {
    path: 'welcome',
    loadChildren: './modules/books/books.module#BooksModule'
  }
];
Enter fullscreen mode Exit fullscreen mode

App Routing Default Route

Change this redirect from books to Welcome.

app-routing.module.ts

const routes: Routes = [
  {
    path: 'welcome',
    loadChildren: './modules/welcome/welcome.module#WelcomeModule'
  },
  {
    path: 'books',
    loadChildren: './modules/books/books.module#BooksModule'
  },
  {
    path: '',
    redirectTo: '/welcome',
    pathMatch: 'full'
  }
];
Enter fullscreen mode Exit fullscreen mode

Update Welcome Feature Module Route

welcome-routing.module.ts

const routes: Routes = [
  {
    path: '',
    component: WelcomeComponent,
  }
]
Enter fullscreen mode Exit fullscreen mode

Using Router Link for Navigation

In order for us to navigate the site we need to add some navigational elements. Using an Angular Material List with a specific mat-nav-list element type is just what we need for our sidenav drawer.

...
  <mat-sidenav opened=false mode="over">
    <mat-nav-list>
      <mat-list-item>
        <h4 matLine routerLink="/welcome"
        [routerLinkActiveOptions]="{exact:true}"
        routerLinkActive="active-link">Home</h4>
      </mat-list-item>
      <mat-list-item>
        <h4 matLine routerLink="/books" routerLinkActive="active-link">Books</h4>
      </mat-list-item>
    </mat-nav-list>
  </mat-sidenav>
  ...
Enter fullscreen mode Exit fullscreen mode

Don't forget that you will now need to add RouterModule and MatListModule in your sidenav.module.ts imports.

sidenav.module.ts

@NgModule({
  declarations: [SidenavComponent],
  imports: [
    CommonModule,
    MatSidenavModule,
    MatToolbarModule,
    MatIconModule,
    MatButtonModule,
    RouterModule,
    MatListModule
  ],
  ...
Enter fullscreen mode Exit fullscreen mode

If you now preview http://localhost:4200 you will see in the sidenav you can click on Home or Books and the content will change to "welcome works!" and books works!" respectively.

Active Router Link

You can style your link to know which link you are currently using by adding the attribute routerLinkActive and passing a class. We have already assigned ours to active-link.

We can then add our style to sidenav.component.scss so that the active link changes to a bold blue.

.active-link {
  color: blue;
  font-weight: bold !important;
  border: none;
}
Enter fullscreen mode Exit fullscreen mode

Because we have our home (welcome) route path assigned to '/' if you preview now both Books and Home would be highlighed. By changing our routerlink to /welcome this issue will be resolved. In later lessons we will also discuss routerLinkOptions such as [routerLinkActiveOptions]="{exact:true}".

Toolbar Updates

In our mat-toolbar we placed a button that currently calls a function that has not yet been defined. We need to assign a variable called snav by using #snav in the element mat-sidenav.

Sidenav Toggle

sidenav.component.html

<mat-sidenav opened=false mode="over" #snav>
Enter fullscreen mode Exit fullscreen mode

We can then use this new variable and pass it on the button click output (click)="snavToggle(snav)"

sidenav.component.html

<button
  type="button"
  aria-label="Toggle sidenav"
  mat-icon-button
  (click)="snavToggle(snav)"
>
Enter fullscreen mode Exit fullscreen mode

Function for Toggle

Using our new snav reference we can just call the method that exists on this object, it will open or close the sidenav drawer.

sidenav.component.ts

snavToggle(snav) {
  snav.toggle();
}
Enter fullscreen mode Exit fullscreen mode

If you now preview http://localhost:4200 you will see that the toolbar hamburger (three horizontal lines) button will open and close the sidenav drawer.

Toolbar Title

We can also specify a title to allow our home routerlink to return home.

<a class="home-link" routerLink=".">{{ title }}</a>
Enter fullscreen mode Exit fullscreen mode

sidenav.component.ts

  title = 'Lesson 9 - Angular Material Router Outlet';
Enter fullscreen mode Exit fullscreen mode

Book Drawer as Named Outlet

Now that we have our book feature module all setup with working navigation and toolbar, we are going to add a named outlet for a drawer on this page.

Visually it will look like this
Drawer Layout
We will change our books.component.html from having text to including an Angular Material Drawer (mat-drawer). Remember now we have one router-outlet in our sidenav.component and two router-outlets in books.component, one named for the drawer and one for content.

Create Drawer Component

No routing needed for this module as it will be used only inside of our books module and not as a feature module.

module

ng g m modules/books/book-drawer
Enter fullscreen mode Exit fullscreen mode

component

ng g c modules/books/book-drawer
Enter fullscreen mode Exit fullscreen mode

Don't forget to export this component as it will be used in book-detail.

book-drawer.module.ts

...
@NgModule({
  declarations: [BookDrawerComponent],
  imports: [
    CommonModule
  ],
  exports: [
    BookDrawerComponent
  ]
...
Enter fullscreen mode Exit fullscreen mode

Add mat-drawer to Books

There are three parts to the drawer just like sidenav, this is because they are the same with sidenav having a few additional structural features.

Having attributes opened="true" will show the drawer on screen and having mode="side" will push the content to beside the drawer.

modules/books/books.component.html

<mat-drawer-container>
  <mat-drawer  opened="true" mode="side">
    <router-outlet name="book-drawer"></router-outlet>
  </mat-drawer>
  <mat-drawer-content>
    <router-outlet></router-outlet>
  </mat-drawer-content>
</mat-drawer-container>
Enter fullscreen mode Exit fullscreen mode

Remember to add MatSidenavModule to books.module.ts, or the mat-drawer element will not be recognized.

@NgModule({
  declarations: [BooksComponent],
  imports: [
    CommonModule,
    BooksRoutingModule,
    MatSidenavModule
  ]
})
Enter fullscreen mode Exit fullscreen mode

Create Book Detail Component

We will use this as an additional child feature route of books, so we need the router module.

module

ng g m modules/books/book-detail --routing
Enter fullscreen mode Exit fullscreen mode

component

ng g c modules/books/book-detail
Enter fullscreen mode Exit fullscreen mode

Update Books Routing

We no longer want just the BookComponent to load when the /books route is hit, we want it to load its children as well. We do this the same as we did with our app-routing.module.ts and we will lazy load it with loadChildren.

const routes: Routes = [
  {
    path: '',
    component: BooksComponent,
    children: [
      {
        path: '',
        loadChildren: './book-detail/book-detail.module#BookDetailModule'
      }
    ]
  }
];
Enter fullscreen mode Exit fullscreen mode

Update Book-Detail Routing with Named Outlet

Now that the books module knows to lazy load the book-detail module on its base path we need to update the route in book-detail to load its own component. This however will have a special route with an outlet for the drawer as well, this tells the router that it must use only this named route for its component.

So the router will load:

book-detail -> <router-outlet>

book-drawer -> <router-outlet name="book-drawer">

const routes: Routes = [
  {
    path: '',
    component: BookDetailComponent
  },
  {
    path: '',
    component: BookDrawerComponent,
    outlet: 'book-drawer'
  }
];
Enter fullscreen mode Exit fullscreen mode

If you now preview http://localhost:4200/books you will see in a drawer "book-drawer works!" and in the content area "book-detail works!".

Final Thoughts

The Angular Router is amazingly powerful, you can create sever nested routes, named routes, guarded routes...

If you cloned the final GitHub repo you will see some additional style updates, I will be covering those in the next Angular Material Themeing lesson.

Latest comments (1)

Collapse
 
gabrielvendrame profile image
gabrielvendrame

What if i don't want to show the header (for example on /login route) In this way theoretically i can't hide the navbar since the router outlet is on mat-sidenav-content, how can i solve this problem without creating a custom header?