We use cookies to enhance your experience on the site
CodeWorlds

Advanced routing - reports from the trail

Your trail is already cut, @name. The routes are written out, parameters are read off the scroll, sentries stand at the gates, and the dojo has rooms inside rooms. And yet the journey is still uncomfortable - in several ways at once.

You click "Armory" in the navigation and for half a second nothing happens. The screen just sits there, because the browser is fetching the armory's bundle of code and a resolver is waiting on data. The user has no idea whether the click even registered, so they click again. On top of that you have twelve browser tabs open and every one of them carries the same name, because you set the page title once, in

index.html
. And finally the war council: the panel with the conversation should sit off to the side and not go dark every time the main view travels from the dojo to the armory and back.

In the Academy no march happens in silence. When the column sets out, the post at the gate beats a drum; when it arrives, the destination gate answers with a beat of its own. Scouts leave ahead of the main body so that supplies are already waiting at the next camp. Above every gate hangs a cartouche with the name of the place, and beside the main gate there is often a small side gate through which you enter the council without leaving the courtyard. In this lesson you will add exactly those four things to your trail.

Drums at the posts - router events

Let us start with the silence during a transition. The

Router
service, which in the lesson on path parameters gave you a way to issue the order to march, has a passive side as well: the
events
field. It is a stream - the same kind of stream you saw when reading parameters - so you sign up for it with the
subscribe()
method, and Angular calls your function on every new report. The difference is that this time you do not receive a map of parameters but an object describing a stage of the journey, and every stage has its own class.

Two classes are enough to build a loading bar. Angular sends

NavigationStart
the moment navigation begins - right after a link is clicked or
navigate()
is called.
NavigationEnd
arrives at the very end, once the route has been matched, the sentries have let the traveller through, the data has been fetched and the component has landed in the outlet. Memorise those names to the letter: there is nothing in the
@angular/router
package called
RouteStart
or
NavigationBegin
. I checked it with the compiler - importing either name ends with an error about a missing export and the application will not even build.

To recognise which object has just arrived we will use the

instanceof
operator. It is a plain JavaScript operator: it asks whether the object on the left is an instance of the class on the right, and answers true or false. I set the subscription up in the constructor rather than in
ngOnInit
, because the very first navigation starts the instant the application boots and I want to hear that drum too.

1import { Component, inject } from '@angular/core';
2import { Router, NavigationStart, NavigationEnd } from '@angular/router';
3
4@Component({
5  selector: 'app-root',
6  standalone: true,
7  template: '<p>Academy</p>'
8})
9export class AppComponent {
10  private router = inject(Router);
11  isLoading = false;
12
13  constructor() {
14    this.router.events.subscribe(event => {
15      if (event instanceof NavigationStart) {
16        this.isLoading = true;
17      }
18      if (event instanceof NavigationEnd) {
19        this.isLoading = false;
20      }
21    });
22  }
23}

The

isLoading
field now always agrees with the state of the journey: true while it lasts, false once it is over. Read the inside of that subscription line by line, because that is exactly the order it sits in the file: first the line that opens the subscription with a function taking the event, then the condition that lights the bar, then the condition that puts it out, and at the end the brace that closes the whole call. Notice what this change did not touch - the route configuration looks the same, the destination components know nothing about the bar, the sentries work unchanged. The subscription only listens in on the reports and blocks nothing: if you deleted it, navigation would run identically, just without telling the user anything.

Between the drum and the answering gate Angular sends a few more reports, and it is worth knowing they are there. I ran the router and wrote the stream down: after

NavigationStart
comes
RoutesRecognized
(the router now knows which route matches), then
GuardsCheckStart
and
GuardsCheckEnd
- those are your sentries from the lesson on guards, with reports about component activation in between - then
ResolveStart
and
ResolveEnd
, the work of the resolver from the previous lesson, and only at the very end
NavigationEnd
. The entire life of a route, which you have been learning in pieces throughout this module, flows through this one field.

A logical flag on its own is not enough - the user has to see something. In the template of the root component I show the bar conditionally, with the

@if
syntax from the lesson on components, and underneath it I leave the main outlet exactly where it stood.

