Angular Beginners Guide #3 — Routing

Brandon Wohlwend
11 min readOct 12, 2023

--

Have you ever clicked on a link in a web app and noticed the entire page doesn’t refresh, but instead, only a part of it changes? This magic trick is a core feature of Single Page Applications (SPAs), and Angular, a leading framework in this domain, has its own special toolkit for it: the Angular Router.

In traditional websites, clicking a link often results in a new page request to the server, causing the entire page to reload. While this is straightforward, it’s not the most efficient nor user-friendly approach for dynamic web applications. SPAs revolutionize this process by loading a single HTML page and dynamically updating its content as the user interacts, leading to smoother transitions and a more app-like experience.

Angular’s routing system allows developers to define paths for different content or views within the application. This means when you click on a link or navigate to a specific URL, Angular ensures the right content or view is displayed, all without a full page refresh.

But Angular’s routing is not just about switching views. It offers a range of features like passing data between routes, protecting certain routes from unauthorized access, and even lazy loading parts of an app only when they’re needed.

Throughout this article, we’ll explore how Angular’s routing system functions, how to set it up, and how to make the most of its features to create intuitive and efficient navigation for your Angular applications.

Setting Up Angular Routing

In the world of Angular, setting the foundation is crucial. Before we can navigate through our app, we need to set up the routing system. Let’s walk through the steps:

Ensure Necessary Packages are Installed

Angular’s CLI comes with everything you need for routing out-of-the-box when you generate a new project. However, if you need to add it manually or want to double-check, ensure you have the @angular/router package:

npm install @angular/router --save

Import the RouterModule

Before you can use routing, you need to import Angular’s RouterModule into your main application module or the module where you want to implement routing.

import { RouterModule } from '@angular/router';

@NgModule({
imports: [
RouterModule.forRoot([]) // We'll populate this array soon!
],
exports: [RouterModule]
})
export class AppModule { }

Here, the forRoot method is used to configure the router at the application's root level. It takes an array of routes, which we'll define shortly.

Define Your Routes

Routes in Angular are just an array of objects, with each object mapping a URL path to a component. Let’s say we have two components, HomeComponent and AboutComponent. Here's a basic route configuration:

import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' }, // default route
{ path: 'home', component: HomeComponent },
{ path: 'about', component: AboutComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppModule { }

Set Up Router Outlet

The <router-outlet> directive acts as a placeholder where the router should display the component for the current route:

<!-- app.component.html -->
<nav>
<a routerLink="/home">Home</a>
<a routerLink="/about">About</a>
</nav>
<router-outlet></router-outlet>

With routerLink, we've also introduced a directive to bind clickable elements to routes, allowing users to navigate between views.

Initialize Base href

Lastly, ensure you have the base href set in your index.html:

<base href="/">

This tells Angular to include the app’s base URL when navigating between views.

3. Basic Routing

Now that we have the foundation laid out for Angular routing, it’s time to get our hands dirty with some basic navigation. At its core, Angular routing is about mapping a URL to a component, allowing for smooth transitions between different parts of your application.

Defining Routes

To start, you’ll define your routes as an array of objects, where each object has a path and a component.

import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

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

Here:

  • The empty path is a default route that redirects to the home path.
  • pathMatch: 'full' ensures a full URL path match. This means that the router will only redirect to /home if the entire URL is empty.

Using RouterOutlet

The <router-outlet> directive acts as a dynamic placeholder, telling Angular where to load the routed component. Place it in your main component template (usually app.component.html).

<router-outlet></router-outlet>

Every time a user navigates to a path that matches one of your defined routes, the content in <router-outlet> updates to display the associated component.

Navigating with RouterLink

Instead of traditional HTML anchor tags, Angular provides the routerLink directive to link elements to routes:

<nav>
<a routerLink="/home">Home</a>
<a routerLink="/about">About</a>
</nav>

By clicking these links, Angular’s router takes care of updating the URL and loading the correct component in <router-outlet>, all without a full page reload.

Active Route Styling with RouterLinkActive

To give users visual feedback on the current route, Angular provides the routerLinkActive directive:

<nav>
<a routerLink="/home" routerLinkActive="active">Home</a>
<a routerLink="/about" routerLinkActive="active">About</a>
</nav>

By adding this directive, Angular will automatically append the class “active” to the link when its associated route is the current active route. Style the .active class as desired in your CSS.

Route Parameters

As you create more intricate applications, there will be times when you want to capture specific values from the URL, like IDs or other dynamic values. This is where route parameters come in.

Defining a Route with a Parameter

To define a route that captures a parameter, you use a colon (:) followed by the parameter name in the path. For example, if you want to capture a product ID:

const routes: Routes = [
// ... other routes
{ path: 'product/:id', component: ProductDetailComponent }
];

In this setup, both /product/1 and /product/abc would match the route, with 1 and abc being the captured parameters, respectively.

Accessing Route Parameters

To read the captured parameter value in your component, Angular provides the ActivatedRoute service. First, inject this service into your component:

import { ActivatedRoute } from '@angular/router';

constructor(private route: ActivatedRoute) { }

Then, you can access the parameters from the params Observable:

ngOnInit() {
this.route.params.subscribe(params => {
let productId = params['id'];
// Now, you can use this ID to fetch product details or perform other actions.
});
}

Optional Route Parameters

Sometimes, you want parameters to be optional. Angular’s router allows for this via query parameters:

<!-- Setting optional parameters -->
<a [routerLink]="['/products']" [queryParams]="{ order: 'desc' }">View Products</a>

This generates a link to /products?order=desc. To read these in your component:

ngOnInit() {
this.route.queryParams.subscribe(queryParams => {
let order = queryParams['order'];
});
}

Matrix Parameters

Matrix parameters are a unique type of parameter, allowing you to include key-value pairs within route segments. They look like this: /products;id=1;name=phone. While not as commonly used, they can be useful in specific scenarios.

To set matrix parameters:

<a [routerLink]="['/products', { id: 1, name: 'phone' }]">View Product</a>

And, similar to above, you can retrieve these values with the params Observable.

Child Routes & Nested Routing

As Angular applications grow in complexity, developers often find the need to structure routes in a hierarchical manner. Child routes and nested routing let you encapsulate parts of your application in a more organized and scalable structure.

The Need for Nested Routing

Consider an application with a user dashboard. Within this dashboard, you might have various sections: profile, settings, messages, etc. Instead of creating completely separate routes, you can use child routes to maintain a consistent layout while changing only the inner content.

Setting Up Child Routes

To establish child routes, you nest an array of routes within a parent route using the children property.

Let’s see a basic example:

const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: '', redirectTo: 'profile', pathMatch: 'full' },
{ path: 'profile', component: ProfileComponent },
{ path: 'settings', component: SettingsComponent }
]
}
];

