Introduction
The goal here is to give a comprehensive introduction to the topic of routing via the Angular template using the RouterLink directive. Along the way, we’ll discover a few patterns and nuances to make development easier and give you, the developer, a clear route (get it? GET IT??) to mastery. By the end of this article, you will understand and be able to implement the following concepts:
- The basics of routing from the template
- Routing to different levels relative to the current route
- Adding named parameters, optional parameters, and fragments to routes
- Providing custom data to a route change without showing that data in the URL
- Styling elements based on the activated route
- Dictate how a route change should reflect in browser history
- Strategies for disabling a
RouterLink
instance in the template
Let’s dive in!
Setup
We’re going to keep it simple with the following route configuration. The majority of the code examples will use this configuration and any references to “the provided route config” are referencing this configuration:
const routes: Route[] = [ { path: "dashboard", component: DashboardComponent }, { path: "items", component: ItemsComponent, children: [{ path: ":id", component: ItemComponent }] } ];
There is also an example Stackblitz that illustrates some of these concepts and is useful as a visual companion. You are encouraged to clone it and make changes as you read this article!
Getting Started
Turning an element into a link is very straightforward: just add the routerLink
attribute to the element and give it a valid Angular route as the value. Here are a few examples:
<!-- A Link --> <a routerLink="dashboard">Dashboard</a> <!-- A Button --> <button routerLink="dashboard">Dashboard</button> <!-- An Image --> <img routerLink="dashboard" src="https://picsum.photos/200/30">
These examples would result in the following compiled DOM elements:
<!-- A Link --> <a href="/dashboard">Dashboard</a> <!-- A Button --> <button tabindex="0">Dashboard</button> <!-- An Image --> <img tabindex="0" src="https://picsum.photos/200/30" />
The RouterLink
directive translates the given route into an href
attribute for anchor tags, and simply creates a click event handler and adds a tab index for other elements.
There is another, more flexible syntax for routes: the “array” syntax. This allows for specifying individual route segments as values in an array, which then get turned into strings and concatenated together to form the final URL. Here is an example of the array syntax and the resulting DOM element:
<!-- HTML --> <a [routerLink]="['items', customer.id, 'items', item.id]">{{item.name}}</a> <!-- Resulting DOM Element --> <a href="/customers/1/items/5">Some Item</a>
Quick Timeout
Now that I have just shown you how to turn any element into a link, I am going to tell you why you shouldn’t. Even though any HTML element can contain a RouterLink
directive, you should only add links to elements that you would normally use for navigation: which is basically the anchor (<a>
) tag. Why? One word: “accessibility”. The anchor tag is purpose-built to indicate a link to another view and using a different tag to indicate a navigation element can confuse assistive technology and result in a user not knowing that an element is a link. For more information, check out this link accessibility article.
Now that we understand the basics of template routing, let’s see how we can navigate in a slightly more complex scenario.
Relative linking
The URL paths provided to the RouterLink
directive follow the same convention as file paths. Specifically, prepending a forward slash at the front of the path makes that path “absolute” while adding no qualifiers or prepending the path with some combination of periods and forward slashes marks the path as “relative”. For example, to link to the dashboard, given the provided route config, all of the following are equivalent:
<a routerLink="/dashboard">Dashboard</a> <a routerLink="dashboard">Dashboard</a> <a routerLink="./dashboard">Dashboard</a> <!-- All of these will result in this DOM element --> <a href="/dashboard">Dashboard</a>
When using relative paths, the router calculates route changes against the currently activated route. So, given the provided route config, routing directly from items
to dashboard
would look like this:
<a routerLink="../dashboard">Dashboard</a> <!-- Which in this particular case would be equivalent to the following--> <a routerLink="/dashboard">Dashboard</a> <!-- And both of those would result in the following DOM element --> <a href="/dashboard">Dashboard</a>
Here is an example that uses relative routing and the array syntax:
<a [routerLink]="['../items', item.id]">{{item.name}}</a> <!-- Will result in the following DOM element --> <a href="/items/1">First Item</a>
Ok; so we understand how to navigate in a flexible manner but how do we pass data to a route?
Parameters
There are a few different ways to pass persistent state via the URL bar. Let’s investigate!
Named Parameters
Named parameters are parameters specified by a colon within a route definition. Let’s take an example from our original route definition:
{ path: "items", component: ItemsComponent, children: [{ path: ":id", component: ItemComponent }] }
In this example route, the path :id
is the path segment that the router will replace with an actual value whenever /items/:id
is the navigation target. Here’s an example:
<a [routerLink]="['items', item.id]">{{item.name}}</a> <!-- Results in the following DOM element --> <a href="/items/1">First Item</a>
But what if we want to pass some state in the URL without adding additional route configuration or if we want the values in the URL to be dynamic? The answer is “optional parameters”.
Optional Parameters
There are two types of optional parameters used in Angular routing: “query” parameters and “matrix” parameters. Let’s take a closer look at each.
Query Parameters
Query parameters are probably the most well-known optional parameter type. They are specified in a URL like so: https://example.com/route?param1=value¶m2=othervalue
. In frontend applications, some kind of parsing is generally employed to turn the query parameters into a JavaScript object.
Specifying query parameters from the RouterLink
directive is very straightforward. Simply add the queryParams
input to any element containing the RouterLink
directive and pass it a valid JS object. For example:
<a routerLink="dashboard" [queryParams]="{show: 'items'}">Show in dashboard</a> <!-- Will result in the following DOM element --> <a href="/dashboard?show=items">Show in dashboard</a>
Clicking this link would navigate to /dashboard?show=items
. At this point, you could respond to the query parameters in the dashboard component via ActivatedRoute.queryParams
(an example of consuming query params can be found in the DashboardComponent
in the sample app stackblitz).
When navigating with query params, we can specify how params from the source and destination routes should be handled via the queryParamsHandling
input on the RouterLink
directive. There are two options: “merge” or “preserve”. “Preserve” keeps the source query params when navigating to a new route, even at the expense of existing parameters. Take the following example given that the current route is /dashboard?show=items
:
<a routerLink="[../items]" [queryParams]="{foo: 'bar'}" queryParamsHandling="preserve">Items</a>
Clicking the above link will result in this URL /items?show=items
. The query params on the new route essentially get overwritten.
On the other hand, “merge” actually keeps params from both the source and destination. So, given the same source route and router link as above, the resulting URL will be /items?show=items&foo=bar
.
Let’s take a look at another optional parameter mechanism called “matrix” parameters.
Matrix Parameters
Matrix parameters are similar to query parameters but are more flexible in that they can be used at multiple URL segment levels. For example: /items;foo=bar/1;bar=baz
could be used to specify two hierarchical routes (“route” and “sub-route”), each with their own parameters.
Routing with matrix parameters is a bit different than query parameters. Instead of passing a separate input, the parameters are passed as JS objects within the RouterLink
route array itself; directly after the URL segment that they apply to. Here’s an example:
<a [routerLink]="['items', {foo: 'bar'}, item.id, {bar: 'baz'}]">{{item.name}}</a> <!-- Would result in the following DOM element --> <a href="/items;foo=bar/1;bar=baz">Example</a>
Clicking this link would result in the example URL given at the start of this section: /items;foo=bar/1;bar=baz
. These matrix parameters can be retrieved via ActivatedRoute.params
and remember: only the parameters specified at a particular level will be returned in the associated component. So, given the route shown above, when accessing ActivatedRoute.params
in the Items
component only {foo: 'bar'}
will be returned. Accessing ActivatedRoute.params
in the Item
component will yield {bar: 'baz'}
(along with {id: 1}
since the named parameter :id
is also defined on that route).
Next up, let’s see how to route to fragments with the RouterLink
directive.
Fragments
URI fragments allow linking to a specific element in a template by referencing that element’s ID in the URI, prefixed with a #
. For example, /dashboard#graph
would load the dashboard
route and then move the viewport down to the element with the ID graph
.
Linking to a fragment on a page in Angular is accomplished with the fragment
input property available on any element with the RouterLink
directive.
Here’s an example:
<a routerLink="dashboard" fragment="graph">Show in dashboard</a> <!-- Will result in the following DOM element --> <a href="/dashboard#graph">Show in dashboard</a>
If a fragment needs to be preserved across a route change, add the [preserveFragment]
input to the destination route and set it to true. So if the current route is /dashboard#graph
and we want to route to /items
but keep the same fragment, this would be the appropriate RouterLink
usage:
<a routerLink="items" [preserveFragment]="true">Items</a>
Clicking that link would navigate to /items#graph
, preserving the existing fragment.
We’ve seen how to persist state using the URL but how do we go about passing state from one route to the other without using the URL? Let’s check it out!
Routing With Data
Sometimes there is a need to pass state to a new route without actually putting that state in the URL. The tool for this is the state
input on the RouterLink
directive; which takes the provided data and sets it in the current browser History
entry. An example:
<a routerLink="items" [state]="{foo: 'bar'}">Items</a>
This will navigate to the items
route and pass {foo: 'bar'}
along with the rest of the navigation event data to the History
entry. After clicking that link, accessing history.state
in the browser console would return an object like this: {foo: "bar", navigationId: 4}
. Navigating to a different route and then clicking the back button would still result in {foo: "bar"}
being included in history.state
.
This state can be retrieved anywhere that the Router
is injected via router.getCurrentNavigation();
Once that object is returned, the actual state is retrieved by drilling down into extras.state
and grabbing whatever data was provided. Here’s an example:
export class ItemsComponent { constructor(private router: Router) { // Outputs "{foo: 'bar'}" to the console console.log('Current State', router.getCurrentNavigation().extras.state); } }
Let’s explore a couple of additional options that let give us fine-grained control over the URL and browser history.
Navigation Options
There are a couple inputs that allow us to control how navigation events are handled in the URL and subsequently browser history: skipLocationChange
and replaceUrl
. On the surface, they seem to do similar things but with subtle differences.
skipLocationChange
The skipLocationChange
input tells the router to perform the navigation “silently” without changing the URL or pushing a new state entry into history. Consider the following example, assuming that a user is starting on the root route (“/”):
<!-- User navigates to the dashboard using the following link --> <a routerLink="dashboard" [skipLocationChange]="true">Dashboard</a> <!-- User then navigates to items using the following link --> <a routerLink="items">Items</a>
When the user clicks “Dashboard”, the URL will not actually update and will stay at “/”. Furthermore, once the user clicks “Items” and then clicks the browser “Back” button, they will not be taken back to the dashboard page but will be taken back to the previous route or the root route /
. As far as the browser history is concerned, /dashboard
was never navigated to. Similarly, if a user then clicks the “Forward” button, they will jump straight back to /items
once again skipping /dashboard
.
replaceUrl
The replaceUrl
input tells the router to perform the navigation and reflect that change in the URL but, instead of adding a new routing event to the stack, it replaces the current URL in history. Consider the following example:
<!-- User navigates to the dashboard using the following link --> <a routerLink="dashboard">Dashboard</a> <!-- User then navigates to items using the following link --> <a routerLink="items">Items</a> <!-- User then navigates to a single item using the following link --> <a routerLink="1" [replaceUrl]="true">First Item</a>
At this moment, when the user clicks the browser back button, they will not be taken back to /items
but they will go back to /dashboard
. Similarly, going forward will skip /items
and navigate directly to /items/1
.
Though both skipLocationChange
and replaceUrl
seem to do pretty much the same thing, the difference lies in the route that is targeted for skipping. With skipLocationChange
, we are telling the browser: “don’t show the URL for this route or put it in browser history”. With replaceUrl
, we are telling the browser: “remove the previous route and URL from history once this route is activated”.
Let’s look at one last routing format, “Auxillary Outlets”.
Routing to Multiple Outlets
Auxillary (Aux) routing could be a whole article itself, so we’ll just cover the basics here. Consider the following route config with multiple routes and router-outlets:
// Route Config const ROUTES = [ { path: 'items', component: ItemsComponent, outlet: 'left' }, { path: 'widgets', component: WidgetsComponent, outlet: 'right' } ]
<!-- Router Outlets --> <router-outlet name="left" class="left-column"></router-outlet> <router-outlet name="right" class="right-column"></router-outlet>
<!-- Router Links --> <a [routerLink]=[{outlets: {left: 'items'} }]>Items</a> <a [routerLink]=[{outlets: {right: 'widgets'} }]>Widgets</a> <a [routerLink]=[{outlets: {left: 'items', right: 'widgets'} }]>Dashboard</a>
Clicking the first link would display just the “items” route on the left side of the page. Clicking the second link would show the “widgets” route on the right hand side of the page and clicking the last link would show both side by side effectively creating a dashboard. The URLs would be /(left:'items')
, /(right:'widgets')
and /(left:'items'//right:'widgets')
respectively; also, since the outlet is specified in the URL, we could actually deep link to the exact UI configuration we want, allowing us to create highly flexible UIs and keeping that flexibility in the URL. We could dive deeper but that’s for another time.
Now that we’ve done a deep dive into all the RouterLink
directive has to offer, it’s time to switch gears and get stylish with the RouterLinkActive
directive!
Styling Activated Routing Elements
We often want to give a particular routing element a conditional style based on whether or not the associated route is activated. This is accomplished with the RouterLinkActive
directive; available on any element that also has the RouterLink
directive. It takes either a string of whitespace-separated class names, or an array of class names, and appends those classes to that element when the associated route is active. Here are a couple examples:
<a class="nav-item" routerLink="items" routerLinkActive="active other">Items</a> <a class="nav-item" routerLink="items" [routerLinkActive]="['active', 'other']">Items</a> <!-- Both result in the following when the "items" route is activated --> <a href="/items" class="nav-item active other">Items</a>
By default, the RouterLinkActive
directive will mark an element as active when the active route merely contains the linked route. So, in the previous example, both /items
and /items/1
would result in the “active” classes being applied.
If an element should only be active when the route is matched exactly, add the routerLinkActiveOptions
input and set it to {exact: true}
. Here’s an example:
<a routerLink="items" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Items</a>
Now, the anchor element will only be activated when the route is /items
exactly. This means that /items/1
will not append the active classes to the element. Bear in mind that this also includes optional parameters; so, in the above example, a URL of /items?foo=bar
will not append the activated class.
The RouterLinkActive
directive is very flexible in that it will not just look at the element to which it is bound for the active route but will actually search for any child elements with the RouterLink
directive in order to calculate the active status. Consider the following:
<div routerLinkActive="active-container"> <a routerLink="dashboard">Dashboard</a> <a routerLink="items">Items</a> </div>
In this example, the containing div
will have the class active-container
if the URL contains either /dashboard
or /items
. Once again it is possible to leverage [routerLinkActiveOptions]="{exact: true}"
for a parent element to narrow down when an element should be considered “active”.
It is also possible to expose the underlying RouterLinkActive
directive as a template variable; which allows for complex use cases that the patterns we’ve talked about so far don’t cover. To expose the directive for use in the template, add a template variable to the element with the RouterLinkActive
directive and then assign the routerLinkActive
instance to that variable. Here’s an example:
<a routerLink="dashboard" routerLinkActive #rla="routerLinkActive"> Dashboard {{ rla.isActive ? '(already open)' : ''}} </a>
Once the directive is assigned to the template variable rla
, it can be accessed anywhere in the template and used to perform more complex operations on the active state of the link.
Let’s take a look at another pattern for styling links: disabling them.
Disabling a Router Link
Disabling a RouterLink
depends on the type of element to which it was added. For example, disabling a RouterLink
button is easy: just bind to the disabled
attribute of the button.
Most other elements, including anchor tags, don’t have a disabled
property to use yet. In the meantime, we recommend using an ng-if-else statement for elements that can’t be disabled. Here’s an example:
<span *ngIf="isDisabled; else regularLink">Dashboard</span> <a #regularLink routerLink="dashboard">Dashboard</a>
“`
Recap
After reading this article, you should now have a pretty thorough understanding of how to use the RouterLink
directive, the related RouterLinkActive
directive and how to achieve most template routing patterns that crop up in day-to-day development.
Specifically, we covered the following topics:
- The basics of routing from the template
- Routing to different levels relative to the current route
- Adding named parameters, optional parameters, and fragments to routes
- Providing custom data to a route change without showing that data in the URL
- Styling elements based on the activated route
- Dictate how a route change should reflect in browser history
- Strategies for disabling a
RouterLink
instance in the template
Thanks for reading!