Updated on

One of every web application’s main features is navigation, and to enable it in our project, we need to use routing. The Angular Router enables navigation from one view to the next as users perform application tasks.

In our navigation menu, we are going to have three options: one for the home screen, another one for the owner operations, and the last one for the account operations. We build the menu with Bootstrap classes and the ngx-bootstrap collapse directive, and then we hand our URLs to the Angular router so each one renders its own component.

This part continues from Angular Components and Project Setup, where we created the project and its first component.

For the complete navigation and all the basic instructions of the Angular series, check out: Introduction of the Angular series.

To download the source code for this article, you can visit the AngularRoutingAndNavigation folder in our GitHub repository. The source code for the whole Angular series is here.
This part builds the client only, so there is no API to start yet. From the next part onward the client talks to the Web API we built in the first half of this series, whose final state lives in the UsingRepositoryForWriteRequests folder.

Create a Navigation Menu

So, let’s start by creating a new Menu component:

ng g component menu --skip-tests

We are going to use Bootstrap classes to implement the navigation menu within the menu.html file:

<div class="row">
  <div class="col">
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
      <div class="container-fluid">
        <a class="navbar-brand" href="#">Account-Owner Home</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#collapseNav"
          aria-controls="collapseNav" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="collapseNav">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <a class="nav-link" href="#">Owner Actions </a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#">Account Actions </a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </div>
</div>

Currently, we are not going to modify the menu.ts file.

But, we are going to change our app.html file:

<div class="container">
  <div class="row">
    <div class="col">
      <app-menu></app-menu>
    </div>
  </div>
  <div class="row">
    <div class="col">
      <app-home></app-home>
    </div>
  </div>
</div>

One more step, and it is the one the older version of this article never needed. Our components are standalone, so each one declares what its own template is allowed to use. The App component’s template now names two selectors, app-menu and app-home, so both classes have to sit in its imports array in app.ts:

import { Home } from './home/home';
import { Menu } from './menu/menu';

@Component({
  imports: [Home, Menu],
  selector: 'app-root',
  styleUrl: './app.css',
  templateUrl: './app.html',
})

Leave that array out and the build stops on this very template, with Angular reporting that app-menu is not a known element.

Now, we can start our Angular project with ng serve -o.

As soon as the project runs, we are going to see our menu on the screen:

Angular Navigation menu

There it is.

Add Collapse Functionality to the NavBar

Right now, if we shrink our screen, we will be able to see the hamburger button, which once we click on it, should show our menu items. But it doesn’t because we didn’t install all the JavaScript parts for Bootstrap. And we don’t want to.

What we want is to use ngx-bootstrap as much as we can with all the components it provides for us. If you would rather build the same menu on a different component library, we have a walkthrough of a responsive navigation menu built with Angular Material as well.

That said, we are going to use the Collapse directive from ngx-bootstrap to enable our collapsable menu.

The directive is standalone, so the component that uses it imports the class itself. Let’s open menu.ts and add both the import and the property that holds the menu’s state:

import { Component } from '@angular/core';
import { CollapseDirective } from 'ngx-bootstrap/collapse';

@Component({
  imports: [CollapseDirective],
  selector: 'app-menu',
  styleUrl: './menu.css',
  templateUrl: './menu.html',
})
export class Menu {
  isExpanded = false;
}

Do not reach for CollapseModule.forRoot() here, which is what older Angular tutorials register in an app.module.ts file. On ngx-bootstrap 22 that call no longer exists, and there is no app.module.ts to put it in either, so copying it gives a compile error rather than a deprecation warning.

And finally, we are going to modify our NavBar HTML code:

<div class="row">
  <div class="col">
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
      <div class="container-fluid">
        <a class="navbar-brand" href="#">Account-Owner Home</a>
        <button class="navbar-toggler" type="button" (click)="isExpanded = !isExpanded"
          [attr.aria-expanded]="isExpanded" aria-controls="collapseNav">
          <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="collapseNav" [collapse]="!isExpanded" [isAnimated]="true">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <a class="nav-link" href="#">Owner Actions </a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#">Account Actions </a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </div>
</div>

Here we add the (click) event, which flips the isExpanded property each time we click the hamburger button. Also, pay attention that the aria-controls attribute must have the same id value as our div below it. Additionally, in the mentioned div, we set the value for the [collapse] input to indicate the visibility of our content, and we set the animation to true with the [isAnimated] input.