When a user navigates to /dashboard/profile, the DashboardComponent will be loaded, and within that component, the ProfileComponent will be rendered.

Using <router-outlet> for Child Components

For child routes to render, the parent component (in our example, DashboardComponent) must have a <router-outlet> directive. This serves as a placeholder for the child components.

<!-- dashboard.component.html -->
<h1>Dashboard</h1>
<nav>
<a routerLink="./profile">Profile</a>
<a routerLink="./settings">Settings</a>
</nav>
<router-outlet></router-outlet> <!-- Child components will render here -->

Accessing Parameters from Parent Routes

Sometimes, child components need to access parameters from parent routes. Using our earlier ActivatedRoute example, this can be achieved as follows:

ngOnInit() {
this.route.parent.params.subscribe(params => {
let userId = params['id'];
// Use this ID as necessary
});
}

Auxiliary (Named) Outlets

Beyond the primary <router-outlet>, Angular allows for named outlets, which can display multiple components simultaneously in different sections of a page.

Define a route with an outlet:

{
path: 'chat',
component: ChatComponent,
outlet: 'auxiliary'
}

Include a named <router-outlet> in your template:

<router-outlet name="auxiliary"></router-outlet>

To navigate to a named outlet:

<a [routerLink]="[{ outlets: { auxiliary: ['chat'] } }]">Open Chat</a>

6. Route Guards

Route guards act as gatekeepers, allowing or denying access to specific routes based on certain conditions, like user authentication or feature access. They can be invaluable in applications where specific routes should only be accessible under certain circumstances.

Why Use Route Guards?

Imagine you have an admin panel in your app. You wouldn’t want unauthenticated users to access this panel, would you? Route guards can help prevent such unauthorized access.

Creating a Basic Guard

To generate a guard, you can use Angular CLI:

ng generate guard auth

This creates auth.guard.ts. In this file, you can define your guard logic. For simplicity, let's create a guard that denies access to a route:

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
canActivate(): boolean {
return false; // This guard always denies access
}
}

Applying a Guard to Routes

Once you’ve defined your guard, apply it to the routes you wish to protect:

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

In the example above, the AdminComponent route is protected by the AuthGuard.

Different Types of Guards

Angular provides several guard interfaces, each for specific scenarios:

  • CanActivate: Determines if a route can be activated.
  • CanActivateChild: Determines if child routes of a specific route can be activated.
  • CanDeactivate: Determines if you can exit a route, useful for preventing users from navigating away from unsaved changes.
  • CanLoad: Determines if a module can be loaded lazily.

Guard with Redirect

Often, when users are denied access, it’s good practice to redirect them, commonly to a login page. You can achieve this by injecting the Router into your guard:

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(): boolean {
// Example logic to check authentication
const isAuthenticated = false;
if (isAuthenticated) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}
}

Combining Multiple Guards

For more intricate scenarios, you might want to combine multiple guards. Angular will execute them in the order they’re provided and stop as soon as one returns false:

const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard, AnotherGuard, YetAnotherGuard]
}
];

7. Lazy Loading & Asynchronous Routing

In a growing Angular application, the size of the app can become a concern. If a user has to download the entire application to start using it, the initial load time can be slow. This is where lazy loading comes in.

