Skip to content

Latest commit

 

History

History
280 lines (218 loc) · 10.3 KB

guide-routing.asciidoc

File metadata and controls

280 lines (218 loc) · 10.3 KB

Routing

A basic introduction to the Angular Router can be found in Angular Docs.

This guide will show common tasks and best practices.

Defining Routes

For each feature module and the app module all routes should be defined in a seperate module with the suffix RoutingModule. This way the routing modules are the only place where routes are defined. This pattern achieves a clear seperation of concernes. The following figure illustrates this.

Routing module declaration
Figure 1. Routing module declaration

It is important to define routes inside app routing module with .forRoot() and in feature routing modules with .forChild().

Example 1 - No Lazy Loading

In this example two modules need to be configured with routes - AppModule and FlightModule.

The following routes will be configured

  • / will redirect to /search

  • /search displays FlightSearchComponent (FlightModule)

  • /search/print/:flightId/:date displays FlightPrintComponent (FlightModule)

  • /search/details/:flightId/:date displays FlightDetailsComponent (FlightModule)

  • All other routes will display ErrorPage404 (AppModule)

Listing 1. app-routing.module.ts
const routes: Routes = [
  { path: '', redirectTo: 'search', pathMatch: 'full' },
  { path: '**', component: ErrorPage404 }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Listing 2. flight-search-routing.module.ts
const routes: Routes = [
  {
    path: 'search', children: [
      { path: '', component: FlightSearchComponent },
      { path: 'print/:flightId/:date', component: FlightPrintComponent },
      { path: 'details/:flightId/:date', component: FlightDetailsComponent }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class FlightSearchRoutingModule { }
💡
The import order inside AppModule is important. AppRoutingModule needs to be imported after FlightModule.

Example 2 - Lazy Loading

Lazy Loading is a good practice when the application has multiple feature areas and a user might not visit every dialog. Or at least he might not need every dialog up front.

The following example will configure the same routes as example 1 but will lazy load FlightModule.

Listing 3. app-routing.module.ts
const routes: Routes = [
  { path: '/search', loadChildren: 'app/flight-search/flight-search.module#FlightSearchModule' },
  { path: '**', component: ErrorPage404 }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Listing 4. flight-search-routing.module.ts
const routes: Routes = [
  {
    path: '', children: [
      { path: '', component: FlightSearchComponent },
      { path: 'print/:flightId/:date', component: FlightPrintComponent },
      { path: 'details/:flightId/:date', component: FlightDetailsComponent }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class FlightSearchRoutingModule { }

Triggering Route Changes

With Angular you have two ways of triggering route changes.

  1. Declarative with bindings in component HTML templates

  2. Programmatic with Angular Router service inside component classes

On the one hand, architecture-wise it is a much cleaner solution to trigger route changes in Smart Components. This way you have every UI event that should trigger a navigation handled in one place - in a Smart Component. It becomes very easy to look inside the code for every navigation, that can occure. Refactoring is also much easier, as there are no navigation events "hidden" in the HTML templates

On the other hand, in terms of accessibility and SEO it is a better solution to rely on bindings in the view - e.g. by using Angulars router-link directive. This way screen readers and the Google crawler can move through the page easily.

💡
If you do not have to support accessibility (screen readers, etc.) and to care about SEO (Google rank, etc.), then you should aim for triggering navigations only in Smart Components.
Triggering navigation
Figure 2. Triggering navigation

Guards

Guards are Angular services implemented on routes which determines whether a user can naviagate to/from the route. There are examples below which will explain things better. We have the following types of Guards:

  • CanActivate: It is used to determine whether a user can visit a route. The most common scenario for this guard is to check if the user is authenticated. For example, if we want only logged in users to be able to go to a particular route, we will implement the CanActivate guard on this route.

  • CanActivateChild: Same as above, only implemented on child routes.

  • CanDeactivate: It is used to determine if a user can naviagate away from a route. Most common example is when a user tries to go to a different page after filling up a form and does not save/submit the changes, we can use this guard to confirm whether the user really wants to leave the page without saving/submiting.

  • Resolve: For resolving dynamic data.

  • CanLoad: It is used to determine whether an Angular module can be loaded lazily. Example below will be helpful to understand it.

Let’s have a look at some examples.

Example 1 - CanActivate and CanActivateChild guards

CanActivate guard

As mentioned earlier, a guard is an Angular service and services are simply TypeScript classes. So we begin by creating a class. This class has to implement the CanActivate interface (imported from angular/router), and therefore, must have a canActivate function. The logic of this function determines whether the requested route can be navigated to or not. It returns either a boolean value or an Observable or a Promise which resolves to a boolean value. If it is true, the route is loaded, else not.

Listing 5. CanActivate example
...
import {CanActivate} from "@angular/router";

@Injectable()
class ExampleAuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  canActivate(route: ActivatedRouterSnapshot, state: RouterStateSnapshot) {
	if (this.authService.isLoggedIn()) {
      return true;
    } else {
	  window.alert('Please log in first');
      return false;
    }
  }
}

In the above example, let’s assume we have a AuthService which has a isLoggedIn() method which returns a boolean value depending on whether the user is logged in. We use it to return true or false from the canActivate function. The canActivate function accepts two parameters (provided by Angular). The first parameter of type ActivatedRouterSnapshot is the snapshot of the route the user is trying to naviagate to (where the guard is implemented); we can extract the route parameters from this instance. The second parameter of type RouterStateSnapshot is a snapshot of the router state the user is trying to naviagate to; we can fetch the URL from it’s url property.

💡
We can also redirect the user to another page (maybe a login page) if the authService returns false. To do that, inject Router and use it’s naviagate function to redirect to the appropriate page.

Since it is a service, it needs to be provided in our module:

Listing 6. provide the guard in a module
@NgModule({
  ...
  providers: [
    ...
    ExampleAuthGuard
  ]
})

Now this guard is ready to use on our routes. We implement it where we define our array of routes in the application:

Listing 7. Implementing the guard
...
const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'page1', component: Page1Component, canActivate: [ExampleAuthGuard] }
];

As you can see, the canActivate property accepts an array of guards. So we can implement more than one guard on a route.

CanActivateChild guard

To use the guard on nested (children) routes, we add it to the canActivateChild property like so:

Listing 8. Implementing the guard on child routes
...
const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'page1', component: Page1Component, canActivateChild: [ExampleAuthGuard], children: [
	{path: 'sub-page1', component: SubPageComponent},
    {path: 'sub-page2', component: SubPageComponent}
  ] }
];

Example 2 - CanLoad guard

Similar to CanActivate, to use this guard we implement the CanLoad interface and overwrite it’s canLoad function. Again, this function returns either a boolean value or an Observable or a Promise which resolves to a boolean value. The fundamental difference between CanActivate and CanLoad is that CanLoad is used to determine whether an entire module can be lazily loaded or not. If the guard returns false for a module protected by CanLoad, the entire module is not loaded.

Listing 9. CanLoad example
...
import {CanLoad, Route} from "@angular/router";

@Injectable()
class ExampleCanLoadGuard implements CanLoad {
  constructor(private authService: AuthService) {}

  canLoad(route: Route) {
	if (this.authService.isLoggedIn()) {
      return true;
    } else {
	  window.alert('Please log in first');
      return false;
    }
  }
}

Again, let’s assume we have a AuthService which has a isLoggedIn() method which returns a boolean value depending on whether the user is logged in. The canLoad function accepts a parameter of type Route which we can use to fetch the path a user is trying to navigate to (using the path property of Route).

This guard needs to be provided in our module like any other service.

To implement the guard, we use the canLoad property:

Listing 10. Implementing the guard
...
const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canLoad: [ExampleCanLoadGuard] }
];