Creating Navigation page
Learn how to create dynamic links with Angular that navigate to different parts of an application.
Overview
In this part, we will:
- Use
routerLink
to create navigation links - Use
routerLinkActive
to highlight current navigation
Problem
We want to create Home and Restaurant links in our app navigation that can be used to navigate between pages. We also want those links to change color if they are for the current page or not.
What you need to know
To solve this, you will need to know how to:
- How to use the
routerLink
directive - How to use the
routerLinkActive
directive
RouterLink
You may have noticed the links in the routing examples to switch between views. They use the routerLink directive. The routerLink
takes a property of a path that can be static or built dynamically based on properties on the component.
Static routerLink Segments
The following is an example of a hard coded path:
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/router@7.2.0/bundles/router.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<script src="//unpkg.com/mock-url@^5.0.0" type="module"></script>
<mock-url pushstate:from="true"></mock-url>
<base href="/">
<my-app></my-app>
<script type="typescript">
const { Component, NgModule, VERSION } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;
@Component({
selector: 'my-app',
template: `
<ul class="nav">
<li routerLinkActive="active">
<a routerLink="/about">About</a>
</li>
</ul>
<router-outlet></router-outlet>
`
})
class AppComponent {
constructor() {
}
}
@Component({
selector: 'about-component',
template: `
<p>An about component!</p>
`
})
class AboutComponent {
constructor() {
}
}
@Component({
selector: 'home-component',
template: `
<p>A home component!</p>
`
})
class HomeComponent {
constructor() {
}
}
const routes: Routes = [
{ path: 'about', component: AboutComponent },
{ path: '', component: HomeComponent },
{ path: '**', component: HomeComponent }
]
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
class AppRoutingModule { }
@NgModule({
declarations: [AppComponent, AboutComponent, HomeComponent],
imports: [
BrowserModule,
CommonModule,
AppRoutingModule,
],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
Dynamic routerLink Segments
The following is an example of a path being built from different pieces of data. In reusable components we’ll often want to dynamically create paths based on a piece of data’s unique property values.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/router@7.2.0/bundles/router.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<script src="//unpkg.com/mock-url@^5.0.0" type="module"></script>
<mock-url pushstate:from="true"></mock-url>
<base href="/">
<my-app></my-app>
<script type="typescript">
const { Component, NgModule, VERSION } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;
@Component({
selector: 'my-app',
template: `
<ul class="nav">
<li routerLinkActive="active">
<a routerLink="/about">About</a>
</li>
<li routerLinkActive="active">
<a [routerLink]="['/users', userId]">User 1</a>
</li>
</ul>
<router-outlet></router-outlet>
`
})
class AppComponent {
userId: number = 1;
constructor() {
}
}
@Component({
selector: 'about-component',
template: `
<p>An about component!</p>
`
})
class AboutComponent {
constructor() {
}
}
@Component({
selector: 'home-component',
template: `
<p>A home component!</p>
`
})
class HomeComponent {
constructor() {
}
}
@Component({
selector: 'user-component',
template: `
<p>A user component!</p>
`
})
class UserComponent {
constructor() {
}
}
const routes: Routes = [
{ path: 'about', component: AboutComponent },
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UserComponent },
{ path: '**', component: HomeComponent }
]
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
class AppRoutingModule { }
@NgModule({
declarations: [AppComponent, AboutComponent, HomeComponent, UserComponent],
imports: [
BrowserModule,
CommonModule,
AppRoutingModule,
],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
RouterLinkActive
The routerLinkActive directive lets you add a CSS class to an element when the link's route becomes active. Angular looks at the path to determine if the route is active and will return true if any of the path matches, meaning when a path contains a "child" segment the route active status will still return true. If exact specificity is needed, [routerLinkActiveOptions]="{exact: true}"
can be used.
Adding a Basic Active Class
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/router@7.2.0/bundles/router.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<script src="//unpkg.com/mock-url@^5.0.0" type="module"></script>
<mock-url pushstate:from="true"></mock-url>
<base href="/">
<my-app></my-app>
<script type="typescript">
const { Component, NgModule, VERSION } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;
@Component({
selector: 'my-app',
template: `
<ul class="nav">
<li>
<a routerLink="/about" routerLinkActive="my-active-class">About</a>
</li>
<li>
<a [routerLink]="['/users', userId]">User 1</a>
</li>
</ul>
<router-outlet></router-outlet>
`
})
class AppComponent {
userId: number = 1;
constructor() {
}
}
@Component({
selector: 'about-component',
template: `
<p>An about component!</p>
`
})
class AboutComponent {
constructor() {
}
}
@Component({
selector: 'home-component',
template: `
<p>A home component!</p>
`
})
class HomeComponent {
constructor() {
}
}
@Component({
selector: 'user-component',
template: `
<p>A user component!</p>
`
})
class UserComponent {
constructor() {
}
}
const routes: Routes = [
{ path: 'about', component: AboutComponent },
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UserComponent },
{ path: '**', component: HomeComponent }
]
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
class AppRoutingModule { }
@NgModule({
declarations: [AppComponent, AboutComponent, HomeComponent, UserComponent],
imports: [
BrowserModule,
CommonModule,
AppRoutingModule,
],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
<style>
.my-active-class {
background: pink;
}
</style>
Adding an Active Class with Options
Using {exact: true}
will only set the class if the path match is exactly equal. {exact: true}
can also be applied to parents of routerLink
s.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/router@7.2.0/bundles/router.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<script src="//unpkg.com/mock-url@^5.0.0" type="module"></script>
<mock-url pushstate:from="true"></mock-url>
<base href="/">
<my-app></my-app>
<script type="typescript">
const { Component, NgModule, VERSION } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;
@Component({
selector: 'my-app',
template: `
<ul class="nav">
<li routerLinkActive="my-active-class">
<a routerLink="/about">About</a>
</li>
<li routerLinkActive="my-active-class" [routerLinkActiveOptions]="{exact: true}">
<a [routerLink]="['/users', userId]">User 1</a>
</li>
<li routerLinkActive="my-active-class" [routerLinkActiveOptions]="{exact: true}">
<ul>
<li><a routerLink="/user/1">User 1</a></li>
<li><a routerLink="/user/2">User 2</a></li>
</ul>
</li>
</ul>
<router-outlet></router-outlet>
`
})
class AppComponent {
userId = 1;
constructor() {
}
}
@Component({
selector: 'about-component',
template: `
<p>An about component!</p>
`
})
class AboutComponent {
constructor() {
}
}
@Component({
selector: 'home-component',
template: `
<p>A home component!</p>
`
})
class HomeComponent {
constructor() {
}
}
@Component({
selector: 'user-component',
template: `
<p>A user component!</p>
`
})
class UserComponent {
constructor() {
}
}
const routes: Routes = [
{ path: 'about', component: AboutComponent },
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UserComponent },
{ path: '**', component: HomeComponent }
]
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
class AppRoutingModule { }
@NgModule({
declarations: [AppComponent, AboutComponent, HomeComponent, UserComponent],
imports: [
BrowserModule,
CommonModule,
AppRoutingModule,
],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
<style>
.my-active-class {
background: pink;
}
</style>
Adding an Active Class with Template Variable
RouterLinkActive
can also be used to set template variables to check the active status of the route. In this example, we’re creating a template variable userLink
to represent our route and its active state for logic in our template.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/router@7.2.0/bundles/router.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<script src="//unpkg.com/mock-url@^5.0.0" type="module"></script>
<mock-url pushstate:from="true"></mock-url>
<base href="/">
<my-app></my-app>
<script type="typescript">
const { Component, NgModule, VERSION } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;
@Component({
selector: 'my-app',
template: `
<ul class="nav">
<li routerLinkActive="my-active-class">
<a routerLink="/about">About</a>
</li>
<li routerLinkActive #userLink="routerLinkActive"
[ngClass]="{'my-active-class' : userLink.isActive}">
<a [routerLink]="['/users', userId]">User 1</a>
</li>
</ul>
<p *ngIf="userLink.isActive">The users/1 route is active. 💖💖💖</p>
<router-outlet></router-outlet>
`
})
class AppComponent {
userId: number = 1;
constructor() {
}
}
@Component({
selector: 'about-component',
template: `
<p>An about component!</p>
`
})
class AboutComponent {
constructor() {
}
}
@Component({
selector: 'home-component',
template: `
<p>A home component!</p>
`
})
class HomeComponent {
constructor() {
}
}
@Component({
selector: 'user-component',
template: `
<p>A user component!</p>
`
})
class UserComponent {
constructor() {
}
}
const routes: Routes = [
{ path: 'about', component: AboutComponent },
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UserComponent },
{ path: '**', component: HomeComponent }
]
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
class AppRoutingModule { }
@NgModule({
declarations: [AppComponent, AboutComponent, HomeComponent, UserComponent],
imports: [
BrowserModule,
CommonModule,
AppRoutingModule,
],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
<style>
.my-active-class {
background: pink;
}
</style>
Technical requirements
Create a navigation menu that will route to our home and restaurant components when clicked, as well as show an active
class on the li
element when the route is active.
Setup
Make your changes in the src/app/app.component.html file. The markup structure should look like this:
<header>
<nav>
<h1>place-my-order.com</h1>
<ul>
<li>
<a>Home</a>
</li>
<li>
<a>Restaurants</a>
</li>
</ul>
</nav>
</header>
<router-outlet />
How to verify your solution is correct
You will know you’ve completed the exercise correctly when you can click the nav items to see the UI change and see the active class on the current nav item.
✏️ Update the spec file src/app/app.component.spec.ts to be:
import { Location } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
fakeAsync,
flush,
TestBed,
tick,
} from '@angular/core/testing';
import { Router } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { RestaurantComponent } from './restaurant/restaurant.component';
describe('AppComponent', () => {
let fixture: ComponentFixture<AppComponent>;
let location: Location;
let router: Router;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppRoutingModule],
declarations: [AppComponent, HomeComponent, RestaurantComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
location = TestBed.inject(Location);
router = TestBed.inject(Router);
});
it('should create the app', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'place-my-order'`, () => {
const app = fixture.componentInstance;
expect(app.title).toEqual('place-my-order');
});
it('should render title in a h1 tag', () => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain(
'place-my-order.com'
);
});
it('should render the HomeComponent with router navigates to "/" path', fakeAsync(() => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
router.navigate(['']).then(() => {
expect(location.path()).toBe('');
expect(compiled.querySelector('pmo-home')).not.toBe(null);
});
}));
it('should render the RestaurantsComponent with router navigates to "/restaurants" path', fakeAsync(() => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
router.navigate(['restaurants']).then(() => {
expect(location.path()).toBe('/restaurants');
expect(compiled.querySelector('pmo-restaurant')).not.toBe(null);
});
}));
it('should have the home navigation link href set to "/"', () => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const homeLink = compiled.querySelector('li a');
const href = homeLink?.getAttribute('href');
expect(href).toEqual('/');
});
it('should have the restaurants navigation link href set to "/restaurants"', () => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const restaurantsLink = compiled.querySelector('li:nth-child(2) a');
const href = restaurantsLink?.getAttribute('href');
expect(href).toEqual('/restaurants');
});
it('should make the home navigation link class active when the router navigates to "/" path', fakeAsync(() => {
const compiled = fixture.nativeElement as HTMLElement;
router.navigate(['']);
fixture.detectChanges();
tick();
fixture.detectChanges();
const homeLinkLi = compiled.querySelector('li');
expect(homeLinkLi?.classList).toContain('active');
expect(compiled.querySelectorAll('.active').length).toBe(1);
flush();
}));
it('should make the restaurants navigation link class active when the router navigates to "/restaurants" path', fakeAsync(() => {
const compiled = fixture.nativeElement as HTMLElement;
router.navigate(['restaurants']);
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(location.path()).toBe('/restaurants');
const restaurantsLinkLi = compiled.querySelector('li:nth-child(2)');
expect(restaurantsLinkLi?.classList).toContain('active');
expect(compiled.querySelectorAll('.active').length).toBe(1);
flush();
}));
});
Solution
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/app/app.component.html to:<header>
<nav>
<h1>place-my-order.com</h1>
<ul>
<li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
<a routerLink="/">Home</a>
</li>
<li [ngClass]="{ active: rla.isActive }">
<a routerLink="/restaurants" routerLinkActive #rla="routerLinkActive">
Restaurants
</a>
</li>
</ul>
</nav>
</header>
<router-outlet />