1@if (isLoading) {
2  <div class="loader">Messenger on the way...</div>
3}
4
5<router-outlet></router-outlet>

From now on every transition between routes lights that message up and puts it out once the destination component is on screen. The place where rendering happens did not change:

<router-outlet>
is still the same gate in which the active route's component appears, and the bar is only scaffolding standing next to it. Take a good look at the shape of that tag while it is in front of you, because you will be assembling it from pieces soon enough: an opening angle bracket, the tag name
router-outlet
, a closing angle bracket, and at the end the closing tag.
isLoading
is an ordinary class field and Angular refreshes the view by itself when it changes - in the module about smoke signals you will turn it into a signal and see why that is the better idea.

There is a trap in this code and it is better to fall into it now than in production. I checked it on a running router: when a sentry turns the user back from the gate, Angular does not send

NavigationEnd
. What arrives instead is
NavigationCancel
, and the address in the bar stays where it was. Your condition never runs,
isLoading
stays true, and the loading bar hangs on the screen until the end of the session.
NavigationError
, the report of a failure during the journey, ends the same way. So put the bar out on all three reports at once.

1// same constructor, two names added to the import from @angular/router
2this.router.events.subscribe(event => {
3  if (event instanceof NavigationStart) {
4    this.isLoading = true;
5  }
6  if (event instanceof NavigationEnd
7    || event instanceof NavigationCancel
8    || event instanceof NavigationError) {
9    this.isLoading = false;
10  }
11});

Now every journey ends with the bar going out, whether it reached its destination, was turned back by a sentry, or tripped on an error. The condition that lights the bar did not change by a single character - there is still exactly one start event and it is still called

NavigationStart
. I recommend this version as your default, @name: the variant with
NavigationEnd
alone looks tidier in a lesson, but in a real application with sentries at the gates it freezes the bar the first time somebody is refused entry.

Scouts ahead of the column - preloading routes

The second discomfort is waiting for the bundle of code. In the module about clans you set up lazy loading: a route with

loadComponent
or
loadChildren
does not go into the application's first bundle but travels separately, once the user actually goes there. Startup is instant because of it, but the cost does not vanish - it moves to the first click, and on a weak connection that is a few seconds of staring at the bar you have just built.

You can have your cake and eat it: let the bundles arrive in the background once the application is up and nothing is happening. That is the job of a preloading strategy. Angular ships two of them:

NoPreloading
, meaning fetch nothing in advance - and that is the default behaviour - and
PreloadAllModules
, meaning fetch every lazy route in the background. You hand the strategy to the router when you register it, with the
withPreloading()
function. This is one of the so-called router features, extra settings that
provideRouter()
accepts as further arguments after the array of routes. Read that call as one sentence: the name
withPreloading
, an opening parenthesis, the strategy name inside it, a closing parenthesis.

1// app.config.ts
2import { ApplicationConfig } from '@angular/core';
3import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
4import { routes } from './app.routes';
5
6export const appConfig: ApplicationConfig = {
7  providers: [
8    provideRouter(routes, withPreloading(PreloadAllModules))
9  ]
10};

Notice that what goes into

withPreloading()
is the class name itself, with no
new
and no parentheses after it - Angular creates the instance of the strategy on its own. And now the important part, namely what did not change here: the array of routes is untouched, the entries with
loadComponent
are still lazy, and the application's first bundle did not grow by a single byte. The only thing that changed is the moment of the fetch.

I checked this on a running router with one lazy route. Under the default strategy the code of that route was not fetched even once until I went there myself. After turning

PreloadAllModules
on, the same code fetched itself right after the first
NavigationEnd
, even though nobody visited that route - and a later visit did not fetch it a second time, because it was already waiting in memory. That is where the whole benefit comes from: a transition that used to take as long as downloading a bundle becomes instant.

When should you not do this? When the application has a heavy admin panel that an ordinary user will never open,

PreloadAllModules
drags it down to everybody and wastes their bandwidth. For an application the size of our Academy I recommend it without hesitation, because it costs one call; on larger projects you write your own strategy on top of the
PreloadingStrategy
interface and preload only the routes you choose.

A side gate next to the main one - named outlets

