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/homeor
http://localhost:4200/settingsBut, 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
routerLinkin 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
Homepage - Has a
Detailpage - Displays a list of data on the home page
- Sets up
clickevent bindings for the list items such that when they are clicked we navigate to theDetailPageand supply it with theidfor 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).
Click here to reveal solution
Solution
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:
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).
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:
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.