How to use the Angular Router
How to use the Angular Router
Today we will see how to use the Angular Router. The router allows us to define routes which are transformed to urls which are then understood by the browser. Having routes allows us to create different categories and access points to our website. This post will be composed by 6 parts:
- Define routes
- Router outlet
- Special routes
- Data and ActivatedRoute
- Resolve guard
- CanXXX guards
1. Define routes
To start, we need to import the RouteModule
and the Routes
type from the Angular router.
import { RouterModule, Routes } from '@angular/router';
The routes are defined via constant and then injected into the router module using either forRoot
or forChild
.
forRoot
is used to define routes on the main module and forChild
is used to define routes on the child modules
For example we can define two routes which we add to the main module:
const routes: Routes = [
{
path: 'home',
component: MainPageComponent,
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}
];
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(routes),
OnePageModule,
],
declarations: [
AppComponent,
MainPageComponent
],
bootstrap: [
AppComponent
]
})
export class AppModule { }
Here two routes are defined, /home
which will display HomePageComponent
and /
which will redirect to home
.
The routes are then added to the route module with RouterModule.forRoot(routes)
.
2. Router outlet
The HomePageComponent
will be displayed below the router-outlet
tag.
Our main AppComponent
template is as followed:
<a routerLink="/main" routerLinkActive="active">Home</a>
<router-outlet></router-outlet>
Component is displayed after router-outlet
. routerLink
is used to specify the link, it can be specified as a relative path or full path.
routerLink="test"
will add to the current path while routerLink="/test"
will replace the path. For example if the current path is /hello
, ="test"
will result in /hello/test
while ="/test"
will result in /test
.
routerLinkActive="active"
defines the class which will be added to the element when the route is active.
The router-outlet
accepts a name. This can be used to display components side by side. For example we define a route my-page
and two outlet first
and second
, a possible route could be as followed:
/my-page/(first:x/y/z//second:a/b/c)
The two outlets maintain relative paths first:x/y/z
and second:a/b/c
, separated by a double slash //
. This is very powerful as it gives a unique route for any permutation of the side by side components.
The routeLink
would be defined like so:
myRoute = [
'/one-page',
{
outlets: {
first: ['x','y','z'],
second: ['a','b','c']
}
}];
3. Special routes
3.1 Redirect
In 1) we saw the following route:
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
}
redirectTo
will redirect to /home
. In order to redirect properly, we need to specify the pathMatch
. There are two types prefix
and full
. Prefix
being the default we only need to specify when we want the matching to be full
. full
will match the full remaining path while prefix
will match the path which starts with the path given. ''
being the prefix of all paths, if we set prefix
it will always redirect.
3.2 Wildcard
Another special route is the wildcard. It can be defined using the path path:'**'
. For example here we can define the following under the one-page
route:
{
path: 'one-page',
component: OnePageComponent,
children: [
{
path: '',
component: SecondPageComponent
},
{
path: '**',
redirectTo: '',
pathMatch: 'full'
}
]
},
Any route after one-page
that aren’t ''
will be redirected to ''
which will then result in /one-page
.
4. Data and ActivatedRoute
4.1 Static data
Static data can be configured in the route.
{
path: '',
component: MyComponent,
data: {
hello: 'world'
}
}
Then in MyComponent
, we can access those data via the ActivatedRoute
. The data are held in an observable. We can access it as followed:
@Component({
template: `
<strong>{{ data$ | async }}</strong>
`
})
export class MyComponent implements OnInit {
data$: Observable<any>;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.data$ = this.route.data.pluck('hello');
}
}
4.2 Parameters
Similarly to the data, parameters can be extracted from the route using the notation :parameter
and accessed via the ActivatedRoute
:
{
path: ':id',
component: MyComponent,
}
export class MyComponent implements OnInit {
data$: Observable<string>;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.data$ = this.route
.paramMap
.filter(m => m.has('id'))
.map(m => m.get('id'));
}
}
5. Resolve guard
Other than resolving static data using data
, there are instances where it is desired to retrieve asynchronous data when accessing a page. In order to achieve that we can use the resolve
guard.
It is an interface to implement on a service:
interface Resolve {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable<T>|Promise<T>|T
}
The route
given is the current route activated, giving access to the data, component and params of the current route.
The state
is a tree representing the snapshot at this instant of the whole route tree. It can be used to traverse all child routes activated.
The return type accepts an Observable
, a Promise
or a concrete type T
.
For example we could have a title which requires dynamic data:
@Injectable()
export class TitleResolver implements Resolve<string> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<string> {
return Observable
.of('An async title')
.delay(5000);
}
}
Make sure the Observable completes as the resolve guard will take its last value. Is the Observable is infinite, the guard will prevent your component from loading.
Then we will need to register this service in the module:
@NgModule({
imports: [...],
declarations: [...],
providers: [
TitleResolver
]
})
export class MyModule { }
We can then add the resolver into the route configuration and define a variable myTitle
to place the result in. We will then be able to access this variable from the data
.
const routes: Routes = [
{
path: 'my-route',
component: MyComponent,
resolve: {
myTitle: TitleResolver
}
}
];
This will ensure that the title is resolved before the compononent is shown. We can then use the resolved value by plucking it out of the data. We can be sure that at least one value will be resolved from the myTitle
variable.
export class OnePageComponent implements OnInit {
title$: Observable<string>;
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.title$ = this.route
.data
.pluck('myTitle');
}
}
6. CanXXX guards
There are three other guards available, canActivate
, canActivateChild
and canLoad
.
canActivate
and canActivateChild
specify whether the route ca be activated.
A common scenario is authentication verification which will be performed in the guard and if the user isn’t authenticated, it will redirect to the login page.
canActivate
is used to decide whether the user is able to access the current route while canActivateChild
decides if the user is able to access the child of the route where the guard is placed. Another difference is in the arguments given by the interfaces. For canActivate
, we have access to the route:
export interface CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean;
}
While with canActivateChild
, we have access to the child:
export interface CanActivateChild {
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean;
}
Therefore the main difference is that in one we use the current route where the guard is placed on to decide whether the user is allowed to activate the route while in the other case, we use the child route where the guard is placed to decide.
We have protected the route with canActive
and canActiveChild
but the modules which are protected are still imported into the app module therefore still loaded.
There are instances where the module is very specific and targeted to a limited amount of users. In those cases, we can lazy load the module which will result in loading dynamically the module when the route is hit.
This can be achieved using loadChildren
and providing the path of the file containing the module to lazily load followed by #MyModule
.
{
path: 'my-lazy-route',
loadChildren: './my-lazy-module/my-lazy-module.module#MyModule'
}
Then we can remove the module import in the app module. The module will no longer be loaded on boot but only loaded on navigation to my-lazy-route
.
In this case, canActive
will happen after we load the module. But what we actually want isn’t to load and check but to directly prevent the module to be loaded for unauthorized users. This is where we can use canLoad
which will prevent the module to be loaded.
{
path: 'my-lazy-route',
loadChildren: './my-lazy-module/my-lazy-module.module#MyModule'
canLoad: SomeGuard
}
This canLoad
is a service which must implement the CanLoad
interface which allow to decide, based on the route, if the module should be loaded or not.
export interface CanLoad {
canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean;
}
Conclusion
In this post we saw how we could use the Angular router and configure routes. We learnt how to configure the different type of routes including the wildcard route and different configurations available like the redirects. We also learnt how to provide routes to side by side components with outlets. We learnt how to define static data and how to pass parameters into the URL and retrieve it using the activated route object which we can inject in our components. Lastely we learnt about guards and how to use them to ensure data are loaded and allow or restrict access to certain routes. Hope you like this post, if you have any question leave it here or hit me on Twitter @Kimserey_Lam. See you next time!
Nice article but how about lazy load module with named outlet in module?
ReplyDelete