Introduction to Angular Routing
Angular Router enables navigation from one view to another as users perform tasks in your single-page applications (SPAs). Instead of loading entirely new pages from the server, the router enables a “virtual page navigation” experience where only the necessary components are loaded and displayed while the rest of the page remains intact. This creates a smoother user experience and reduces load times.
Key Benefits of Angular Routing
- Single-Page Application Navigation: Navigate between views without full page reloads
- Lazy Loading: Load feature modules on demand to improve initial load time
- Route Guards: Protect routes from unauthorized access
- Route Parameters: Pass data between routes
- Child Routes: Create nested route hierarchies
- Route Animations: Add smooth transitions between routes
Setting Up Basic Routing
1. Installing the Router
The Angular Router is included when you create a new project with Angular CLI using the --routing
flag:
ng new my-app --routing
For existing projects, ensure @angular/router
is in your package.json
dependencies.
2. Creating a Routing Module
If you didn’t use the --routing
flag, create a routing module manually:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
// Define your routes
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent },
{ path: '**', redirectTo: '' } // Wildcard route for 404
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
3. Import Routing Module
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
AboutComponent,
ContactComponent
],
imports: [
BrowserModule,
AppRoutingModule // Import the routing module
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
4. Add Router Outlet
The <router-outlet>
directive tells Angular where to display the component for the currently activated route.
<!-- app.component.html -->
<header>
<nav>
<!-- Navigation links go here -->
</nav>
</header>
<main>
<router-outlet></router-outlet>
</main>
<footer>
<!-- Footer content -->
</footer>
Route Configuration Options
Basic Route Properties
Property | Type | Description |
---|---|---|
path | string | URL path segment for this route |
component | Component | Component to display for this route |
redirectTo | string | URL to redirect to |
pathMatch | ‘full’ | ‘prefix’ | How to match the URL (‘full’ = exact match) |
children | Routes[] | Child routes |
loadChildren | Function | Lazy loaded child routes |
canActivate | CanActivate[] | Guards for route activation |
canDeactivate | CanDeactivate[] | Guards preventing leaving a route |
data | object | Static data for the route |
resolve | object | Dynamic data resolvers |
Route Configuration Examples
const routes: Routes = [
// Basic route
{ path: 'products', component: ProductListComponent },
// Default route
{ path: '', component: HomeComponent, pathMatch: 'full' },
// Redirect route
{ path: 'old-path', redirectTo: 'new-path', pathMatch: 'full' },
// Route with parameters
{ path: 'product/:id', component: ProductDetailComponent },
// Route with static data
{ path: 'admin', component: AdminComponent, data: { roles: ['ADMIN'] } },
// Route with guard
{ path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] },
// Child routes
{
path: 'user',
component: UserComponent,
children: [
{ path: 'details', component: UserDetailsComponent },
{ path: 'orders', component: UserOrdersComponent }
]
},
// Lazy loaded routes
{
path: 'orders',
loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule)
},
// Wildcard route (404)
{ path: '**', component: PageNotFoundComponent }
];
Navigation and Router Links
Template-Based Navigation
Use the routerLink
directive to create links:
<!-- Basic link -->
<a routerLink="/home">Home</a>
<!-- Dynamic link -->
<a [routerLink]="['/products', product.id]">{{ product.name }}</a>
<!-- Relative routing (navigate within current route) -->
<a routerLink="../">Go up one level</a>
<a routerLink="./details">Go to details</a>
<!-- With Active Link Highlighting -->
<a routerLink="/products"
routerLinkActive="active-link"
[routerLinkActiveOptions]="{exact: true}">
Products
</a>
Programmatic Navigation
Navigate from your component using the Router service:
import { Router, ActivatedRoute } from '@angular/router';
@Component({...})
export class MyComponent {
constructor(
private router: Router,
private route: ActivatedRoute
) {}
// Basic navigation
goToHome() {
this.router.navigate(['/home']);
}
// Navigation with parameters
viewProduct(productId: number) {
this.router.navigate(['/products', productId]);
}
// Navigation with relative path
goToSibling() {
this.router.navigate(['../sibling'], { relativeTo: this.route });
}
// Navigation with query parameters
searchProducts(term: string) {
this.router.navigate(['/products'], {
queryParams: { search: term, category: 'all' }
});
}
// Navigation with fragment (hash)
goToSection() {
this.router.navigate(['/page'], { fragment: 'section1' });
}
// Preserving query parameters
navigatePreservingParams() {
this.router.navigate(['/new-route'], {
queryParamsHandling: 'preserve' // or 'merge'
});
}
}
Route Parameters and Data Passing
Route Parameters
Define a route with parameters:
{ path: 'product/:id', component: ProductDetailComponent }
Access parameters in your component:
import { ActivatedRoute } from '@angular/router';
@Component({...})
export class ProductDetailComponent implements OnInit {
productId: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// Snapshot approach (doesn't react to changes in the same component)
this.productId = this.route.snapshot.paramMap.get('id');
// Observable approach (reacts to parameter changes)
this.route.paramMap.subscribe(params => {
this.productId = params.get('id');
// Fetch product details using this.productId
});
}
}
Query Parameters
// Setting query parameters in the template
<a [routerLink]="['/products']" [queryParams]="{ category: 'electronics' }">
Electronics
</a>
// Setting query parameters programmatically
this.router.navigate(['/products'], {
queryParams: { category: 'electronics', sort: 'price' }
});
// Accessing query parameters
ngOnInit() {
// Snapshot approach
const category = this.route.snapshot.queryParamMap.get('category');
// Observable approach
this.route.queryParamMap.subscribe(params => {
const category = params.get('category');
const sort = params.get('sort');
// Filter products based on category and sort
});
}
Route Data
// Static data in route configuration
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
data: {
title: 'Admin Panel',
permissions: ['manage-users', 'manage-content']
}
}
];
// Accessing route data
ngOnInit() {
// Snapshot approach
const title = this.route.snapshot.data['title'];
// Observable approach
this.route.data.subscribe(data => {
const title = data['title'];
const permissions = data['permissions'];
// Use the data
});
}
Child and Nested Routes
Basic Child Routes
const routes: Routes = [
{
path: 'products',
component: ProductsComponent,
children: [
{ path: '', component: ProductListComponent },
{ path: 'new', component: ProductNewComponent },
{ path: ':id', component: ProductDetailComponent },
{ path: ':id/edit', component: ProductEditComponent }
]
}
];
In the parent component template, add a nested router outlet:
<!-- products.component.html -->
<div class="products-container">
<h1>Products</h1>
<nav>
<a routerLink="/products">All Products</a>
<a routerLink="/products/new">Add New Product</a>
</nav>
<!-- Child components will be rendered here -->
<router-outlet></router-outlet>
</div>
Nested Named Outlets
const routes: Routes = [
{
path: 'products',
component: ProductsComponent,
children: [
{ path: ':id', component: ProductDetailComponent },
{
path: ':id/edit',
component: ProductEditComponent,
children: [
{ path: 'info', component: ProductInfoComponent, outlet: 'details' },
{ path: 'specs', component: ProductSpecsComponent, outlet: 'details' }
]
}
]
}
];
In the template:
<!-- product-edit.component.html -->
<div class="product-edit">
<nav>
<a [routerLink]="['./', { outlets: { details: ['info'] } }]">Basic Info</a>
<a [routerLink]="['./', { outlets: { details: ['specs'] } }]">Specifications</a>
</nav>
<!-- Main content -->
<div class="edit-form">
<!-- Edit form fields -->
</div>
<!-- Named outlet -->
<div class="details-pane">
<router-outlet name="details"></router-outlet>
</div>
</div>
Lazy Loading Feature Modules
Setting up a Feature Module for Lazy Loading
- Create a feature module with its own routing:
// products/products-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductsComponent } from './products.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
const routes: Routes = [
{
path: '', // Empty path since the parent path is defined in app-routing
component: ProductsComponent,
children: [
{ path: '', component: ProductListComponent },
{ path: ':id', component: ProductDetailComponent }
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProductsRoutingModule { }
// products/products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductsRoutingModule } from './products-routing.module';
import { ProductsComponent } from './products.component';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
@NgModule({
declarations: [
ProductsComponent,
ProductListComponent,
ProductDetailComponent
],
imports: [
CommonModule,
ProductsRoutingModule
]
})
export class ProductsModule { }
- In your app routing module, use the
loadChildren
property:
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
];
Preloading Strategies
// app-routing.module.ts
import { PreloadAllModules } from '@angular/router';
@NgModule({
imports: [
// Preload all lazy loaded modules after the app loads
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule { }
Creating a custom preloading strategy:
// custom-preloading.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable()
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Only preload routes with data.preload set to true
return route.data && route.data['preload'] ? load() : of(null);
}
}
// app-routing.module.ts
const routes: Routes = [
{
path: 'frequently-used',
loadChildren: () => import('./frequently-used/frequently-used.module').then(m => m.FrequentlyUsedModule),
data: { preload: true }
},
{
path: 'rarely-used',
loadChildren: () => import('./rarely-used/rarely-used.module').then(m => m.RarelyUsedModule)
// No preload data, won't be preloaded
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: CustomPreloadingStrategy })],
exports: [RouterModule],
providers: [CustomPreloadingStrategy]
})
export class AppRoutingModule { }
Route Guards
Route guards protect routes from unauthorized access or prevent users from leaving a route with unsaved changes.
Types of Route Guards
Guard Type | Interface | Purpose |
---|---|---|
CanActivate | CanActivate | Controls if a route can be activated |
CanActivateChild | CanActivateChild | Controls if children routes can be activated |
CanDeactivate | CanDeactivate<T> | Controls if a user can leave a route |
Resolve | Resolve<T> | Pre-fetches data before route activation |
CanLoad | CanLoad | Controls if a lazy loaded module can be loaded |
Authentication Guard Example
// auth.guard.ts
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
if (this.authService.isLoggedIn()) {
return true; // Allow access
}
// Redirect to login page with return URL
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false; // Block access
}
}
Form Deactivation Guard Example
// can-deactivate.guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
@Injectable({
providedIn: 'root'
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(
component: CanComponentDeactivate
): Observable<boolean> | Promise<boolean> | boolean {
return component.canDeactivate ? component.canDeactivate() : true;
}
}
// In your component:
@Component({...})
export class EditProductComponent implements CanComponentDeactivate {
form: FormGroup;
originalData: any;
// Implementation of the canDeactivate method
canDeactivate(): boolean {
if (this.form.dirty && !this.form.pristine && !this.submitted) {
return confirm('You have unsaved changes. Do you really want to leave?');
}
return true;
}
}
Using Guards in Route Configuration
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
children: [
{ path: 'users', component: AdminUsersComponent },
{ path: 'reports', component: AdminReportsComponent }
]
},
{
path: 'product/:id/edit',
component: EditProductComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
product: ProductResolver
}
},
{
path: 'secret-module',
loadChildren: () => import('./secret/secret.module').then(m => m.SecretModule),
canLoad: [AuthGuard]
}
];
Resolvers
Resolvers pre-fetch data before activating a route, ensuring the component has the data it needs.
// product.resolver.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Product } from './product.model';
import { ProductService } from './product.service';
@Injectable({
providedIn: 'root'
})
export class ProductResolver implements Resolve<Product> {
constructor(private productService: ProductService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<Product> {
const id = route.paramMap.get('id');
return this.productService.getProduct(id).pipe(
catchError(error => {
console.error('Error retrieving product:', error);
return of(null); // Return default data or null
})
);
}
}
// Using the resolver in routes
const routes: Routes = [
{
path: 'product/:id',
component: ProductDetailComponent,
resolve: {
product: ProductResolver
}
}
];
// Accessing resolved data in the component
ngOnInit() {
this.route.data.subscribe(data => {
this.product = data['product'];
});
}
Route Animations
Add smooth transitions between routes:
// app.component.ts
import { Component } from '@angular/core';
import {
RouterOutlet,
RouterModule
} from '@angular/router';
import {
trigger,
transition,
style,
animate,
query,
group
} from '@angular/animations';
@Component({
selector: 'app-root',
template: `
<div class="page">
<router-outlet #outlet="outlet"></router-outlet>
</div>
`,
animations: [
trigger('routeAnimations', [
transition('* <=> *', [
// Initial state of new page
query(':enter', [
style({ opacity: 0, position: 'absolute' })
], { optional: true }),
// Move the old page out
query(':leave', [
style({ opacity: 1, position: 'absolute' }),
animate('300ms ease-out', style({ opacity: 0 }))
], { optional: true }),
// Move the new page in
query(':enter', [
animate('300ms ease-in', style({ opacity: 1 }))
], { optional: true })
])
])
]
})
export class AppComponent {
prepareRoute(outlet: RouterOutlet) {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
}
}
// Add animation data to routes
const routes: Routes = [
{ path: 'home', component: HomeComponent, data: { animation: 'HomePage' } },
{ path: 'about', component: AboutComponent, data: { animation: 'AboutPage' } }
];
Common Challenges and Solutions
1. Preserving Route State (scroll position, form values)
Using RouteReuseStrategy
to preserve component state:
// custom-route-reuse.strategy.ts
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
private storedRoutes = new Map<string, DetachedRouteHandle>();
// Whether the route should be stored
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.routeConfig.path === 'products'; // Store only products route
}
// Store the detached route
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
if (route.routeConfig.path) {
this.storedRoutes.set(route.routeConfig.path, handle);
}
}
// Whether the route should be retrieved
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!route.routeConfig && !!this.storedRoutes.get(route.routeConfig.path);
}
// Retrieve stored route
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this.storedRoutes.get(route.routeConfig.path);
}
// Whether the same route should be reused
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}
// Register in your app module
@NgModule({
providers: [
{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
]
})
2. Handling Permissions Based on User Roles
// role.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
// Get the required roles from the route data
const requiredRoles = route.data['roles'] as Array<string>;
// Check if the user has the required role
if (this.authService.hasRole(requiredRoles)) {
return true;
}
// Redirect to unauthorized page
this.router.navigate(['/unauthorized']);
return false;
}
}
// Using in route configuration
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, RoleGuard],
data: { roles: ['ADMIN', 'SUPER_ADMIN'] }
}
];
3. Managing Browser History and Browser Back Button
// Preventing navigation with window:beforeunload
@HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any): void {
if (this.form.dirty) {
$event.returnValue = true; // Browser will show confirmation dialog
}
}
// Disabling browser back button
import { Location } from '@angular/common';
constructor(private location: Location) {
// Override the back button behavior
this.location.onPopState(() => {
// Custom handling here
return false;
});
}
Best Practices for Angular Routing
Organize Routes Hierarchically
- Use feature modules with their own routing
- Utilize child routes for related components
Use Lazy Loading for Large Applications
- Each feature module should be lazy loaded
- Consider preloading strategies for better UX
Always Implement Guards for Protected Routes
- Use
CanActivate
for checking permissions - Use
CanDeactivate
for forms with unsaved changes
- Use
Handle Navigation Errors
- Create a 404 Not Found component
- Use wildcard routes to catch invalid URLs
Use Resolvers for Data Prefetching
- Prevent rendering components with missing data
- Show loading indicators during resolution
Maintain Clean URLs
- Use semantic route paths
- Prefer route parameters over query parameters for core resources
Use Route Data and Params Wisely
- Route parameters for resource identifiers
- Query parameters for filters, sorting, pagination
- Route data for static configuration
Implement Proper Navigation UX
- Provide breadcrumbs for deep hierarchies
- Highlight active routes
- Preserve user state when appropriate
Resources for Further Learning
Official Documentation
Community Resources
Tools
Books
- “Angular Router” by Victor Savkin
- “Angular Development with TypeScript” – Chapters on Routing