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

[Angular | Storybook] Tailwind, Directives, Content Projection, Icons and i18n

Install packages npm i -D @nx/storybook npm i -D @storybook/angular Create config nx g @nx/storybook:configuration <project-name> I created the config inside the main app to share some configs/modules. My stories are located at libs/shared/ui. That's why I had to

[Angular | Storybook] Tailwind, Directives, Content Projection, Icons and i18n
Members Public

[Angular | RxJS] BehaviorSubject with custom states

Whenever I call an api, often my frontend has to go through some different visualization steps. Imagine you have some kind of search input and a list which displays the results but you want to show an loading indicator, an error hint and something different when the result is empty.

[Angular | RxJS] BehaviorSubject with custom states
Members Public

[Angular] Dynamic App Config and Translations in a Submodule

When I'm setting up a new angular application, I always start with translation support and an external app config json loader. You should never start a new app without an internationalization (i18n) system - even if you only support one language! We are going to use the package

[Angular] Dynamic App Config and Translations in a Submodule