Skip to content

Navigating and Passing Data Between Pages

We’ve already seen how to set up routes to navigate to different parts of our application:

import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'home',
loadComponent: () =>
import('./home/home.component').then((m) => m.HomeComponent),
},
{
path: 'settings',
loadComponent: () =>
import('./settings/settings.component').then((m) => m.SettingsComponent),
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
];

We also know how to activate different components in the application using the URL, e.g:

http://localhost:4200/home

or

http://localhost:4200/settings

But, we don’t typically navigate by typing addresses into the URL bar. In this lesson, we are going to look at a few more routing and navigation concerns, specifically:

  • How to navigate between various routes
  • How to pass data between pages

Let’s start with some basic navigation.

Basic Navigation

As I just mentioned, we don’t want our users to have to manually change the URL. We want them to be able to click buttons, or swipe, or just be automatically navigated to where they need to go.

We can do this primarily in two ways:

  • If we want the user to just click on something and then navigate to some page we can use routerLink in the template
  • If we need to navigate programatically, then we can use router.navigate() in the class

Let’s take a closer look at both. First, routerLink:

import { Component } from '@angular/core';
import { RouterLink, RouterModule } from '@angular/router';
@Component({
selector: 'app-home',
template: `
<p>I am the home component</p>
<a routerLink="/settings">Go to settings</a>
`,
imports: [RouterLink],
})
export class HomeComponent {}

If we use a / at the beginning of our route it will look up the route from the root of the application. If we use ./ then it will look up the route relative to the currently active route. If we did this instead:

<p>I am the home page</p>
<a routerLink="./settings">Go to settings</a>

It would actually try to find the route home/settings which does not exist. However, if do have child routes within individual modules the relative path can make navigating easier.

Now let’s see how to navigate programatically. Let’s say that instead of navigating directly to some page, we want to first perform some action. Maybe we want to perform an authentication and then if it succeeds we want to redirect the user to another page. In that case, we can inject the Router and navigate using that:

import { Component, inject } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
@Component({
selector: 'app-home',
template: `
<p>I am the home page</p>
<button (click)="handleLogin()">Login</button>
`,
})
export class HomeComponent {
private router = inject(Router);
handleLogin() {
// Do the login and then:
this.router.navigate(['settings']);
}
}

Like with our previous example, we can also use a relative route if we want. To do that, we can supply NavigationExtras to the navigate command:

import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-home',
template: `
<p>I am the home page</p>
<button (click)="handleLogin()">Login</button>
`,
})
export class HomeComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
handleLogin() {
// Do the login and then:
this.router.navigate(['settings'], { relativeTo: this.route });
}
}

Notice that we have also injected ActivatedRoute to get information about the current route. Again home/settings does not exist so this will fail.

Using Route Params

Sometimes we don’t want to just navigate to a route, we also want to pass it some information. One way we can do this is with route parameters. The basic idea is reasonably simple. When defining the route, we use this special syntax with a colon:

{
path: 'movie/:id',
loadComponent: () =>
import('./movie/movie.component').then((m) => m.MovieComponent),
},

Now we can activate this route like this:

<a routerLink="/movie/2">Dune</a>

Then, on our Movie page, we will be able to access that value. We will discuss exactly how in the next section.

Usually, we will be passing in some dynamic id or other value to a route rather than just a static value like above. If you want, you can just drop this in using standard interpolation:

<a routerLink="/movie/{{ movie.id }}">Dune</a>

Or you can make use of the extra features in routerLink to construct it:

<a [routerLink]="['/movie', movie.id]">Dune</a>

We can do something similar when using the navigate method:

this.router.navigate(['movie', this.movie.id]);

Passing Data Between Pages

We saw in the previous section that it is quite easy to pass a parameter through the route when navigating to a page, but how do we use it? Let’s consider the classic situation of a master/detail pattern. We have one page that displays a list of data — titles of movies perhaps — and when we click on one of those items it will take us to a detail page with more information about that movie.

We might have routes set up like this:

import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'home',
loadComponent: () =>
import('./home/home.component').then((m) => m.HomeComponent),
},
{
path: 'detail/:id',
loadComponent: () =>
import('./detail/detail.component').then((m) => m.DetailComponent),
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
];

