Adding Routing page
Learn how to add basic routing to components in an Angular application.
Overview
In this part, we will:
- Add two routes to our generated routing module
- Hook the router outlet into our markup
- Test that new routes are working
Problem
We need to set up routes for the home view and restaurant view.
Technical Requirements
Create routes for the HomeComponent
and RestaurantComponent
. When the route is ''
, the HomeComponent
should display, and when the route is /restaurants
the RestaurantComponent
should display. These changes should be made in src/app/app-routing.module.ts.
Notice that you will be able to click the Choose a Restaurant button at the end of this tutorial:
Setup
<router-outlet>
, which handles routing to a component based on a url, was added to our src/app/app.component.html file when we first generated our app and answered yes
to the routing question. But since that time, we added components to that view. Let's remove those components because <router-outlet>
will handle showing
those components going forward.
✏️ Update src/app/app.component.html to:
<h1>Place My Order App: Coming Soon!</h1>
<router-outlet></router-outlet>
How to Verify Your Solution is Correct
If you have completed the exercise successfully you should be able to see the home component when the app loads, and the restaurant component when you navigate to localhost:4200/restaurants. You may have noticed the routerLink
attribute on the <a>
tag in our home component markup. This one of the ways we link to specific routes in our app. When you click that link, you should see the restaurants component.
<a class="btn" routerLink="/restaurants" role="button"> Choose a Restaurant </a>
✏️ 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, TestBed } 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>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppRoutingModule],
declarations: [AppComponent, HomeComponent, RestaurantComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
});
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 App: Coming Soon!'
);
});
it('should render the HomeComponent with router navigates to "/" path', fakeAsync(() => {
const location: Location = TestBed.inject(Location);
const router: Router = TestBed.inject(Router);
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(() => {
const location: Location = TestBed.inject(Location);
const router: Router = TestBed.inject(Router);
const compiled = fixture.nativeElement as HTMLElement;
router.navigate(['restaurants']).then(() => {
expect(location.path()).toBe('/restaurants');
expect(compiled.querySelector('pmo-restaurant')).not.toBe(null);
});
}));
});
If you've implemented the solution correctly, when you run
npm run test
all tests will pass!
What You Need to Know
Router Outlet
RouterOutlet
is an Angular directive that is a placeholder for content that is filled when the route changes. In this example the HomeComponent
template or the AboutComponent
template are shown below the <router-outlet>
element as the route changes.
<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="" >Home</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 }
]
@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>
Router
To be able to navigate between different views in our app, we can take advantage of Angular's built-in routing module. We already told Angular we'd like to set up routing, so it generated src/app/app-routing.module.ts
for us and included it in our root module. src/app/app-routing.module.ts
currently looks like:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [];
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
The router module takes an array of routes we can generate in a few different ways that will render content in the router-outlet
directive.
Setting Paths to Components
The following example will render the AboutComponent in the router-outlet when the path is /about
:
<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="" >Home</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 }
]
@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>
Using Wildcards
The next example uses the wildcard path, which will render the PageNotFoundComponent when any unregistered route is hit:
<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">
// app.js
const { Component, NgModule, VERSION } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;
@Component({
selector: 'my-app',
template: `
<p>Look at me!</p>
<ul class="nav">
<li routerLinkActive="active">
<a routerLink="/about" >About</a>
</li>
<li routerLinkActive="active">
<a routerLink="/merp">/merp - Not an actual route</a>
</li>
<li routerLinkActive="active">
<a routerLink="/blarg">/blarg - Another not actual route</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: 'unfound-component',
template: `
<p>These are not the components you are looking for.</p>
`
})
class PageNotFoundComponent {
constructor() {
}
}
const routes: Routes = [
{ path: 'about', component: AboutComponent },
{ path: '', redirectTo: '/about', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
class AppRoutingModule { }
@NgModule({
declarations: [AppComponent, AboutComponent, PageNotFoundComponent],
imports: [
BrowserModule,
CommonModule,
AppRoutingModule
],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
Redirecting Routes
This example shows one route redirecting to another:
<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">
// app.js
const { Component, NgModule, VERSION } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;
@Component({
selector: 'my-app',
template: `
<p>Look at me!</p>
<ul class="nav">
<li routerLinkActive="active">
<a routerLink="/about" >About</a>
</li>
<li routerLinkActive="active">
<a routerLink="/merp">Not an actual route</a>
</li>
<li routerLinkActive="active">
<a routerLink="/blarg">Another not actual route</a>
</li>
<li routerLinkActive="active">
<a routerLink="boop">/boop - I will reroute to /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: 'unfound-component',
template: `
<p>These are not the components you are looking for.</p>
`
})
class PageNotFoundComponent {
constructor() {
}
}
const routes: Routes = [
{ path: 'about', component: AboutComponent },
{ path: 'boop', redirectTo: '/about', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
class AppRoutingModule { }
@NgModule({
declarations: [AppComponent, AboutComponent, PageNotFoundComponent],
imports: [
BrowserModule,
CommonModule,
AppRoutingModule
],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
Setting Paths to Modules
As our applications grow, it doesn't make sense to load all the code at once. Thanks to lazyloading, we can wait to render modules until a specific route requiring them is hit.
<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">
// app.js
const { Component, NgModule, VERSION } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
@Component({
selector: 'my-app',
template: `
<p>Look at me!</p>
<ul class="nav">
<li routerLinkActive="active">
<a routerLink="/about" >About</a>
</li>
<li routerLinkActive="active">
<a routerLink="boop">/boop - I will reroute to /about</a>
</li>
<li routerLinkActive="active">
<a routerLink="/mymodule">/mymodule - I will load the child module</a>
</li>
</ul>
<router-outlet></router-outlet>
`
})
class AppComponent {
constructor() {
}
}
@Component({
selector: 'about-component',
template: `
<p>A very sweet component!</p>
`
})
class AboutComponent {
constructor() {
}
}
@Component({
selector: 'unfound-component',
template: `
<p>These are not the components you are looking for.</p>
`
})
class PageNotFoundComponent {
constructor() {
}
}
@Component({
selector: 'child-component',
template: `
<p>I'm in a child module!</p>
`
})
class ChildComponent {
constructor() {
console.log('loading child component');
}
}
const childRoutes: Routes = [
{
path: '',
component: ChildComponent
}
];
@NgModule({
imports: [RouterModule.forChild(childRoutes)],
exports: [RouterModule]
})
class ChildRoutingModule { }
@NgModule({
imports: [
CommonModule,
ChildRoutingModule
],
declarations: [ChildComponent],
bootstrap: [ChildComponent],
providers: []
})
class ChildModule {
constructor() {
console.log('loading child module');
}
}
platformBrowserDynamic()
.bootstrapModule(ChildModule)
.catch(err => console.error(err));
const routes: Routes = [
{ path: 'about', component: AboutComponent },
{ path: 'boop', redirectTo: '/about', pathMatch: 'full' },
/*
loadChildren would normally be a result of an import Promise,
but we used a function for demonstration purposes and to enable it to work on codepen
Correct: { path: 'mymodule', loadChildren: () => import('./child/child.module').then(m => m.ChildModule) },
*/
{ path: 'mymodule', loadChildren: () => ChildModule},
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
class AppRoutingModule { }
@NgModule({
declarations: [AppComponent, AboutComponent, PageNotFoundComponent],
imports: [
BrowserModule,
CommonModule,
AppRoutingModule
],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
<base-href>
In our index.html file, the angular cli included <base href="/>
. This isn't an Angular specific feature and you can read more about it here, but it's important to know this is how the Angular router will know how to compose URLs - the value in the href
attribute specifies the base URL for all relative URLs contained in the app. If you'd like to serve your app from a different directory (wherever the index.html will be served from) or have a specific hosting URL that your app will be deployed at, you will need to change the base href
to match.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PlaceMyOrder</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<pmo-root></pmo-root>
</body>
</html>
Solution
Click to see the solution
✏️ Update src/app/app-routing.module.ts to:import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { RestaurantComponent } from './restaurant/restaurant.component';
const routes: Routes = [
{
path: '',
component: HomeComponent,
},
{
path: 'restaurants',
component: RestaurantComponent,
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}