cyberangles guide

Routing in Angular: From Basics to Advanced

Angular is a powerful framework for building single-page applications (SPAs), and at the heart of any SPA lies **routing**—the mechanism that enables navigation between different views, manages URL states, and ensures a seamless user experience. Whether you’re building a simple blog or a complex enterprise application, mastering Angular routing is essential. In this blog, we’ll start with the fundamentals of Angular routing,逐步 (zhúbù - step by step) progress to intermediate concepts like route parameters and guards, and finally dive into advanced topics such as lazy loading and route resolvers. By the end, you’ll have a comprehensive understanding of how to architect and implement routing in Angular applications.

Table of Contents

  1. What is Angular Routing?
  2. Setting Up Angular Routing
  3. Basic Routing Concepts
  4. Route Parameters: Dynamic Routes
  5. Child Routes: Nested Navigation
  6. Route Guards: Protecting Routes
  7. Lazy Loading: Optimizing Performance
  8. Advanced Routing Techniques
  9. Best Practices for Angular Routing
  10. References

1. What is Angular Routing?

Angular Routing is a module (RouterModule) that enables navigation between different components in an Angular application. It maps URLs to components, synchronizes the browser’s URL with the application state, and supports features like dynamic routes, nested navigation, and route protection.

Key benefits of Angular routing:

  • Single-Page Application (SPA) Behavior: No full-page reloads—only the routed component updates.
  • URL Management: Users can bookmark or share URLs, and the browser’s back/forward buttons work as expected.
  • Modularity: Routes can be organized into feature modules, improving maintainability.
  • Performance: Lazy loading reduces initial bundle size by loading non-critical code on demand.

2. Setting Up Angular Routing

To use routing in Angular, you’ll need to set up the RouterModule. Here’s how:

Option 1: Add Routing During Project Creation

When creating a new Angular app with the CLI, include the --routing flag to auto-generate a routing module:

ng new my-routing-app --routing  

Option 2: Add Routing Manually

If you already have an app, create a routing module manually:

  1. Generate a routing module:

    ng generate module app-routing --flat --module=app  
    • --flat: Puts the file in src/app instead of a subfolder.
    • --module=app: Registers the routing module with the root AppModule.
  2. The generated app-routing.module.ts will look like this:

    import { NgModule } from '@angular/core';  
    import { RouterModule, Routes } from '@angular/router';  
    
    const routes: Routes = []; // Define routes here  
    
    @NgModule({  
      imports: [RouterModule.forRoot(routes)], // Configure root routes  
      exports: [RouterModule] // Make RouterModule available to the app  
    })  
    export class AppRoutingModule { }  
  3. Import AppRoutingModule into AppModule (auto-done if using --module=app).

3. Basic Routing Concepts

3.1 Configuring Routes

Routes are defined as an array of Route objects in the routing module. Each Route specifies a path (URL segment) and a component to render when that path is matched.

Example: Basic Route Configuration
Update app-routing.module.ts to define routes for Home, About, and a 404 page:

import { NgModule } from '@angular/core';  
import { RouterModule, Routes } from '@angular/router';  
import { HomeComponent } from './home/home.component';  
import { AboutComponent } from './about/about.component';  
import { NotFoundComponent } from './not-found/not-found.component';  

