that-routing-lib
TypeScript icon, indicating that this package has built-in type declarations

1.3.1 • Public • Published

that-routing-lib

Stop worrying about those URLs

Intro

This library provides an easy-to-use API to build URLs in your JS/TS-project. Creating a URL-string that links to a given location becomes as simple as

// "/articles/4/edit"
const editArticle4 = routesApi.articles.$articleId("4").edit();

with full Typescript support.

that-routing-lib has been designed with the Angular router in mind, but the client side functionality can be used to construct URLs for any setting.

Why use this lib?

Whenever we need a URL in our projects, we can typically choose to either

  • Write the URL out as a string
  • Use predefined constants for given URLs

The first method is prone to typos and difficult to adjust should things change eventually. The latter method is safe with regard to these problems, but poses the question of which parts of a URL to save as constants. The entire URL? Or each segment separately? Saving the entire URL, as in /topics/cooking/soup is easy to use, but requires a lot of variables for each possible URL, such as /topics/cooking/fish and /topics/travel/spain. When saving each segment, we avoid having one constant per leaf in our routes tree, but have no way of knowing whether TOPICS + '/' + FISH + '/' + SPAIN is a valid URL or not. And what about route parameters as in /topics/travel/articles/4?

We need a scheme that allows us to

  • centrally define all routes to avoid typos
  • preserve the structure of route segments and URL params
  • easily refactor code
  • access URLs easily with minimal risk of mistakes

that-routing-lib helps you in fulfilling all these requirements, by allowing you to specify your routes with minimal effort in a tree-like structure and providing functions to turn this data into function objects with a super nifty developer experience.

How to define your URL tree

In the simplest case, your URLs are defined as follows

import {buildRoutes} from './that-routing-lib';

const routesDefinition = {
    home: {},
    topics: {
        subRoutes: {
            cooking: {
                subRoutes: {
                    soup: {},
                    fish: {}
                }
            },
            travel: {
                subRoutes: {
                    spain: {},
                    articles: {
                        subRoutes: {
                            $articleId: {}
                        }
                    }
                }
            }
        }
    }
};

const routesApi = buildRoutes(routesDefinition);

The strings of the keys which you use are directly turned into the strings of your URL segments. URL params are prefixed with a dollar sign $, which will make the resulting API prompt for a string parameter when you construct an actual instance of the URL.

Reserved keywords

Under the hood, the library constructs a nested function object. That's why, at any point along your route, you can either call the function to return the string, or continue to build a URL that is nested more deeply:

// "topics/travel"
const routeToTravel = routesApi.topics.travel();

// "topics/travel/acticles/5
const routeToTravelArticle = routesApi.topics.travel.articles.$articleId("5")();

