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:

  1. Define routes
  2. Router outlet
  3. Special routes
  4. Data and ActivatedRoute
  5. Resolve guard
  6. 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!

Comments

  1. Nice article but how about lazy load module with named outlet in module?

    ReplyDelete

Post a Comment

Popular posts from this blog

A complete SignalR with ASP Net Core example with WSS, Authentication, Nginx

Verify dotnet SDK and runtime version installed

SDK-Style project and project.assets.json