The third discomfort is the war council. You want a side panel with its own route sitting next to the main view, you want a change of the main view not to put it out, and you want a page refresh not to close it. Which means the panel's state has to live in the address, exactly like the state of the main view.

Up to now every template of yours had a single

<router-outlet>
, though nobody ever said there had to be only one. That default outlet has a name -
primary
- you simply never type it. Next to it you are allowed to put a second one carrying a
name
attribute, and in the route entry add an
outlet
field with the same name. The route then says: render me in that side gate, not in the main one. Let us start with the routes, because that is where the decision is made.

1// app.routes.ts
2import { Routes } from '@angular/router';
3import { DojoComponent } from './dojo/dojo.component';
4import { ChatComponent } from './chat/chat.component';
5
6export const routes: Routes = [
7  { path: 'dojo', component: DojoComponent },
8  { path: 'chat', component: ChatComponent, outlet: 'sidebar' }
9];

The entry with

outlet: 'sidebar'
is not a child of any route - it is a separate trail running parallel to the main one. Nothing about matching the address
/dojo
changes because of it: when you go there the router still picks
DojoComponent
and puts it in the main gate, and the side gate stays empty until somebody opens it. Now the template. The main outlet stays as it was, next to it I add a second one and give it exactly the name I wrote in the route.

1<main>
2  <router-outlet></router-outlet>
3</main>
4
5<aside>
6  <router-outlet name="sidebar"></router-outlet>
7</aside>

Two tags, one difference: the

name
attribute with a value in quotes. The one without the attribute is
primary
, the one with it serves routes marked as
sidebar
. The order of the pieces is always the same and is worth fixing in memory: the opening of the tag together with its name, then the attribute name with an equals sign, then the value in quotes, and at the end the part that closes the whole thing. I checked what happens when that last part is missing, because it is not obvious: Angular reports no error at all, it simply treats the rest of the template as the content of the outlet and inserts the route's component after it, which puts the elements on the screen in reverse order. The shorthand with a slash works too, but the full pair of tags is the basic form in Angular and I am sticking with it.

What remains is the link that opens that side gate. Instead of a plain path you give

routerLink
an object with an
outlets
field, and inside it a map: the name of the side gate and an array of the segments that should show up in it. Remember to have the
RouterLink
directive in the component's
imports
array, exactly as with ordinary links. I checked what happens without it: Angular refuses to bind the attribute, the anchor comes out with no address at all, and the only trace is a message in the console about
routerLink
not being a known property.

1<a [routerLink]="[{ outlets: { sidebar: ['chat'] } }]">Call the council</a>

I checked what the address looks like after that click, because the syntax is anything but self-explanatory: standing on

/dojo
you get
/dojo(sidebar:chat)
. The named side gate lands in parentheses after the main path, which is why it survives a page refresh and can be sent to somebody as a link. The most important part is the one you cannot see: the main gate did not even flinch -
DojoComponent
was on the screen and stays on the screen, only the panel's content was added. To close the side gate you navigate to it with
null
instead of an array, that is
{ outlets: { sidebar: null } }
, and the address goes back to a clean
/dojo
.

A cartouche above the gate - the page title

The fourth discomfort is the easiest one to repair. The page title - the one visible on the browser tab, in the history and in a screen reader - is in an ordinary project set once in

index.html
and never changes again. The router can look after it for you: a route entry accepts a
title
field, and once navigation finishes Angular writes that value into the title of the document.

Three other names come to mind in this spot and all three are wrong.

pageTitle
is a habit from other libraries, unknown in Angular.
head
brings the section of an HTML document to mind, but a route entry has never heard of anything like that.
name
sounds the most natural of the lot and that is exactly what makes it dangerous, because a route has no field of that name. I compiled each of those three attempts and each ends with the same message from TypeScript: no such property exists on the
Route
type. There is exactly one correct field and it is called
title
.

1// app.routes.ts, imports as above
2export const routes: Routes = [
3  { path: 'dojo', component: DojoComponent, title: 'Dojo - Samurai Academy' },
4  { path: 'armory', component: ArmoryComponent, title: 'Armory - Samurai Academy' }
5];

After entering