If you’re following along with your own example application, see if you can get this pattern set up. Don’t worry about actually displaying real data just yet, we just want an application that:

  • Has a Home page
  • Has a Detail page
  • Displays a list of data on the home page
  • Sets up click event bindings for the list items such that when they are clicked we navigate to the DetailPage and supply it with the id for the item (this can just be a static value if you want)

Depending on your experience level coming into this course, this might prove to be a bit of a challenge. However, we have covered everything you need to know to achieve this in the previous lessons.

Give this a go and when you are ready continue below (don’t worry if you can’t get it).

If we set up something like this:

import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-home',
template: `
<p>I am the home page</p>
<ul>
@for (movie of movies; track movie.id){
<li>
<a [routerLink]="['/detail', movie.id]">{{ movie.name }}</a>
</li>
}
</ul>
`,
imports: [RouterLink],
})
export class HomeComponent {
movies = [
{ id: 1, name: 'Dune' },
{ id: 2, name: 'Coco' },
{ id: 3, name: 'Soul' },
];
}

We can quite easily pass the id of our movie to the detail page using the route param, and we could access that id on the detail page. But we don’t just want an id we want the entire movie object — which isn’t quite as well suited to being passed through the URL. We might have additional information we want to grab for that movie like a description, a trailer, a rating, comments, reviews, and so on.

What we will typically do is just pass some simple identifier as a param, like the id, and then on our detail page we will fetch the full data from a service using that id value.

We might have a service set up like this:

shared/data-access/movies.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root',
})
export class MoviesService {
private http = inject(HttpClient);
getMovieById(id: string) {
return this.http.get(`https://some-api.com/movie/${id}`);
}
}

This will allow us to call the getMovieById method and it will return us a stream that will emit the data fetched from the server (again, we will be talking more about observables/streams later).

app/app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
],
};

Although we do not need to do the following because Angular now provides provideHttpClient for us, some older Angular libraries might require setting up modules to enable functionality. In a standalone application, you can automatically import the providers from those modules using the following approach:

app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { HttpClientModule } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), importProvidersFrom(HttpClientModule)],
};

This achieves the same thing as provideHttpClient but we are instead importing the providers manually from HttpClientModule. As I mentioned, this is not necessary in this case, but it may be for some libraries.

With our service set up, we could inject this in our DetailPage, along with the information for the ActivatedRoute to get the id passed in, and access a stream of the movie details:

import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { MoviesService } from '../shared/data-access/movies.service';
@Component({
selector: 'app-detail',
template: `{{movie}}`,
})
export class DetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private moviesService = inject(MoviesService);
public movie: any;
ngOnInit() {
const id = this.route.snapshot.params['id'];
this.moviesService.getMovieById(id).subscribe((movie) => {
this.movie = movie;
});
}
}

The above is perhaps a more standard/beginner way to do this, but we will be using reactive code in our applications generally which would look more like this:

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { MoviesService } from '../shared/data-access/movies.service';
import { switchMap } from 'rxjs/operators';
@Component({
selector: 'app-detail',
template: `{{ movie() }}`,
})
export class DetailComponent {
private route = inject(ActivatedRoute);
private moviesService = inject(MoviesService);
public movie = toSignal(
this.route.params.pipe(
switchMap((params) => this.moviesService.getMovieById(params['id']))
)
);
}

This example probably looks scarier (although maybe you can see that it is also a bit cleaner/simpler), so I don’t want to freak you out with this right away when we haven’t even got to the module on RxJS and reactive coding yet. Just know there are different ways we can approach this.

Now our DetailPage has the full details for a particular movie, and we can display that however we like in the template. If we go back to our home page and choose a different movie, that movies details will be loaded up instead.

Recap

    What is the correct way to navigate to another route in Angular from the template?

    What is the correct way to navigate to another route in Angular from the class?

    If we want a route to accept a parameter called ‘word’ on a route called ‘define’ how would we set that up?

    If we wanted to get access to the ‘word’ param on our ‘define’ page what should we inject into our component?