Skip to content

Router Helper Service

The Router Helper Service addresses several common challenges in Angular routing:

  1. Centralized API: It provides a single, consistent API for managing route parameters, query parameters, and route data, reducing the need to directly interact with the Angular Router in multiple places or the browsers location API.

  2. Signal and Observable Support: It offers both signal-based and observable-based APIs, allowing developers to choose the best approach for their use case.

// Single route parameter as a signal
const id = this.routerHelper.getRouteParam('id');
console.log(id()); // Current value of 'id' parameter
// All route parameters as a signal
const params = this.routerHelper.getRouteParams();
console.log(params()); // { id: '123', category: 'books' }
// Single query parameter as a signal
const page = this.routerHelper.getQueryParam('page');
console.log(page()); // Current value of 'page' parameter
// All query parameters as a signal
const queryParams = this.routerHelper.getQueryParams();
console.log(queryParams()); // { page: '1', sort: 'desc' }
// Access route data as a signal
const data = this.routerHelper.getRouteData();
console.log(data()); // All resolved data from route
// Single route parameter as observable
this.routerHelper.getRouteParam$('id').subscribe((id) => {
console.log(id); // Value changes over time
});
// All route parameters as observable
this.routerHelper.getRouteParams$().subscribe((params) => {
console.log(params); // All parameter changes
});
// Single query parameter as observable
this.routerHelper.getQueryParam$('page').subscribe((page) => {
console.log(page); // Page changes over time
});
// All query parameters as observable
this.routerHelper.getQueryParams$().subscribe((params) => {
console.log(params); // All query parameter changes
});
// Access route data as observable
this.routerHelper.getRouteData$().subscribe((data) => {
console.log(data); // Route data changes
});
// Navigation end events with URL
this.routerHelper.navigationEnd$.subscribe((url) => {
console.log('Navigation completed to:', url);
});
// Navigation start events with URL
this.routerHelper.navigationStart$.subscribe((url) => {
console.log('Navigation starting to:', url);
});
// Current URL as a signal
const currentUrl = this.routerHelper.getCurrentUrl();
console.log(currentUrl()); // Current URL
// Current URL as observable
this.routerHelper.getCurrentUrl$().subscribe((url) => {
console.log('URL changed to:', url);
});
// Parent route as signal
const parentRoute = this.routerHelper.getParentRoute();
console.log(parentRoute()); // Parent route information
// Parent route as observable
this.routerHelper.getParentRoute$().subscribe((route) => {
console.log('Parent route changed:', route);
});
// Previous route as signal
const previousRoute = this.routerHelper.getPreviousRoute();
console.log(previousRoute()); // Previous route information
// Previous route as observable
this.routerHelper.getPreviousRoute$().subscribe((route) => {
console.log('Previous route changed:', route);
});
// Navigate back
this.routerHelper.back();
// Navigate to external URL
this.routerHelper.navigateOutside('https://example.com');
// Open in new tab/window
this.routerHelper.openNew('/some/route');
Router Helper Service Implementation
import { Injectable, inject, signal, computed } from '@angular/core';
import { Router, NavigationExtras, ActivatedRoute, NavigationEnd, NavigationStart, Params } from '@angular/router';
import { Location, DOCUMENT } from '@angular/common';
import { Observable, map, filter, shareReplay, startWith } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
@Injectable({
providedIn: 'root',
})
export class RouterHelperService {
private router = inject(Router);
private location = inject(Location);
private document = inject(DOCUMENT);
private activatedRoute = inject(ActivatedRoute);
// Navigation Events
readonly navigationEnd$ = this.router.events.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
map((event) => event.url),
shareReplay(1),
);
readonly navigationStart$ = this.router.events.pipe(
filter((event): event is NavigationStart => event instanceof NavigationStart),
map((event) => event.url),
shareReplay(1),
);
// URL Management
readonly getCurrentUrl$ = this.navigationEnd$.pipe(startWith(this.router.url));
readonly getCurrentUrl = toSignal(this.getCurrentUrl$, { initialValue: this.router.url });
// URL Management (Signals & Values)
currentUrlSignal() {
return computed(() => this.getCurrentUrl());
}
getCurrentUrlValue(): string {
return this.getCurrentUrl();
}
// Domain and Full URL Management
getDomain(): string {
return this.document.location.origin;
}
getFullUrl(): string {
return this.document.location.href;
}
getFullUrlForPath(path: string[]): string {
const relativePath = path.join('/');
const fullPath = relativePath.startsWith('/') ? relativePath : `/${relativePath}`;
return `${this.getDomain()}${fullPath}`;
}
getFullUrlForCurrentRoute(): string {
return `${this.getDomain()}${this.getCurrentUrlValue()}`;
}
// Build full URLs with query parameters and fragments
buildFullUrlWithQuery(path: string[], queryParams: Params): string {
const relativeUrl = this.buildUrlWithQuery(path, queryParams);
return `${this.getDomain()}${relativeUrl}`;
}
buildFullUrlWithFragment(path: string[], fragment: string): string {
const relativeUrl = this.buildUrlWithFragment(path, fragment);
return `${this.getDomain()}${relativeUrl}`;
}
buildFullUrlWithQueryAndFragment(path: string[], queryParams: Params, fragment: string): string {
const relativeUrl = this.buildUrlWithQueryAndFragment(path, queryParams, fragment);
return `${this.getDomain()}${relativeUrl}`;
}
// Domain utilities
getProtocol(): string {
return this.document.location.protocol;
}
getHostname(): string {
return this.document.location.hostname;
}
getPort(): string {
return this.document.location.port;
}
getHost(): string {
return this.document.location.host;
}
// Check if we're on localhost/development
isLocalhost(): boolean {
const hostname = this.getHostname();
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.');
}
// Check if we're using HTTPS
isSecure(): boolean {
return this.getProtocol() === 'https:';
}
// Get base URL (domain + port if applicable)
getBaseUrl(): string {
return this.getDomain();
}
// Route Parameters (Observable)
getRouteParam$(key: string): Observable<string | null> {
return this.activatedRoute.params.pipe(map((params) => params[key] || null));
}
private routeParams = toSignal<Params, Params>(this.activatedRoute.params, { initialValue: {} });
getRouteParams$(): Observable<Params> {
return this.activatedRoute.params;
}
// Query Parameters (Observable)
getQueryParam$(key: string): Observable<string | null> {
return this.activatedRoute.queryParams.pipe(map((params) => params[key] || null));
}
private queryParams = toSignal<Params, Params>(this.activatedRoute.queryParams, { initialValue: {} });
// Fragment Management
private fragment = toSignal(this.activatedRoute.fragment, { initialValue: null });
// Fragment (Observable)
getFragment$(): Observable<string | null> {
return this.activatedRoute.fragment;
}
// Fragment (Signal)
fragmentSignal() {
return computed(() => this.fragment());
}
// Fragment (Value)
getFragmentValue(): string | null {
return this.fragment();
}
getQueryParams$(): Observable<Params> {
return this.activatedRoute.queryParams;
}
// Route Data (Observable)
getRouteData$(): Observable<Record<string, unknown>> {
return this.activatedRoute.data;
}
private routeData = toSignal(this.activatedRoute.data, { initialValue: {} as Record<string, unknown> });
// Route Parameters (Signals)
routeParamSignal(key: string) {
return computed(() => this.routeParams()[key] || null);
}
routeParamsSignal() {
return computed(() => this.routeParams());
}
// Route Parameters (Values)
getRouteParamValue(key: string): string | null {
return this.routeParams()[key] || null;
}
getRouteParamsValue(): Params {
return this.routeParams();
}
// Query Parameters (Signals)
queryParamSignal(key: string) {
return computed(() => this.queryParams()[key] || null);
}
queryParamsSignal() {
return computed(() => this.queryParams());
}
// Query Parameters (Values)
getQueryParamValue(key: string): string | null {
return this.queryParams()[key] || null;
}
getQueryParamsValue(): Params {
return this.queryParams();
}
// Route Data (Signals)
routeDataSignal() {
return computed(() => this.routeData());
}
// Route Data (Values)
getRouteDataValue(): Record<string, unknown> {
return this.routeData();
}
// Parent Route Management
getParentRoute$(): Observable<ActivatedRoute | null> {
return this.activatedRoute.parent
? this.activatedRoute.parent.params.pipe(map(() => this.activatedRoute.parent))
: new Observable((observer) => observer.next(null));
}
private parentRoute = toSignal(this.getParentRoute$(), { initialValue: this.activatedRoute.parent });
parentRouteSignal() {
return computed(() => this.parentRoute());
}
getParentRouteValue(): ActivatedRoute | null {
return this.parentRoute();
}
// Previous Route Management
private previousRoute = signal<string | null>(null);
getPreviousRoute$(): Observable<string | null> {
return this.navigationEnd$.pipe(
map((currentUrl) => {
const previous = this.previousRoute();
this.previousRoute.set(currentUrl);
return previous;
}),
);
}
previousRouteSignal() {
return this.previousRoute.asReadonly();
}
getPreviousRouteValue(): string | null {
return this.previousRoute();
}
// Navigation Methods
navigateTo(path: string[], extras?: NavigationExtras): void {
this.router.navigate(path, extras);
}
// Navigation with Query Parameters
navigateWithQuery(path: string[], queryParams: Params, extras?: NavigationExtras): void {
this.router.navigate(path, {
queryParams,
...extras,
});
}
// Update only query parameters (preserve current route)
updateQueryParams(queryParams: Params, merge = true): void {
this.router.navigate([], {
queryParams,
queryParamsHandling: merge ? 'merge' : 'replace',
relativeTo: this.activatedRoute,
});
}
// Navigation with Fragment
navigateWithFragment(path: string[], fragment: string, extras?: NavigationExtras): void {
this.router.navigate(path, {
fragment,
...extras,
});
}
// Update only fragment (preserve current route)
updateFragment(fragment: string): void {
this.router.navigate([], {
fragment,
relativeTo: this.activatedRoute,
});
}
// Navigation with both Query Parameters and Fragment
navigateWithQueryAndFragment(path: string[], queryParams: Params, fragment: string, extras?: NavigationExtras): void {
this.router.navigate(path, {
queryParams,
fragment,
...extras,
});
}
// Remove specific query parameter
removeQueryParam(key: string): void {
const currentParams = this.getQueryParamsValue();
const updatedParams = { ...currentParams };
delete updatedParams[key];
this.router.navigate([], {
queryParams: updatedParams,
relativeTo: this.activatedRoute,
});
}
// Clear all query parameters
clearQueryParams(): void {
this.router.navigate([], {
queryParams: {},
relativeTo: this.activatedRoute,
});
}
// Clear fragment
clearFragment(): void {
this.router.navigate([], {
fragment: undefined,
relativeTo: this.activatedRoute,
});
}
// URL Construction Utilities
buildUrlWithQuery(path: string[], queryParams: Params): string {
const urlTree = this.router.createUrlTree(path, { queryParams });
return this.router.serializeUrl(urlTree);
}
buildUrlWithFragment(path: string[], fragment: string): string {
const urlTree = this.router.createUrlTree(path, { fragment });
return this.router.serializeUrl(urlTree);
}
buildUrlWithQueryAndFragment(path: string[], queryParams: Params, fragment: string): string {
const urlTree = this.router.createUrlTree(path, { queryParams, fragment });
return this.router.serializeUrl(urlTree);
}
// Check if current route matches
isCurrentRoute(path: string[]): boolean {
const urlTree = this.router.createUrlTree(path);
const currentUrlTree = this.router.parseUrl(this.router.url);
return this.router.serializeUrl(urlTree) === this.router.serializeUrl(currentUrlTree);
}
// Check if query parameter exists and has specific value
hasQueryParam(key: string, value?: string): boolean {
const currentValue = this.getQueryParamValue(key);
if (value === undefined) {
return currentValue !== null;
}
return currentValue === value;
}
// Check if fragment matches
hasFragment(fragment?: string): boolean {
const currentFragment = this.getFragmentValue();
if (fragment === undefined) {
return currentFragment !== null;
}
return currentFragment === fragment;
}
back(): void {
this.location.back();
}
navigateOutside(url: string): void {
this.document.defaultView?.open(url, '_self');
}
openNew(path: string | string[]): void {
const url = Array.isArray(path) ? path.join('/') : path;
const fullUrl = new URL(url, this.document.location.origin).href;
this.document.defaultView?.open(fullUrl, '_blank');
}
}
Create a Router Helper Service in Angular that simplifies route management by providing:
1. Signal-based APIs for route parameters, query parameters, and route data
2. Observable-based APIs for route parameters, query parameters, and route data
3. Navigation event tracking
4. URL management utilities
5. Route management utilities including parent and previous route tracking
6. Navigation methods for back navigation and external URLs
Include implementation details for handling:
- Type safety
- Route parameter consistency
- Data and resolver naming conventions
- Error handling and edge cases