const routes: Routes = [  
  { path: 'home', component: HomeComponent },  
  { path: 'about', component: AboutComponent },  
  { path: '', redirectTo: '/home', pathMatch: 'full' }, // Default route  
  { path: '**', component: NotFoundComponent } // Wildcard route (404)  
];  

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }  
  • path: '': Matches the root URL (http://localhost:4200). Use redirectTo to send users to /home.
  • pathMatch: 'full': Ensures the entire URL is matched (avoids partial matches).
  • path: '**': Wildcard route—matches any URL not defined above (use for 404 pages).

3.2 Router Outlet

The <router-outlet> directive marks the location where the routed component will be rendered. Add it to your root component template (app.component.html):

<!-- app.component.html -->  
<nav>  
  <!-- Navigation links will go here -->  
</nav>  
<router-outlet></router-outlet> <!-- Routed components render here -->  

Use the routerLink directive to create navigation links. It replaces <a href> tags and prevents full-page reloads.

<!-- app.component.html -->  
<nav>  
  <a routerLink="/home" routerLinkActive="active">Home</a>  
  <a routerLink="/about" routerLinkActive="active">About</a>  
</nav>  
  • routerLink="/home": Binds to the /home path.
  • routerLinkActive="active": Adds the active CSS class to the link when its route is active (style it in styles.css).

For dynamic paths (e.g., with variables), use property binding:

<!-- Navigate to /user/123 -->  
<a [routerLink]="['/user', userId]">View User</a>  

4. Route Parameters: Dynamic Routes

Route parameters let you define dynamic URL segments (e.g., /user/123, where 123 is a user ID).

Step 1: Define a Route with a Parameter

Add a route with a parameter placeholder (:paramName) in app-routing.module.ts:

const routes: Routes = [  
  // ... other routes  
  { path: 'user/:id', component: UserComponent } // :id is the parameter  
];  

Step 2: Access Parameters in the Component

Use ActivatedRoute to access route parameters. Inject ActivatedRoute into the component constructor:

// user.component.ts  
import { Component } from '@angular/core';  
import { ActivatedRoute } from '@angular/router';  

@Component({  
  selector: 'app-user',  
  template: `<h2>User ID: {{ userId }}</h2>`  
})  
export class UserComponent {  
  userId: string;  

  constructor(private route: ActivatedRoute) {  
    // Snapshot: Get parameters once (use for static data)  
    this.userId = this.route.snapshot.paramMap.get('id');  

    // Observable: Subscribe to parameter changes (use for dynamic updates)  
    this.route.paramMap.subscribe(params => {  
      this.userId = params.get('id');  
    });  
  }  
}  
  • Snapshot: Use route.snapshot for one-time access (e.g., initial load).
  • Observable: Use route.paramMap.subscribe() if the parameter might change (e.g., navigating from /user/123 to /user/456 without reloading the component).

5. Child Routes: Nested Navigation

Child routes enable nested UI layouts (e.g., a dashboard with tabs like /dashboard/stats and /dashboard/settings).

Step 1: Configure Child Routes

Define child routes using the children property in a parent route:

// app-routing.module.ts  
const routes: Routes = [  
  {  
    path: 'dashboard',  
    component: DashboardComponent,  
    children: [  
      { path: 'stats', component: StatsComponent }, // /dashboard/stats  
      { path: 'settings', component: SettingsComponent }, // /dashboard/settings  
      { path: '', redirectTo: 'stats', pathMatch: 'full' } // Default child route  
    ]  
  }  
];  

Step 2: Add a Child Router Outlet

In the parent component (DashboardComponent), add a <router-outlet> to render child components:

<!-- dashboard.component.html -->  
<h2>Dashboard</h2>  
<nav>  
  <a routerLink="stats">Stats</a> <!-- Relative path (to /dashboard) -->  
  <a routerLink="settings">Settings</a>  
</nav>  
<router-outlet></router-outlet> <!-- Child components render here -->  

6. Route Guards: Protecting Routes

Route guards control access to routes (e.g., restrict to authenticated users) or prevent accidental navigation (e.g., unsaved form changes). They implement interfaces like CanActivate, CanActivateChild, or CanDeactivate.

6.1 canActivate: Control Access to Routes

Use canActivate to restrict access to a route (e.g., only logged-in users).

Step 1: Create an Auth Guard

Generate a guard with the CLI:

ng generate guard auth  

Select CanActivate when prompted. Update the guard to check authentication:

// auth.guard.ts  
import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';  

@Injectable({ providedIn: 'root' })  
export class AuthGuard implements CanActivate {  
  constructor(private router: Router) {}  

  canActivate(  
    route: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot  
  ): boolean {  
    const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; // Simplified check  

    if (!isLoggedIn) {  
      this.router.navigate(['/login']); // Redirect to login if not authenticated  
      return false;  
    }  
    return true;  
  }  
}  

Step 2: Apply the Guard to a Route

Add the guard to the route in app-routing.module.ts:

const routes: Routes = [  
  {  
    path: 'profile',  
    component: ProfileComponent,  
    canActivate: [AuthGuard] // Only authenticated users can access  
  }  
];  

6.2 canActivateChild: Protect Child Routes

canActivateChild works like canActivate but applies to all child routes of a parent.

// auth.guard.ts (add CanActivateChild)  
import { CanActivateChild } from '@angular/router';  

export class AuthGuard implements CanActivate, CanActivateChild {  
  // ... canActivate logic  

  canActivateChild(  
    childRoute: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot  
  ): boolean {  
    return this.canActivate(childRoute, state); // Reuse canActivate logic  
  }  
}  

Apply to the parent route:

{  
  path: 'admin',  
  component: AdminComponent,  
  canActivateChild: [AuthGuard], // Protects all child routes  
  children: [/* ... */]  
}  

6.3 canDeactivate: Prevent Unsaved Changes

canDeactivate prevents navigation away from a component if there are unsaved changes (e.g., a form).

Step 1: Create a CanDeactivate Guard

Generate a guard and select CanDeactivate:

ng generate guard unsaved-changes  

Step 2: Define a Component Interface

Create an interface for components that use the guard:

// unsaved-changes.guard.ts  
import { CanDeactivate } from '@angular/router';  

export interface CanComponentDeactivate {  
  canDeactivate: () => boolean | Promise<boolean>;  
}  

@Injectable({ providedIn: 'root' })  
export class UnsavedChangesGuard implements CanDeactivate<CanComponentDeactivate> {  
  canDeactivate(component: CanComponentDeactivate): boolean | Promise<boolean> {  
    return component.canDeactivate();  
  }  
}  

Step 3: Implement the Interface in the Component

// edit-profile.component.ts  
import { Component } from '@angular/core';  
import { CanComponentDeactivate } from './unsaved-changes.guard';  

@Component({ /* ... */ })  
export class EditProfileComponent implements CanComponentDeactivate {  
  hasUnsavedChanges = false;  

  canDeactivate(): boolean | Promise<boolean> {  
    if (this.hasUnsavedChanges) {  
      return confirm('You have unsaved changes. Do you want to leave?');  
    }  
    return true;  
  }  
}  

Step 4: Apply the Guard to the Route

{  
  path: 'edit-profile',  
  component: EditProfileComponent,  
  canDeactivate: [UnsavedChangesGuard]  
}  

7. Lazy Loading: Optimizing Performance

Lazy loading loads feature modules only when the user navigates to their routes, reducing the initial bundle size and improving load times.

Step 1: Create a Feature Module with Routing

Generate a feature module (e.g., ProductsModule) with its own routing module:

ng generate module products --routing  

Step 2: Define Routes in the Feature Module

Update products-routing.module.ts with the feature’s routes:

// products-routing.module.ts  
import { NgModule } from '@angular/core';  
import { RouterModule, Routes } from '@angular/router';  
import { ProductsListComponent } from './products-list/products-list.component';  
import { ProductDetailComponent } from './product-detail/product-detail.component';  

const routes: Routes = [  
  { path: '', component: ProductsListComponent }, // /products  
  { path: ':id', component: ProductDetailComponent } // /products/123  
];  

@NgModule({  
  imports: [RouterModule.forChild(routes)], // Use forChild() for feature routes  
  exports: [RouterModule]  
})  
export class ProductsRoutingModule { }  

Step 3: Lazy Load the Module in the Root Routing Module

In app-routing.module.ts, use loadChildren to lazy load the feature module:

const routes: Routes = [  
  // ... other routes  
  {  
    path: 'products',  
    loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)  
  }  
];  