The exclamation mark in [collapse]="!isExpanded" is worth one sentence, because the input reads backwards from the property. [collapse]="true" hides the content, so the value we pass is “is this collapsed”, while the property we keep is “is this open”. Naming the property isExpanded keeps the click handler and the aria-expanded binding readable, and leaves the single negation in the one place it belongs.

That is all. Now we can shrink the screen and once the hamburger button appears, we can click on it and see our menu items.

How Do We Configure Routing in an Angular Application?

Routing in Angular is one array and one provider. The array maps URL paths to components, and the provider hands that array to the router when the application starts.

The array lives in app.routes.ts. Each entry names a path and what to do when the URL matches it: render a component, redirectTo another route, or load something on demand.

The provider lives in app.config.ts, as provideRouter(routes) in the providers array. That is the whole registration step, and there is no module involved in it.

Order matters. The router walks the array top to bottom and takes the first match, which is why a wildcard route always goes last.

Rendering happens in <router-outlet>. Whichever component the matched route names is created inside that tag, so the outlet marks the part of the page that changes while the menu around it stays put.

The component holding the outlet has to import RouterOutlet for the tag to work.

Both files already exist, because ng new writes them. So all we have to do is fill the array in the app.routes.ts file:

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

import { Home } from './home/home';

export const routes: Routes = [
  { path: 'home', component: Home },
  { path: '', redirectTo: '/home', pathMatch: 'full' }
];

And this is the app.config.ts file that hands the array to the router:

import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes)
  ]
};

So, we add two routes inside the routes array. The first one means that on the http://localhost:4200/home address we are going to serve the Home component. The second one is the default redirection to the home page, and it is the one entry that needs pathMatch: 'full': without it, the empty path would match as a prefix of every URL in the application and redirect all of them.

Those two properties are the ones this project uses, and a Route object accepts several more:

PropertyWhat it doesExample
pathThe URL segment this route matchespath: 'home'
componentThe component rendered when the path matchescomponent: Home
loadComponentLoads a standalone component only when the route is visitedloadComponent: () => import('./owner/owner').then(m => m.Owner)
loadChildrenLoads a child route array only when neededloadChildren: () => import('./owner/owner.routes').then(m => m.routes)
childrenNested routes rendered in a nested outletchildren: [ ... ]
redirectToSends the browser to another route instead of renderingredirectTo: '/home'
pathMatch'prefix' by default; 'full' is required alongside redirectTo on an empty pathpathMatch: 'full'
titleSets the browser tab title for the routetitle: 'Home'
canActivateGuards that decide whether the route may be enteredcanActivate: [authGuard]
'**' as the pathWildcard: matches anything no earlier route matched, so it goes last{ path: '**', component: NotFound }

Lazy loading is part 10’s subject, so the loadComponent and loadChildren rows are here for reference and we do not use them yet.

The path from a click to a rendered component is four steps long.

Clicking a routerLink hands the URL to the Angular router, which matches it against the routes array and renders the matched component inside router-outlet.

Now, to enable content from the routes, we have to modify the app.html file:

<div class="container">
  <div class="row">
    <div class="col">
      <app-menu></app-menu>
    </div>
  </div>
  <div class="row">
    <div class="col">
      <router-outlet></router-outlet>
    </div>
  </div>
</div>

Angular’s outlets guide defines the tag we just added: “The RouterOutlet directive is a placeholder that marks the location where the router should render the component for the current URL.” The same guide is precise about where that content lands. The outlet element stays in the DOM as a reference point for the next navigation, and Angular inserts the routed component just after it, as a sibling rather than a child. So everything that lives at the address we are routing to appears at that spot, and the menu above it stays put.

That tag comes from the router, so App imports it the same way it imported our own components. And because the template no longer names app-home, the Home class comes back out of the array: the home screen now arrives through the home route instead:

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

import { Menu } from './menu/menu';

@Component({
  imports: [RouterOutlet, Menu],
  selector: 'app-root',
  styleUrl: './app.css',
  templateUrl: './app.html',
})

Now if we navigate to localhost:4200 we should be able to see the same result as before, but this time, we are providing our home component through the <router-outlet> and not the <app-home> selector.

Additionally, if we click on any other menu item, we will be automatically redirected to the home page.

How Do We Style the Active Link in an Angular Menu?

routerLinkActive adds a CSS class to an element while its routerLink matches the current URL, and removes it when the URL changes.

The three pieces work together. [routerLink]="['/home']" says where the link goes, routerLinkActive="active" names the class to apply while it is current, and the stylesheet decides what that class looks like.

By default the match is a prefix match, so a link to /home stays highlighted on /home/details as well. [routerLinkActiveOptions]="{exact: true}" narrows it to an exact URL match, which is what a link back to the root of the application usually wants.

