[Angular] Routing Helper - a Typesafe Routing Attempt
It always bothered me that you have to define the routing paths as strings. If a route changes, you have to change all references manually. There must be a better way!
Routing Constants
The constants are for your specific app - here you can define a class which represents your entire routing. Place it in an utils library, core folder or somewhere where your constants live.
export class RoutingMap {
ngmug = {
about: {},
};
headlessui = {
about: {},
hlAutocomplete: {
definition: {},
},
};
rxjs = {
about: {},
stateSubject: {},
};
routing = {
about: {},
parent: {
parentId: {
details: {
childId: {},
},
},
},
};
}
export const ROUTING: RoutingHelper<RoutingMap> = new RoutingHelper<RoutingMap>(
new RoutingMap()
);
Routing Helper
This should be the generic API - place it in a shared folder.
export const REDIRECTION: string = '**';
export const ID_ROUTE_END: string = 'Id';
export class RoutingHelper<T> {
private readonly PROP_PATH = '_pathProp';
private readonly PROP_NAVROUTE = '_navRoute';
private readonly IGNORE_PROPS = [this.PROP_PATH, this.PROP_NAVROUTE];
private routingMap: T;
constructor(routingMap: T) {
this.routingMap = routingMap;
this.init();
}
getPath(f: (x: T) => any): string {
return f(this.routingMap)[this.PROP_PATH] as string;
}
getNavRoute(f: (x: T) => any, ...ids: string[]): string[] {
const pathArray = f(this.routingMap)[this.PROP_NAVROUTE] as string[];
if (ids && ids.length > 0) {
let paramCounter = 0;
pathArray
.filter((p) => p.endsWith(ID_ROUTE_END))
.forEach((p) => {
const i = pathArray.findIndex((path) => path == p);
pathArray.splice(i, 1, ids[paramCounter]);
paramCounter++;
});
}
return pathArray;
}
private init() {
this.addRoutingProperties(this.routingMap, ['/']);
}
private addRoutingProperties(obj: any, prevNavRoute: string[]) {
const keys = Object.keys(obj);
keys
.filter((x) => !this.IGNORE_PROPS.includes(x))
.forEach((key) => {
let o = obj[key];
o[this.PROP_PATH] = key.endsWith(ID_ROUTE_END)
? `${prevNavRoute[prevNavRoute.length - 1]}/:${key}`
: key;
o[this.PROP_NAVROUTE] = [...prevNavRoute, key];
this.addRoutingProperties(o, [...prevNavRoute, key]);
});
}
}
Let's go through it step by step:
In the constructor we set the routingMap and initialize it. The method addRoutingProperties adds two helper properties to each path.
- _pathProp: path for routing definition
- _navRoute: contains the navigation string array
As you already recognized the API code isn't typesafe. (if you find a way, let me know!). So I added two methods to provide typesafe-like access to the helper properties.
getPath
Takes a lambda expression as parameter and returns the property name as string. Example usage:
{
path: ROUTING.getPath((p) => p.headlessui),
loadChildren: () =>
import('@ngmug/ngmug/feature-components').then(
(m) => m.FeatureComponentsModule
),
}
getNavRoute
Takes a lambda expression as parameter and returns the routerLink array.
Example usage:
headlessUiUrl: string[] = ROUTING.getNavRoute((p) => p.headlessui);
To handle param routes (e.g. :id) I implemented the constant ID_ROUTE_END. It defines that any route which ends with 'Id' should be handled like an param route. So you can do something like this:
routing = {
parent: {
parentId: {
details: {
childId: {},
},
},
},
};
- .getPath((p) => p.routing.parent.parentId) - will result in 'parent/:parentId'
- .getNavRoute((p => p.routing.parent.parentId.details.childId), '1', '2') - will result in ['/', 'routing', 'parent', '1', 'details', '2']
Routing Helper Source:
Implementation Example:
If you have further questions about this topic or need help with a problem in general, please write a comment or simply contact me at yesreply@georghoeller.dev :)
Georg Hoeller Newsletter
Join the newsletter to receive the latest updates in your inbox.