Now, ProductsModule (and its components) will only load when the user navigates to /products.

8. Advanced Routing Techniques

8.1 Route Resolvers: Fetch Data Before Navigation

Resolvers fetch data before a component loads, ensuring the component has data when it initializes.

Step 1: Create a Resolver

Generate a resolver:

ng generate resolver user-resolver  

Step 2: Implement the Resolver

// user-resolver.resolver.ts  
import { Injectable } from '@angular/core';  
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';  
import { UserService } from './user.service';  
import { User } from './user.model';  

@Injectable({ providedIn: 'root' })  
export class UserResolver implements Resolve<User> {  
  constructor(private userService: UserService) {}  

  resolve(route: ActivatedRouteSnapshot): Promise<User> {  
    const userId = route.paramMap.get('id');  
    return this.userService.getUserById(userId); // Fetch data from API  
  }  
}  

Step 3: Attach the Resolver to a Route

// app-routing.module.ts  
const routes: Routes = [  
  {  
    path: 'user/:id',  
    component: UserComponent,  
    resolve: { user: UserResolver } // Resolver key: 'user'  
  }  
];  

Step 4: Access Resolved Data in the Component

// user.component.ts  
import { Component } from '@angular/core';  
import { ActivatedRoute } from '@angular/router';  
import { User } from './user.model';  

