Angular Routing - Interesting features tiny bits

May 9, 2022

Introduction

In Angular SPAs, routing allows to implement a way to move between different views by showing or hiding portions of the display instead of going out to the server to get a new page. This is what Angular Routing is usually used for, just for emulating page navigation.

In this post we have an overview about some other cool routing features that can improve the way we implement almost any Angular application.

Basics

Angular Router takes the browser url as an instruction to change the view, which are essentially Angular components that are loaded where the <router-outlet> tag is placed, in the root component generally.

<h1>This is my app!</h1>
<router-outlet></router-outlet>

Routes are defined in a file which name must end with "-routing.module.ts".

A basic routes file would look like this:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin/admin.component';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {path: '',      component: HomeComponent},
  {path: 'admin', component: AdminComponent}
];

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

Wildcard Routes

Wildcard routes act as a fallback for any route that does not match a defined one.

They have to be included the last in the list, as the angular router uses first-match wins strategy.

const routes: Routes = [
  {path: '',      component: HomeComponent},
  {path: 'admin', component: AdminComponent},
  {path: '**',    component: NotFoundComponent}
];

This will cause any route that does not match to return the NotFoundComponent as view.

If this is not set, the router will redirect to an empty route without loading anything instead.

Redirection

Redirection routes switch from one path to another instead of directly loading a component.

const routes: Routes = [
  {path: 'home',      component: HomeComponent},
  {path: 'admin',     component: AdminComponent},
  {path: 'not-found', component: NotFoundComponent},
  {path: '',   redirectTo: '/home',      pathMatch: 'full'},
  {path: '**', redirectTo: '/not-found', pathMatch: 'full'}
];

In our example, now the root path redirects to /home, and not specified ones always redirect to /not-found

Nested routes

As an Angular application grows, it might come handy having some routes that are relative to another component instead of root. A new <router-outlet> tag inside the parent component template is needed for this.

const routes: Routes = [
  {path: 'home',      component: HomeComponent},
  {path: 'admin',     component: AdminComponent},
  {path: 'browser',   component: BrowserComponent, children: [
    {path: 'results',   component: ResultsComponent},
    {path: 'detail',    component: DetailComponent}
  ]},
  {path: 'not-found', component: NotFoundComponent},
  {path:'',    redirectTo: '/home',      pathMatch: 'full'},
  {path: '**', redirectTo: '/not-found', pathMatch: 'full'}
];

In our example, inside the browser view we would have some content (Like a browsing bar) and then it would load the 'results' and 'detail' views inside.

Nested routes can also have their own nested ones, so this nesting can go as deep as we want. When this happens and our application grows even further, we can also split our routing file and create child routing modules that will be imported by the parent.

In order to separate our 'browser' routing we need to create a BrowserRoutingModule:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ResultsResolver } from '../results.resolver';
import { DetailComponent } from './detail/detail.component';
import { ResultsComponent } from './results/results.component';

const routes: Routes = [
  {path: 'results',   component: ResultsComponent, resolve: {results: ResultsResolver}},
  {path: 'detail',    component: DetailComponent}
];

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

This module will be imported by a new BrowserModule, which will also declare the browser dependant components, simplifying the main AppModule in the process:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowserRoutingModule } from './browser-routing.module';
import { BrowserComponent } from './browser.component';

@NgModule({
  declarations: [
    BrowserComponent,
    ResultsComponent,
    DetailComponent],
  imports: [
    CommonModule,
    BrowserRoutingModule,
  ]
})
export class BrowserModule { }

And to finish, the changes on the AppRoutingModule routes:

const routes: Routes = [
  {path: 'home',      component: HomeComponent},
  {path: 'admin',     component: AdminComponent},
  {path: 'browser',   component: BrowserComponent, loadChildren: () => BrowserModule},
  {path: 'not-found', component: NotFoundComponent},
  {path:'',    redirectTo: '/home',      pathMatch: 'full'},
  {path: '**', redirectTo: '/not-found', pathMatch: 'full'}
];

This way we can scale and organize our application without getting hard reading bulky modules.

Resolvers

Taking our example, if we have a browser view that shows some results, the regular implementation would be directly loading the results view and then showing a loading icon or whatever while the results are being loaded.

Instead, we can implement a resolver so the results are fetched before the view is loaded, like a service dependant navigation.

Same logic could apply to the detail view but we are going to focus on the results view for simplification.

The 'results' route would need to be adapted somewhat like this:

{path: 'results', component: ResultsComponent, resolve: {results: ResultsResolver}},

This is a simple resolver example for our case:

import { Injectable } from '@angular/core';
import { Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { Result } from './result';

@Injectable({
  providedIn: 'root'
})
export class ResultsResolver implements Resolve<Result[]> {
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Result[]> {
    // TODO: Implement true results retrieving. The 'route' parameter can be used to retrieve the browser input.
    return of([
      {data: 'result1'},
      {data: 'result2'}
    ]);
  }
}

After loading, the ResultsComponent can then retrieve the results data from the ActivatedRoute snapshot, like this:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Result } from '../result';

@Component({
  selector: 'app-results',
  templateUrl: './results.component.html',
  styleUrls: ['./results.component.scss']
})
export class ResultsComponent implements OnInit {

  public results: Result[] = [];

  constructor(private activatedRoute: ActivatedRoute) { }

  ngOnInit(): void {
    this.results = this.activatedRoute.snapshot.data['results'];
  }

}

Guards: Preventing unauthorized access

Integrating routes with Guards applies when it comes to manage which user can access what view.

For our example, we'll establish that only admin users can access the Admin view using a 'canActivate' guard.

const routes: Routes = [
  {path: 'home',      component: HomeComponent},
  {path: 'admin',     component: AdminComponent,   canActivate: [AdminGuard]},
  {path: 'browser',   component: BrowserComponent, children: [
    {path: 'results',   component: ResultsComponent, resolve: {results: ResultsResolver}},
    {path: '',          component: DetailComponent}
  ]},
  {path: 'not-found', component: NotFoundComponent},
  {path:'',    redirectTo: '/home',      pathMatch: 'full'},
  {path: '**', redirectTo: '/not-found', pathMatch: 'full'}
];

And here the sample guard we use for this purpose:

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AdminGuard implements CanActivate {

  private isAdmin: boolean = false;

  constructor(private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
      // TODO: Implement the logic to determine whether the user is admin or not
      if (!this.isAdmin) {
        this.router.navigate(['/not-found']);
      }
      return this.isAdmin;
  }
  
}

After this implementation if the user is not an admin and tries to access the 'admin' path, it will be redirected to the 'not-found' view as if the path didn't exist.

Conclusion

As we have briefly seen Angular Routing is not just a tool for handling view switching, it has many features that can improve the way we implement almost any Angular application.

Further info at https://angular.io/guide/router

About the author: Pablo Herrero

Fullstack Software Engineer at mimacom.

Comments
Join us