Client-side routing is a key feature in single page applications. Learn how to implement routing in an Angular app with this practical guide.
Angular is a framework for building dynamic client-side applications using HTML, CSS, and JavaScript. It has a nice CLI tool that helps with developer productivity and with generating code that follows the recommended Angular design guide so you can build fast, responsive and modular applications. In this article, you'll learn how to implement routing in an Angular application and how to handle events (e.g button click event).
You should have an understanding of components, directives, modules, data binding, services, and dependency injection in Angular, as those concepts are needed for you to understand what I'm writing about. If you don't know those concepts, you're in good company because I've written about them . Here are the links to the articles I've written covering those topics:
- A Practical Guide To Angular: Environment and Project Set Up
- A Practical Guide To Angular: Components and Modules
- A Practical Guide To Angular: Data Binding & Directives
- A Practical Guide To Angular: Services & Dependency Injection
- A Practical Guide To Angular: Handling HTTP Operations
The application we'll build together while you go through this article builds on the expense tracking application I built from scratch while I wrote the articles listed above. If you have been reading and working along with me over those articles, you should have the complete code. Otherwise, you can download the project on GitHub. When you download it, you should then copy the content from the src-part-5 folder into the src folder if you want to code along while you read.
Adding the History Page
The application only has one page at the moment. That page allows users to view the current month's expenses. We're going to add a new page that will allow users to pick a month and see the expenses for the selected month.
Run the command ng g c expenses/history
to generate a new component. Paste the code below in the component's template file.
<div class="row">
<div class="col-sm-2">
Select Month:
</div>
<div class="col-md-4">
<input #month (keyup.enter)="getExpense(month.value)" type="month" />
</div>
</div>
<br />
<et-expense-list [expenses]="expenses" [showButton]="false"></et-expense-list>
This component will display an input element that will be used to select a month and a table that will display the expense for the selected month, rendered through the expense-list
component. We use event binding to handle the keyup event for the input element. Event binding is how you listen and subscribe to events in Angular.
The syntax consists of a target event name within parentheses on the left of an equal sign, and a quoted template statement on the right. In the code above it is written as (keyup.enter)="getExpense(month.value)"
. The keyup event will listen for every keystroke, but we want to only respond when the user presses the enter key. Angular provides the keyup.enter pseudo-event which is raised only when the enter key is pressed.
When the event is raised, it will call the getExpense()
function with the value of month.value
. The #month
declares a variable that references the input element and provides access to the element's data. With it, we can get the value of the element and pass it to the getExpense()
function.
Open history.component.ts and update the class with the code below.
import { Component } from "@angular/core";
import { ExpenseService } from "../expense.service";
import IExpense from "../expense";
@Component({
selector: "et-history",
templateUrl: "./history.component.html",
styleUrls: ["./history.component.css"]
})
export class HistoryComponent {
expenses: IExpense[] = [];
constructor(private expenseService: ExpenseService) {}
getExpense(period: string) {
if (period) {
this.expenseService.getExpenses(period).subscribe(
expenses => {
this.expenses = expenses;
},
error => {
console.log("Error retrieving expenses", error);
}
);
}
}
}
The implementation for getExpense
calls this.expenseService.getExpenses(period)
to get an array of IExpense
object and then assigns it to the property expenses
which is bound to the expense-list
component.
Configuring Routes
We have two components that represent two separate pages in the application — the Home component and the History component.
The next thing to do is to enable routing in the application so that users can navigate between pages. To do this, we will first configure the Angular router so that when a user navigates to specific paths, it should render the view of the component that is responsible for that route. The Angular router is managed by the Angular router service, and this service is registered in the RouterModule. The RouterModule also declares some router directives such as the RouterLink and RouterOutlet directives.
The project already has a module called AppRoutingModule
, which is where we will put any logic related to routing for the application. This module is declared in app-routing-module.ts and is included in the imports
array of the root app module.
@NgModule({
declarations: [
AppComponent,
BriefingCardsComponent,
ExpenseListComponent,
HomeComponent,
HistoryComponent
],
imports: [BrowserModule, AppRoutingModule, HttpClientModule],
providers: [],
bootstrap: [AppComponent]
})
In order to work with the router service and directives, we need to import the RouterModule. This module is included in the AppRoutingModule
’s imports
array as you can see in the file
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
const routes: Routes = [];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
The routes
variable is where you will define the routes for the application and its of type Routes
. The Routes
type represents an array of Route
objects. We will define routes for the application but one thing to notice is the RouterModule.forRoot(routes)
call. The forRoot
method exposes the routes to the root application.
Update the routes
variable with the value below.
const routes: Routes = [
{
path: "history",
component: HistoryComponent
},
{ path: "", component: HomeComponent },
{ path: "**", redirectTo: "/", pathMatch: "full" }
];
The Route
is an object that has a path
property which will match the URL path in the browser and a component
property which specifies the component that should be rendered when the route matches the specified path. The history
path maps to the History component, while the path with an empty string will match the default /
route, and is mapped to the Home component.
The **
path indicates a wildcard path, which is what gets called when the requested path in the URL doesn't match any of the defined routes. So if the user visits localhost:4200/dashboard which is not refined, we want to redirect them to the default route /
. That is why we specify the redirectTo
property, which denotes the path to redirect to when this route definition is matched.
Typically, you’d want to have an error page that non-existing paths gets routed to. The pathMatch
property is used to specify the path-matching strategy. By default, the router checks URL elements from the left to see if the URL matches a given path, and stops when there is a match. For example /team/11/user
matches team/:id
.
Add the following import statement to reference the Home and History components.
import { HistoryComponent } from "./expenses/history/history.component";
import { HomeComponent } from "./home/home.component";
Using the RouterLink and RouterOutlet Directives
Now that we have the routes defined, we want to add links that will enable users to navigate the app. The root App
component has a navigation header in the markup. We want the user to browse by clicking any of the two links which should redirect to the actual page. To do this, we're going to use the RouterLink directive. This directive is an attribute directive that we can add to the anchor tag.
Open the app.component.html and update lines 17 to 20 to include the RouterLink directive as an attribute with a value that matches a path.
<a class="nav-item nav-link active" routerLink="/">Home</a>
<a class="nav-item nav-link" routerLink="/history">History</a>
Using that attribute on the anchor tag gives the router control over that element.
We still need one more thing to complete the application. If the route is activated, the router needs to know where to place the view of the component it's supposed to render for that route. This is where you'll use the RouterOutlet directive. It is used as a component directive and indicates the spot in the view where the router should display the component for that route.
With the root App
component still open, change line 25 to use the RouterOutlet directive and not the Home
component directive.
<router-outlet></router-outlet>
That completes the changes we need to make to enable routing for a SPA Angular application. Test your work by running ng serve -o
command to start the Angular application.
Conclusion
Angular can be used to build single-page applications, and client-side routing is a key feature in such applications. In this article, I showed you how to implement routing in an Angular app. You saw how to define routes and use the RouterModule.forRoot()
method. You also learned how to use the RouterLink and RouterOutlet directives. There's so much more to routing than what I covered here, and you can read more in the documentation. I covered the fundamentals you need to know to get started building a SPA. If you run into complex requirements, you can check the documentation.
The code for this article can be downloaded from GitHub. It's contained in the src-part-6
folder. If you have any questions, feel free to leave a comment or reach out to me on Twitter.