Skip to content

[Angular] Routing Helper - a Typesafe Routing Attempt

Georg Höller
Georg Höller
3 min read
[Angular] Routing Helper - a Typesafe Routing Attempt
Photo by Brendan Church / Unsplash

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()
);
https://github.com/Georg632/ngmug/blob/main/frontend/ngmug/libs/shared/ngmug-utils/src/lib/routing/ngmug-routing.const.ts

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]);
      });
  }
}
https://github.com/Georg632/gh-utils/blob/main/src/lib/routing/routing-helper.ts

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']

☝️
This code is by far not perfect - feel free to extend and change it! Suggestions for improvement are welcome in chat or by mail!

Routing Helper Source:

gh-utils/src/lib/routing at main · Georg632/gh-utils
Contribute to Georg632/gh-utils development by creating an account on GitHub.

Implementation Example:

ngmug/frontend/ngmug/libs/shared/ngmug-utils/src/lib/routing at main · Georg632/ngmug
Contribute to Georg632/ngmug development by creating an account on GitHub.
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 :)
AngularDeveloper

Comments


Related Posts

Members Public

[ASP.NET Core 6.0 | Angular] Hosting with Ionos

Update for my previous posts about hosting an angular frontend and ASP.NET Core 6 backend. Prerequirements: * active ionos windows hosting subscription * asp.net core 6 project * buildable angular project Ionos Setup Open up your favorite FTP client and connect it to your ionos webspace. Create two new folders -

[ASP.NET Core 6.0 | Angular] Hosting with Ionos
Members Public

Transition from Windows to Mac - Frontend Developer

My Windows journey started with Windows 98 I think, maybe Windows 95. Now it is the end of 2022 and I was in need for a new laptop. I have been searching for weeks for a suitable windows notebook, tested an Asus ROG X13 but sent it back because hardware

Transition from Windows to Mac - Frontend Developer
Members Public

[Tailwind | Angular] Responsive breakpoints in typescript

Tailwind breakpoints in typescript with an easy angular component

[Tailwind | Angular] Responsive breakpoints in typescript