@Component({ /* ... */ })  
export class UserComponent {  
  user: User;  

  constructor(private route: ActivatedRoute) {  
    this.user = this.route.snapshot.data.user; // Get resolved data  
  }  
}  

8.2 Query Parameters: Passing Optional Data

Query parameters (e.g., ?page=1&sort=asc) are used for optional data like pagination or search filters.

Add Query Parameters

<!-- Navigate with query params -->  
<a [routerLink]="['/products']" [queryParams]="{ page: 1, sort: 'name' }">Products</a>  

Access Query Parameters

// products.component.ts  
import { ActivatedRoute } from '@angular/router';  

constructor(private route: ActivatedRoute) {  
  // Snapshot (one-time access)  
  const page = this.route.snapshot.queryParamMap.get('page');  

  // Observable (updates on navigation)  
  this.route.queryParamMap.subscribe(params => {  
    this.page = params.get('page');  
    this.sort = params.get('sort');  
  });  
}  

8.3 Route Transitions: Animating Navigation

Use Angular’s animation system to animate route transitions (e.g., fade, slide).

Step 1: Define an Animation

// route-animations.ts  
import { trigger, transition, style, animate } from '@angular/animations';  

export const routeTransition = trigger('routeTransition', [  
  transition('* <=> *', [ // Apply to all route changes  
    style({ position: 'relative' }),  
    animate('0.3s ease', style({ opacity: 1 }))  
  ])  
]);  

Step 2: Apply the Animation to Router Outlet

// app.component.ts  
import { Component } from '@angular/core';  
import { routeTransition } from './route-animations';  

@Component({  
  selector: 'app-root',  
  template: `  
    <nav>...</nav>  
    <router-outlet [@routeTransition]></router-outlet>  
  `,  
  animations: [routeTransition]  
})  
export class AppComponent { }  

9. Best Practices for Angular Routing

  • Modularize Routes: Organize routes into feature modules (e.g., ProductsRoutingModule).
  • Use Lazy Loading: Reduce initial bundle size by lazy loading non-critical modules.
  • Handle 404s: Always include a wildcard route (**) for 404 pages.
  • Prefer Observables Over Snapshots: Use route.paramMap.subscribe() for dynamic parameter updates.
  • Avoid Deep Nesting: Limit child routes to 2–3 levels for readability.
  • Test Route Guards: Ensure guards correctly block/unblock routes (e.g., authenticated vs. unauthenticated users).

10. References


By mastering Angular routing, you’ll build robust, performant, and user-friendly SPAs. Start with the basics, experiment with dynamic routes and guards, and optimize with lazy loading—your users (and your bundle size) will thank you! 🚀