What is Lazy Loading?

Lazy loading is a design pattern that defers the initialization of an object until it’s needed. In Angular, this concept is applied to modules, meaning you can split your application into multiple modules and load them on demand.

Benefits of Lazy Loading

  • Improved Initial Load Time: Only the necessary features are loaded initially, reducing the amount of data that needs to be fetched.
  • Optimized Bandwidth Usage: Users don’t download unnecessary code if they don’t access the part of the app that requires it.
  • Better User Experience: Faster load times mean a smoother experience for the end user.

Setting Up Lazy Loading

To implement lazy loading in Angular, follow these general steps:

  1. Create Feature Modules: Split your application into feature modules. You can generate a new module using Angular CLI:
ng generate module my-feature

2. Configure the Routes: In your main routing configuration, use the loadChildren property to point to the module you want to load lazily. This uses the Angular's dynamic import syntax.

const routes: Routes = [
{
path: 'my-feature',
loadChildren: () => import('./my-feature/my-feature.module').then(m => m.MyFeatureModule)
}
];

3. Remove References in App Module: Make sure the module is not referenced in your main AppModule, otherwise it won't be lazy-loaded.

Preloading Strategies

While lazy loading can improve initial load time, it means that when a user navigates to a lazily-loaded part of your app, they might experience a slight delay. Angular offers preloading strategies to combat this:

  • No Preloading (default): Nothing is preloaded. Everything is loaded on demand.
  • PreloadAllModules: All modules are preloaded, but after the main bundle.
  • Custom Preloading: You can define your custom logic for preloading, allowing for more intricate and strategic preloading based on user behavior or network conditions.

Implementing a Preloading Strategy

To use a preloading strategy, first, import it:

import { RouterModule, PreloadAllModules } from '@angular/router';

Then, provide it in your routing configuration:

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

Advanced Navigation Techniques

Angular provides numerous tools for managing navigation. Beyond the basics, there are several advanced techniques that can be used to handle more complex navigation scenarios.

Multiple Named Router Outlets

Sometimes, you may want to display multiple components simultaneously in different sections of the page. Named router outlets can help:

  1. Define Named Outlets: In your template, you can have more than one <router-outlet> by naming them.
<router-outlet></router-outlet>
<router-outlet name="sidebar"></router-outlet><router-outlet></router-outlet>
<router-outlet name="sidebar"></router-outlet>

2. Configure Routes: In your routes, target the named outlet.

const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'sidebar-content', outlet: 'sidebar', component: SidebarComponent }
];

Auxiliary Routes

Auxiliary routes allow you to navigate multiple routes at the same time. They work hand-in-hand with named router outlets. For instance, you could have a page layout with a sidebar that updates independently of the main content area.

Relative Navigation

Instead of always defining route paths absolutely, you can navigate relative to the current route:

this.router.navigate(['../'], { relativeTo: this.route });

This is particularly useful inside feature modules where you don’t necessarily know the full path.

Query Parameters and Fragments

Query parameters and fragments (hashes) allow you to pass additional data during navigation:

  • Query Parameters: Appended to the URL after a ?. Useful for non-essential information or filtering.
this.router.navigate(['/'], { queryParams: { session: '12345' } });
  • Fragments: Appended to the URL after a #. Useful for navigating to specific sections on the page.
this.router.navigate(['/'], { fragment: 'section1' });

Route Resolvers

Sometimes, before navigating to a component, you might want to ensure that certain data is available. Resolvers can help pre-fetch this data:

  1. Create a resolver service:
@Injectable({
providedIn: 'root',
})
export class DataResolver implements Resolve<DataType> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DataType> {
return this.dataService.fetchData();
}
}

2. Attach the resolver to the route:

const routes: Routes = [
{
path: 'data',
component: DataComponent,
resolve: { myData: DataResolver }
}
];

Route Animations

Angular allows you to animate transitions between routes. This can help make your application feel smoother and more dynamic. By utilizing Angular’s animation package, you can define animations that trigger when you enter or leave a route.

Conclusion

Navigating through the complex landscape of Angular routing can feel like a journey in itself. But as we’ve seen, Angular provides a robust and versatile set of tools designed to manage the challenges of modern web navigation. From the foundational concepts of setting up routes to the intricate details of advanced navigation techniques, we have embarked on an enlightening exploration.

What makes Angular’s routing system so powerful is its ability to scale with your application’s needs. Whether you’re crafting a small single-page application or a large enterprise-scale project, these routing techniques can be tailored to fit. They not only improve performance and efficiency but also enhance the user experience, making web applications more intuitive and engaging.

As you continue your development journey, remember that routing is just one piece of the Angular puzzle. Yet, it’s a vital one. With the knowledge you’ve gained from this article, you’re well-equipped to handle the diverse challenges of web navigation and to create seamless user experiences.

Remember, the world of web development is ever-evolving. Stay curious, keep exploring, and continue mastering new techniques. Happy coding!

--

--

Brandon Wohlwend

Mathematician | Data Science, Machine Learning | Java, Software Engineering