The Angular router allows to provide data on each route. This can be helpful to steer the behavior of application level components, e.g. header and footer. Issue is that this data is not typed by default. This may lead to error prone applications - at least if the number of routes to be supported and the complexity of data attributes increases. This article provides a step by step tutorial on how to achieve type-save provisioning of additional router data.
Angular Routes
A typical routing table in Angular may look like this:
export const routes: Routes = [
{
path: "",
component: SidenavComponent,
outlet: "sidenav",
},
{
path: "home",
component: HomeComponent,
},
{
path: "a",
component: TaskAComponent,
},
{
path: "b",
component: TaskBComponent,
},
{
path: "",
redirectTo: "home",
pathMatch: "full",
},
{
path: "**",
component: NotFoundComponent,
},
];
The routing table is an array of Route
-Objects. Each Route
-Object has a path
property and a component
property. The path
property is a string that defines the route. The component
property is the component that should be rendered when the route is activated.
In this example we have a route for the home page, a route for task A, a route for task B, a default route that redirects to the home page, and a route that is activated when no other route matches.
In Angular Routes
is declared as:
export declare type Routes = Route[];
So Routes
is an array of Route
-Objects.
Route
is declared as:
export declare interface Route {
...
path?: string;
...
/**
* Additional developer-defined data provided to the component via
* `ActivatedRoute`. By default, no additional data is passed.
*/
data?: Data;
...
}
And finally Data
is declared as:
export declare type Data = {
[key: string | symbol]: any;
};
So it is a property object or Record providing any data by a key. This is perfect for a generic interface, but requires high level of discipline to use this feature in large applications with non-trivial data.
Type-safe provisioning of route data for Angular application
The basic concept to provide type-safe route data in an Angular application is to extend the interface Route
and use this extended interface instead of Route
in the scope of the application.
In this case we want to provide structured data to an application-level header component.
So we define a new interface AppRoute
which extends Angular Route
.
/**
* Route in the application.
* - with typed data properties
*/
export interface AppRoute extends Route {
/** data property */
data?: AppRouteData;
}
And for convenience we define a type AppRoutes
as an array of AppRoute
-similar to Routes
in Angular:
/**
* AppRoutes type alias
* - list of AppRoute objects
* - provides typed data properties
*/
export type AppRoutes = AppRoute[];
For the header we define the interface
/**
* Header settings for a route
*/
export interface AppRouteDataHeader {
/**
* type of the header
*/
type: "toolbar" | "back_button";
/**
* title of the page
* - optional
*/
title?: string;
/**
* show logo in the header
* - not shown by default
* - if true, title will be omitted
*/
logo?: boolean;
}
Finally we aggregate all this in the interface AppRouteData
as
/**
* data property of the AppRoute
* - with typed header and footer properties
*/
export interface AppRouteData extends Data {
/**
* header settings
* - if not defined, the header is not presented
*/
header?: AppRouteDataHeader;
}
To apply this in the routing table we have to change the type of the routes
variable from Routes
to AppRoutes
and add the data properties to the routes:
export const routes: AppRoutes = [
{
path: "",
component: SidenavComponent,
outlet: "sidenav",
},
{
path: "home",
component: HomeComponent,
data: {
header: {
type: "toolbar",
title: "Demo-App Home",
},
},
},
{
path: "a",
component: TaskAComponent,
data: {
header: {
type: "back_button",
title: "Task A",
},
},
},
{
path: "b",
component: TaskBComponent,
data: {
header: {
type: "back_button",
title: "Task B",
},
},
},
{
path: "",
redirectTo: "home",
pathMatch: "full",
},
{
path: "**",
component: NotFoundComponent,
data: {
header: {
type: "back_button",
title: "Not Found",
},
},
},
];
With that, we are able to provide structured data to the header component. The header component can now be implemented in a type-safe way.
Header component consuming typed route data
The header component requires to access the route data.
It listens to the router events and filters NavigationEnd
events. It then extracts the route data from router state and provides it as a signal. This is a very simple implementation. More complex and flexible implementations are possible.
export class HeaderComponent {
...
/**
* DI
*/
public readonly sidenavService = inject(SidenavService);
private readonly router = inject(Router);
private readonly location = inject(Location);
...
/**
* Signal providing current AppRouteDataHeader settings
* - if null, the header is not shown
*/
public headerData: Signal<AppRouteDataHeader | null> = toSignal(this.router.events.pipe(
filter(event => (event instanceof NavigationEnd)),
map(() => {
/** Get the current route from router state */
let currentRoute = this.router.routerState.root;
while (currentRoute.firstChild) {
currentRoute = currentRoute.firstChild;
}
const appRouteData: AppRouteData = currentRoute.routeConfig?.data as AppRouteData;
return appRouteData?.header ?? null;
})
), { initialValue: null });
...
}
The header data can be accessed in the template as depicted below:
@if(headerData()?.type === 'toolbar') {
<mat-toolbar class="app-toolbar mat-elevation-z1">
<button (click)="sidenavService.open()" type="button" mat-icon-button><mat-icon>menu</mat-icon></button>
<span class="toolbar-content-main">
@if(headerData()?.logo === true) {
<a mat-button routerLink="/"><img src="img/AdvenageLogoPlain-s.svg"/></a>
} @else if (headerData()?.title) {
<span>{{ headerData()?.title }}</span>
}
</span>
<div style="width: 48px"></div>
</mat-toolbar>
} @else if(headerData()?.type === 'back_button') {
<mat-toolbar class="sub-toolbar mat-elevation-z1">
<button type="button" mat-icon-button (click)="backward()">
<mat-icon>west</mat-icon>
</button>
@if (headerData()?.title) {
{{ headerData()?.title }}
}
<div style="width: 48px"></div>
</mat-toolbar>
}
That's it. The header component is now able to access the route data in a type-safe way.
Conclusion
- Angular router allows to provide data on each route.
- By default, this data is not typed.
- This may lead to error prone applications.
- To achieve type-safe provisioning of additional router data, the interface
Route
can be extended. - The extended interface
AppRoute
can be used in the scope of the application. - The data properties can be structured in a way to fit application requirements.
- The header component can access the route data in a type-safe way.
- This approach can be applied to other application-level components as well.
- The approach is a simple and effective way to improve the quality of Angular applications.
Notes
- The code examples are based on Angular 18 and Angular-Material 18.
- They can be used with Angular 17 and Angular-Material 17 as well - but presentation may differ.
- The code examples are using -in 2024 - modern Angular features like:
- Signals
- Signal queries
- Control Flow in components
- Standalone components and Standalone API for sharpness and clarity.
- All this can be achieved with older Angular versions as well - but with more code and less clarity.