Using routerLink instead of href is what makes any of this work. href reloads the whole page and the router never sees the navigation, while routerLink hands the URL to the router, which swaps the component rendered at the outlet.

The component needs both RouterLink and RouterLinkActive in its imports.

In our menu, that means changing the brand anchor in the menu.html file:

<a class="navbar-brand" [routerLink]="['/home']" routerLinkActive="active"
          [routerLinkActiveOptions]="{exact: true}">Account-Owner Home</a>

Both directives belong to the router, so the Menu component’s imports array picks them up beside the collapse directive it already has:

import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { CollapseDirective } from 'ngx-bootstrap/collapse';

@Component({
  imports: [RouterLink, RouterLinkActive, CollapseDirective],
  selector: 'app-menu',
  styleUrl: './menu.css',
  templateUrl: './menu.html',
})

Now in the menu.css file, we are going to add the .active class:

.active {
  font-weight: bold;
  font-style: italic;
  color: #fff;
}

Excellent. If we inspect our application, we are going to see that the Account-Owner Home link is now white, bold and italic, and that it keeps that styling only while we are on the home page.

How Do We Show a 404 Page for Unknown Routes?

A wildcard route catches every URL no other route matched, and it is how an Angular application shows a 404 page.

The path is two asterisks: { path: '**', component: NotFound }. Because the router takes the first matching entry, the wildcard only ever fires after every real route has failed, which is why it goes last in the array.

There are two ways to finish it. Rendering the component at ** directly keeps the URL the user typed on screen, which helps when they mistyped one character. Redirecting to a real /404 route, as this project does, gives a page they can bookmark and link to.

Either way the component is ordinary. It has no routing knowledge, takes no parameters, and is generated with the same command as any other.

A wildcard route placed anywhere but last will swallow the routes below it, and the symptom is every URL showing the 404 page.

So, to complete the Angular routing part of this post, let’s execute the familiar command:

ng g component error-pages/not-found --skip-tests

The CLI creates the folder and three files inside it:

CREATE src/app/error-pages/not-found/not-found.ts (206 bytes)
CREATE src/app/error-pages/not-found/not-found.css (0 bytes)
CREATE src/app/error-pages/not-found/not-found.html (25 bytes)

Let’s modify the not-found.ts file:

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

@Component({
  selector: 'app-not-found',
  styleUrl: './not-found.css',
  templateUrl: './not-found.html',
})
export class NotFound {
  notFoundText = `404 SORRY COULDN'T FIND IT!!!`;
}

We have to pay attention to the string value of the notFoundText property. We are not using apostrophes but backticks (`). All the content inside the backticks will be considered as a string, even the apostrophe sign in the string.

To continue, let’s modify the not-found.html file:

<p>
  {{notFoundText}}
</p>

Also, we need to modify the not-found.css file:

p {
  font-weight: bold;
  font-size: 50px;
  text-align: center;
  color: #f10b0b;
}

Finally, we are going to change the content inside the routes array:

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

import { Home } from './home/home';
import { NotFound } from './error-pages/not-found/not-found';

export const routes: Routes = [
  { path: 'home', component: Home },
  { path: '404', component: NotFound },
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: '**', redirectTo: '/404' }
];

There are two changes here. With the first change, we declare the 404 path and assign the NotFound component to that path, so our component is going to be visible on localhost:4200/404. The second change means that whenever we ask for any route that doesn’t match any of our defined routes, the application redirects us to the 404 page.

Notice that the wildcard entry carries no pathMatch. The router exempts ** from that check, so the property is ignored there rather than merely redundant, and the empty-path entry above it is the only one that needs it. Its position in the array is what does the work, and Angular’s Define routes guide states the rule behind that: “When you define routes, the order is important because Angular uses a first-match wins strategy.”

Typing localhost:4200/whatever takes us to localhost:4200/404 and the not-found page. We may also navigate to localhost:4200/404 ourselves, and the app shows us the same page.

Conclusion

As you might have noticed, creating the menu and using the routing in an Angular project is pretty straightforward. Although we are not creating a large project, it is quite big enough to demonstrate the usage, configuration, and routing of all the pages we currently have. Of course, we are going to create routes for all new pages that we introduce to our project.

In the next part of the series, Angular HttpClient and Environment Files, we fetch data from the API and wrap those calls in a service.

Tested with Angular 22.1.5, Angular CLI 22.1.7, ngx-bootstrap 22.0.0, Bootstrap 5.3.8, TypeScript 6.0.3 and Node 24.