Unfortunately, with any segment of the API being a function, there are a few reserved keywords which can not be used, because they are either readonly (such as a function's name) or should not be overwritten (like bind). When you try to create the API object with input that contains these keys, an error will be thrown.

Reserved keywords are

['name', 'arguments', 'length', 'caller', 'prototype', 'bind', 'call', 'apply', 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'length', 'toString', 'propertyIsEnumerable', 'toLocaleString', 'valueOf']

Since it is quite likely that strings like name will be part of a URL, you can overwrite the string associated with a given URL segment with the segmentName property:

import {buildRoutes} from './that-routing-lib';

const routesDefiniton = {
    user: {
        subRoutes: {
            $userId: {
                subRoutes: {
                    uname: {
                        segmentName: "name" // overwrite
                    }
                }
            }
        }
    }
};

const routesApi = buildRoutes(routesDefiniton);

// "user/78357/name"
const nameUrl = routesApi.user.$userId("78357").uname();

this can also be useful to prepend slashes, entire domain names, or insert chars that would require string notation for keys when used directly:

import {buildRoutes} from './that-routing-lib';

const routesDefiniton = {
    sameOrigin: {
        segmentName: "", // leads to a leading '/'
        subRoutes: {
            home: {},
            articles: {}
        }
    },
    analytics: {
        segmentName: "https://my-analytics.com",
        subRoutes: {
            pageEnter: {
                // avoid routesApi.analytics['page-enter'].$pageId("3")()
                segmentName: "page-enter",
                subRoutes: {
                    $pageId: {}
                }
            },
            pageNavigation: {
                segmentName: "page-navigation",
                subRoutes: {
                    $fromPageId: {
                        subRoutes: {
                            $toPageId: {}
                        }
                    }
                }
            }
        }
    }
};

const routesApi = buildRoutes(routesDefiniton);

// "https://my-analytics.com/page-navigation/etkceaua/aetaeo"
const navigationAnalyticsUrl = routesApi.analytics.pageNavigation.$fromPageId("etkceaua").$toPageId("aetaeo")();

Note: You can not use segmentName to overwrite $parameters, because no reserved keyword starts with a $, and there is no other reason why you'd want to do this for parameters.

Creating URL strings for the Angular router

When you use routing in Angular, you have to define which component is to be shown under which route. Of course, you'll want to have a single source of truth, so your routesInput must be able to produce those route definitions as well.

Two requirements have to be fulfilled for this to work:

  • Route params have to be printed with the colon syntax acticles/:articleId
  • Routes can be nested. Instead of parentRoute/childRoute you'll have to be able to produce parentRoute and childRoute separately

The router API works exactly the same as the client API, except that now you don't have to pass a string to parameter segments. To signal that a given route is a parent route, you add the key isParent: true to the object:

import {buildRoutesForAngularRouter} from './that-routing-lib';

const routesDefinition = {
    topics: {
        subRoutes: {
            travel: {
                isParent: true,
                subRoutes: {
                    articles: {},
                    spain: {}
                }
            },
            otherParent: {
                isParent: true,
                subRoutes: {
                    $parameter: {}
                }
            }
        }
    }
};

const routesApi = buildRoutesForAngularRouter(routesDefinition);

// "topics/travel
const urlForParentForRouter = routesApi.topics.travel();
// "articles"
const urlForChildForRouter = routesApi.topics.travel.articles();
// ":parameter"
const urlForParameterChildForRouter = routesApi.topics.otherParent.$parameter();

Extract parameters to get data from router

We need the name of the parameter to obtain data from the activated route that has been inserted in the URL

const articleId = this.activatedRoute.snapshot.params['articleId'];

This is another case which is prone to errors. How do we know which parameter names exist? We'd have to check in the route definitions, and we'd have no way to safely refactor this code.

But we can do this:

import {getParameterExtractor} from './that-routing-lib';

// In central location:
export const parameters = getParameterExtractor().extract(routesDefinition);

// In component
const articleId = this.activatedRoute.snapshot.params[parameters.$articleId];

This isn't perfect. We have no way of knowing whether $articleId actually is part of the route behind which our component sits (this is always the case when using the router). In fact, we don't know at all which routes contain this parameter. But we do know that some route contains this parameter, which gives some confidence that what we do is right. And there is no way we produce a typo this way.

Gotchas

  • Due to Typescript technicalities, the maximum depth of your URLs is 30ish segments. That's because a recursive type has to be used, and the stack-size that TS allows is very limited. It's possible that this limit will be increased eventually.
  • On TS versions below 4.5, recursive types are not yet optimized if they are tail recursive. Since this library depends on a recursive type, the provided default of 95 for the maximum recursion depth (leading to those 30ish segments) is too large, leading to TS2589: Type instantiation is excessively deep and possibly infinite errors. If you use the getParameterExtractor().extract(routesDefinition) function on these versions, you need to limit the recursion depth to 16 to avoid the error: getParameterExtractor<16>().extract(routesDefinition). That also means that the depth of the URLs for which TS will provide support will shrink considerably.

Open issues

  • No support for query params

Readme

Keywords

Package Sidebar

Install

npm i that-routing-lib

Weekly Downloads

1

Version

1.3.1

License

MIT

Unpacked Size

79 kB

Total Files

33

Last publish

Collaborators

  • nobullsh1t