/dojo
the browser tab is now called "Dojo - Samurai Academy" - I ran this and the title of the document really is swapped on every transition. What does not change along with it: the view itself. The
title
field renders nothing, adds no heading to the template, and touches the tab bar and nothing else. The heading on the page you still have to write yourself.

A static string is enough for screens there is only one of. It goes worse with a warrior's profile: behind the route

samurai/:id
hide as many pages as you have samurai, and the title "Samurai profile" tells none of them apart. That is why
title
accepts not only text but a function as well. Angular calls it on every navigation and passes it the snapshot of the route - the same object from which you read
paramMap
in the lesson on parameters. Whatever string the function returns becomes the title.

1{
2  path: 'samurai/:id',
3  component: SamuraiDetailComponent,
4  title: (route) => 'Samurai #' + (route.paramMap.get('id') ?? '')
5}

Entering

/samurai/7
gives the title "Samurai #7" - I ran that one too and that is exactly what happens. You know the
??
operator from reading parameters: it protects you from the case where the parameter was missing and
get()
returned an empty value. What did not change is the way the parameter is read - the same
paramMap
, the same
get()
method, only called in the route configuration instead of in a component. You can replace the plus with a template string in backticks and the result will be identical; with a title this short I recommend the plus, because it does not make the reader hunt for braces.

An order with a note attached

One thing is left to close, and you have just seen it from the side of the address. If the address can carry the state of a side gate, then it can certainly carry a small note of the "open the profile straight on the skills tab" kind. Notes like that are query parameters, and when navigating from code you point them out with the

queryParams
field in the second argument of the
navigate()
method. The first argument stays as it was: an array of segments in which you give the warrior's number after the name of the route.

1import { Component, inject } from '@angular/core';
2import { Router } from '@angular/router';
3
4@Component({
5  selector: 'app-samurai-list',
6  standalone: true,
7  template: '<button (click)="goToSamurai(5)">View warrior</button>'
8})
9export class SamuraiListComponent {
10  private router = inject(Router);
11
12  goToSamurai(id: number): void {
13    this.router.navigate(['/samurai', id], {
14      queryParams: { tab: 'skills' }
15    });
16  }
17}

Clicking the button takes you to the address

/samurai/5?tab=skills
- the segment with the number after the slash, the note after the question mark. The route matches exactly as it would without the note, because query parameters take no part in matching, and the profile component will read the tab when it needs it. There is one more detail here worth understanding after the lesson on guards:
navigate()
returns a promise, and inside it a boolean. True means the navigation went through, false means it was turned back. I checked both variants, and they differ more than you might expect. A sentry returning
false
gives you false, the address in the bar stays as it was, and only
NavigationCancel
lands in the stream. A sentry returning a
UrlTree
- the variant recommended in the lesson on sentries - behaves differently: the promise resolves to true, because the redirect succeeded, the address changes to the target, and the stream shows
NavigationCancel
first and
NavigationEnd
for the new route right after it. If you do anything else after navigating, clear a form or show a message for instance, it is worth glancing at that value through
await
or
then()
.

The whole trail in four steps

At the end of the module let us lay the road out as a single sequence, because you have been assembling it piece by piece across five lessons. Everything starts from the

Routes
array: that is where you define the pairs of path and component, and along with them the parameters, the children, the guards, the resolvers and the titles. The array on its own does nothing - it is only a map lying on the table. Step two is registering that map in the application through
provideRouter(routes)
in the configuration file
app.config.ts
- only now does the router know that any trails exist at all, and this is where you add features such as
withPreloading()
. Step three belongs to the user: they click a link with
routerLink
or type the address in the browser bar. You can take that same step for them from code by calling one of two methods on the injected service:
navigate()
, which glues the route together out of an array of segments, or
navigateByUrl()
if you already have the whole address in one piece of text - the whole line is then
this
, a dot and the name of the service, a dot and the name of the method, and inside the parentheses the address in quotes. Step four belongs to the router: it matches the address against an entry in the array, walks it past the guards and the resolvers, and then renders the component in
<router-outlet>
. Whatever you add to routing later on, you add it to one of those four steps.

Remember, @name: the trail does not have to be silent - it beats drums at every post, sends scouts for supplies before the column moves, and writes the name of the place on the cartouche above the gate, and your task is to listen to those reports.

Go to CodeWorlds