Routing in Angular
Angular has a built-in router. When initializing a new project, the CLI asks if it should be included or not. If we opted for “No” initially, we can add routing module manually:
ng generate module app-routing --flat --module=app
We can also configure routing in the AppModule
file:
const routes: Routes = [ { path: '', component: MainPageComponent, pathMatch: 'full' }, // match only if path is literally '' { path: 'home', redirectTo: '/' } // reusing a route under a different URL { path: 'users', component: UsersComponent }, { path: 'movies', component: MoviesComponent }, { path: 'movies/:movieId', component: MovieComponent }, { path: '404', component: NotFoundComponent }, { path: '**', redirectTo: '/404' } // catch all undefined cases];
@NgModule({ declarations: [ AppComponent, // ... ], imports: [ BrowserModule, FormsModule, RouterModule.forRoot(routes) ], providers: [], bootstrap: [AppComponent]})export class AppModule { }
The selected component will be rendered as a child of AppComponent
. We need to
specify the exact placement of that component:
<router-outlet></router-outlet>
The routes array should generally contain a wildcard route that handles all bad links. Usually, it would redirect the user to some 404 page.
Paths of routes defined in the array are prefix paths by default. It means
that Angular will match it whenever the beginning of browser’s URL matches the
route’s path. If we want the route to be selected only when the whole URL is
matched (without the host though), we should add the pathMatch: 'full'
option.
External File
Usually, we do not define routes directly in app.module.ts
. Instead, we’d
create a separate module file app-routing.module.ts
with the routing setup
inside of it.
const routes: Routes = [ { path: '', component: MainPageComponent, pathMatch: 'full' }, // other routes...];
@NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ],})export class AppRoutingModule { }
Then, the app.module.ts
file gets simplified:
@NgModule({ declarations: [ AppComponent, // ... ], imports: [ BrowserModule, FormsModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent]})export class AppModule { }
We can also split routes per feature.
Navigation in HTML
With routing in place, we don’t want to navigate between different pages in our
app with traditional <a href="/whatever">Whatever</a>
. Clicking such a link
will work and the user will be taken to the right component (if the /whatever/
route was configured), but it comes with a huge issue - the whole application
will actually be reloaded from the server again. The app’s state will be lost,
and it will be slow. Instead of that, we want the Angular Router to handle the
navigation, making it an in-app navigation rather than a browser-based
navigation. Here’s how <a>
elements should look like:
<a routerLink="/home">Home</a>
The link above is applied to the host of the page. If we didn’t include the /
,
the home
segment would be applied to the currently open page. It works
differently in Programmatic Navigation!
Styling
In order to have visual indication on the currently visited menu element, we
would normally attach some CSS class to that active element. Angular comes with
a helper directive that does that automatically - routerLinkActive
.
<div routerLinkActive="active"> <a routerLink="/">Home</a></div>
The element that has the directive on it will have the “active” class attached
to it when the link is active. The directive can be attached on the <a>
or on
some element that wraps it, like in the example above.
Navigation in TypeScript
Router is accessible via TS as well.
export class SomeComponent { constructor(private router: Router) { }
onButtonClick() { this.router.navigate(['/somewhere']); }}
The path that we navigate to is (by default) relative to the root.
Having or not having slash in the beginning does not change anything (it does
matter with routerLink)! We can change the path that navigate
will be executed in relation to with relativeTo
. For example, we could pass to
it the currently activated route (ActivatedRoute
).
Route Inputs
Path Parameters
Route’s path can have parameters. In the code example at the top of this page,
movies/:movieId
is an example of that. movieId
is a parameter, and the
MovieComponent
will receive it.
ActivatedRoute
We can get information about currently loaded route by injecting
ActivatedRoute
. It contains various metadata about the loaded path, e.g. the
parameters.
export class MovieComponent implements OnInit { movieId: string;
constructor(private route: ActivatedRoute) { }
ngOnInit() { this.movieId = this.route.snapshot.params['movieId']; }}
The parameters may also be subscribed to via an Observable
-
this.route.params
. It might be useful when we plan to link from some site to
itself with different parameters. In such a case, Angular will not reload the
whole component for optimization. Instead, only the ActivatedRoute
will
change.
Query Parameters
We can also make use of query parameters. To attach them to links on our page, we do it as follows:
<a [routerLink]="['/home', '3']" [queryParams]="{ darkMode: true }"> Home 3</a>
Here’s how we add query params from TS:
this.router.navigate( ['/somewhere'], { queryParams: { darkMode: true } });
Here’s how we can read query params from TS by injecting ActivatedRoute
(route
variable):
const { darkMode } = this.route.snapshot.queryParams;
Similarly to path parameters, we can also subscribe to this.route.queryParams
.
Preserving Query Params
When we’re on some page with some query params in the URL, by default these query params will be removed when we navigate to another page. If we don’t want that, we can do it the following way:
this.router.navigate( ['/somewhere'], { queryParamsHandling: 'merge' });
The merge
handling merges together existing query params and those that we
might want to add (by having queryParams
defined).
Fragment
Similarly, we can attach fragment (#fragment
) to link we navigate to:
<a [routerLink]="['/home', '3']" fragment="something"> Home 3</a>
Here’s how we add fragment from TS:
this.router.navigate( ['/somewhere'], { fragment: 'something' });
Here’s how we can read fragment from TS by injecting ActivatedRoute
(route
variable):
const fragment = this.route.snapshot.fragment;
Similarly to path parameters, we can also subscribe to this.route.fragment
.
Static Data
Routes can have some static data defined. This way, the same route can be reused
multiple times. For example, we could have a generic ErrorComponent
which
displays different message depending on the kind of error. Such a component
could look like this:
@Component({ selector: 'app-error', template: '<h2> {{ errorMessage }} </h2>'})export class ErrorComponent implements OnInit { errorMessage: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() { this.errorMessage = this.route.snapshot.data['message']; }}
We can set the static value(s) in the routes collection:
const routes: Routes = [ { path: '', component: MainPageComponent, pathMatch: 'full' }, { path: 'home', redirectTo: '/' } { path: 'users', component: UsersComponent }, { path: 'movies', component: MoviesComponent }, { path: 'movies/:movieId', component: MovieComponent }, { path: 'not-found', component: ErrorComponent, data: { message: 'Page not found' } }, { path: 'not-ready', component: ErrorComponent, data: { message: 'This page is under construction' } }, { path: '**', redirectTo: '/not-found' }];
Nested Routing
Nested Routing allows us to have multiple routing outlets, one within another. We could have a main menu with each entry of it loading a different submenu. Then, each submenu would have a list of links that load a different content.
First, we need to set up our routes properly:
const routes: Routes = [ { path: '', component: MainPageComponent }, { path: 'users', component: UsersComponent }, { path: 'movies', component: MoviesComponent, children: [ { path: '/:movieId', component: MovieComponent } ] }]
In the example above, /:movieId
is a nested route. The full path (without
host) to it is /movies/:moviesId
.
The next thing to do is to place an outlet where the MovieComponent
will be
rendered. We should place it somewhere within MoviesComponent
:
<router-outlet></router-outlet>
With this setup, when navigating to /movies/<id>
, the following will happen:
- the
MoviesComponent
will be rendered in therouter-outlet
within theAppComponent
- the
MovieComponent
will be rendered in therouter-outlet
within theMoviesComponent
The setup could be more complex, having multiple children under the movies
path, or by having more levels of nesting.
Guards
Routes can be protected by Guards. The user may be either allowed or disallowed to enter some content. It could be due to them being (un)authorized in some way.
Guards are services and we normally store them in *.service.ts
files.
Here’s a simple example:
@Injectable() // for Router to be injectedexport class AuthGuard implements CanActivate { constructor(private router: Router)
isAuthorized = true; // just for the sake of this example
canActivate( route: ActivatedRoute, state: RouterStateSnapshot) : bool {
if (this.isAuthorized) { return true; } else { this.router.navigate('/unauthorized'); // take user somewhere return false; // not necessary since not returning anything also counts as negative result } }}
The guard needs to implement CanActivate
. Its canActivate
method should
return one of:
bool | UrlTree
Observable<bool | UrlTree>
Promise<bool | UrlTree>
UrlTree
We can route users to some other page in the guard, most likely when the
condition is not satisfied. We use the UrlTree
for that - it’s one of the
types that may be returned from guards. Here’s how we’d return it:
if (notAuthorized) { return this.router.createUrlTree(['/login']);}
The router
is an instance of a Router
.
The route should be enabled for selected endpoints in the routes definition:
const routes: Routes = [ { path: '', component: MainPageComponent }, { path: 'users', component: UsersComponen, canActivate: [AuthGuard] }, { path: 'movies', component: MoviesComponent, children: [ { path: '/:movieId', component: MovieComponent } ] }]
The /users
endpoint is protected by our guard. If we applied the guard to the
/movies
route, the child of it would also use it.
CanDeactivate Guard
Similarly to checking if a user can enter some route, we can also check if they should be able to leave it. This is to protect users from unintentionally leaving half-done form, forgetting to save their work, etc.
Setting this up is a little bit more involved than using CanActivate
guard.
That’s because the guard will most likely need some input from the component
that we’re leaving to know if the user should be able to leave. The component
could have some isWorkSaved
variable, but the guard cannot reach it - it’s a
separate class after all. We can solve this problem using generics.
Here’s an example:
// the component protected by our guard should implement itexport interface DeactivatableComponent {
// the logic to check whether the user can leave, e.g. isWorkSaved === true canDeactivate: () => Observable<bool> | Promise<bool> | bool;}
// the guardexport class CanDeactivateGuard implements CanDeactivate<DeactivatableComponent> { // ...
// one of the params is our interface - DeactivatableComponent - it will be // the protected component canDeactivate( component: DeactivatableComponent, currentRoute: ActivatedRoute, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot) : Observable<bool> | Promise<bool> | bool {
// here's how we ask the component itself if user is allowed to leave. return component.canDeactivate(); }}
Next piece is the actual component’s code. The component needs to
implement DeactivatableComponent
:
@Component({ selector: 'my-component'})export class MyComponent implements DeactivatableComponent { isWorkSaved: bool = false;
// ...
canDeactivate() { return isWorkSaved; }}
To use the new guard, we need to enable it in the routes collection:
const routes: Routes = [ { path: '', component: MainPageComponent }, { path: 'users', component: UsersComponen, canDeactivate: [CanDeactivateGuard] }, { path: 'movies', component: MoviesComponent, children: [ { path: '/:movieId', component: MovieComponent } ] }]
Resolvers
When a given component needs some external data to be loaded before it can be displayed, custom Resolvers can be used.
Here’s an example of such a resolver:
interface Movie { // some info about a movie}
@Injectable()export class MovieResolver implements Resolve<Movie> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable<Movie> | Promise<Movie> | Movie {
// Get the movie from some external service... // Probably the route's params/query params will be needed // to get the ID of the requested movie. return movie; }}
To use the resolver, we attach it to the route that needs it:
const routes: Routes = [ { path: '', component: MainPageComponent, pathMatch: 'full' }, { path: 'home', redirectTo: '/' } { path: 'users', component: UsersComponent }, { path: 'movies', component: MoviesComponent }, { path: 'movies/:movieId', component: MovieComponent, resolve: { movie: MovieResolver } }, { path: '404', component: NotFoundComponent }, { path: '**', redirectTo: '/404' }];
Here’s how we can access the result of resolver’s work in the MovieComponent
:
export class MovieComponent implements OnInit { movieDetails: Movie;
ngOnInit() { this.route.data.subscribe(data: Data => { this.movieDetails = data['movie']; }); }}
We subscribe to the result, because the resulting data could change when we reload the component (Angular will not reload the whole component for performance reasons).
The result is placed in this.route.data
object under the key that we used in
the route definition’s resolve
section (movie
in this case). The data
object was also used in the Static Data.
Lazy Loading
Lazy Loading is described in the Modules section.