Catalin Ciubotaru avatar

Centralized navigation in Angular with a SiteNavigation Service

Links, links everywhere!

The Problem

An application can have a lot of internal linking. The more the application grows, the more the internal linking setup grows. Inevitably you run into situations where you repeat yourself a lot, creating the same long URL over and over. To make things even worse, if for some reason, the path for one of your features changes, you have to find all the places in your app that link to that feature, and change them. That is not fun!

Good to know before jumping in

This solution applies mainly to an Angular setup, but can be adapted to any framework out there. This is purely a subjective approach. We're all different, we all see things in a different way. This is not a tutorial.

The Solution

The SiteNavigationService. No, this thing does not exist, YET. But we'll create it together.

Now, what do we want from it?

  • We want a service available everywhere
  • That can give us links to different features within our app(and not only)
  • That can handle dynamic links as well(with parameters, query parameters etc.)
  • That is easy to test

Now, let's take these in order:

A Service that's available everywhere


Well, this is an easy one. We have to make sure we mark the service with providedIn: root and Angular will take care of the rest. Something like this:

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

@Injectable({providedIn: 'root'})
export class SiteNavigationService {}

This way, Angular will create the service and make sure it's available the first time it's needed. Angular will also make the service a singleton. Again, nice 👍.

A Service that gives us links do different features


Now, how would we use this service? Ideally, there would be a method that returns the path that we need. Let's say for example, in several places we need the link to the logged-in user dashboard. A solution would be something like this:

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

@Injectable({providedIn: 'root'})
export class SiteNavigationService {
getUserDashboardLink(): string[] {
return ['/', 'account', 'dashboard'];
}
}

That's about it. Whenever we need a link to the user dashboard, we call this method, and we have it.

A Service that can handle dynamic links


What if we need the link to the user public profile. Well, this can't be a static link anymore, since we have(hopefully) many users on our platform. Easy to solve though; we introduce parameters.

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

@Injectable({providedIn: 'root'})
export class SiteNavigationService {
getUserPublicProfileLink(userId: string): string[] {
return ['/', 'users', userId, 'profile'];
}
}

Again, pretty straight forward. We receive the dynamic part as a parameter and use that when building the URL.

A Service that is easy to test


If you followed my previous articles, you probably already have the tests written(TDD baby). If not, you can start here: test driven development in an angular world.

For the above code, the tests are quite easy, which adds to the appeal of this solution. They could look something like this:

it('should return the user dashboard link', () => {
const userDashboardLink = service.getUserDashboardLink();

expect(userDashboardLink).toEqual(['/', 'account', 'dashboard']);
});

it('should return the user public profile link', () => {
const userIdToUse = 'bruce-wayne';

const userPublicProfileLink = service.getUserPublicProfileLink(userIdToUse);

expect(userPublicProfileLink).toEqual( ['/', 'users', userIdToUse, 'profile']);
});

It's so easy, it feels like cheating.

Just for argument’s sake, let's make it a bit more complicated


What if your website has localized links. So, the link for UK would be 'account/dashboard', but the link for Spain would be 'cuenta/tablero'(sorry, I used Google Translate on this one 😅).

Well, since everything is centralized, this is also easy to solve, since we need to change only one place: SiteNavigationService

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

@Injectable({providedIn: 'root'})
export class SiteNavigationService {
private readonly country: Country;

private readonly accountMap = {
UK: 'account',
ES: 'cuenta'
}

private readonly dashboardMap = {
UK: 'dashboard',
ES: 'tablero'
}

constructor(localizationService: LocalizationService){
this.country = localizationService.country;
}

getUserDashboardLink(): string[] {
return ['/', this.accountMap[this.country], this.dashboardMap[this.country];
}
}

type Country = 'UK' | 'ES';
interface UrlSegmentMap {
[countryCode: Country]: string;
}

There's a bit of information here, so let's unpack. First, we have a new type Country which is a Union type. Then, we have the UrlSegmentMap which allows us to create objects that have a specific property for each country. We then use this to create the accountMap and the dashboardMap.

After all this is done, it's simply a matter of using the maps instead of the hardcoded string in the already existing methods. That's about it.

How do you use this service


In order to use this magnificent service we created, we need 2 parts. The first part is injecting it into the component where we need it:

@Component({
selector: 'app-some-component',
templateUrl: './some.component.html',
styleUrls: ['./some.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SomeComponent {

userDashboardRouterLink = this.siteNavigationService.getUserDashboardLink();

constructor(private siteNavigationService: SiteNavigationService) {}
}

And then, in the HTML:

<div class='user-link'>
<a [routerLink]="userDashboardRouterLink">Go to dashboard</a>
</div>

That's it. Enjoy your navigations! ⛵️

Want more?

If you want to know more about this, or something doesn't make any sense, please let me know.

Also, if you have questions, you know where to find me… On the internet!

Be kind to each other! 🧡

Over and out

My Twitter avatar
Catalin Ciubotaru 🚀

⛵️ A solution to losing track of URLs within your Angular app, and how to centralize them in a reusable and testable way. https://catalincodes.com/posts/centralized-navigation #Angular #Typescript #Testing

Sep 18, 2022
11 people are talking about this

Wanna read more?

Updates delivered to your inbox!

A periodic update about my life, recent blog posts, how-tos, and discoveries.

No spam - unsubscribe at any time!