@bespunky/angular-zen
Version:
The Angular tools you always wished were there.
184 lines (174 loc) • 7.74 kB
JavaScript
import { inject, InjectionToken } from '@angular/core';
import { Router, provideRouter, provideRoutes } from '@angular/router';
const _RouteComposer_ = Symbol('RouteComposer');
const _NavigatorXToken_ = Symbol('NavigatorXToken');
/**
* Injects the auto-generated strongly-typed navigation service for the specified route tree.
*
* For this to work, the route must be previously generated using {@link routeConfigFor `routeConfigFor`}, and provided into Angular using
* either {@link provideRouterX `provideRouterX`} or {@link provideRoutesX `provideRoutesX`}.
*
* To change the default composed name of a specific auto-generated navigation method, go to the corresponding
* route config and add the `friendlyName` propertyName.
*
* @export
* @template Route
* @template Entity
* @template Root
* @template FullPath
* @template ComposerName
* @param {(Route & WithRouteComposer<Entity, FullPath, ComposerName> & WithNavigationX<Route, Entity, Root>)} route
* @return {AutoNavigateRouteMethods<Route, Entity, Root>}
*/
function useNavigationX(route) {
return inject(route[_NavigatorXToken_]);
}
function touchFirstLetter([firstLetter, ...rest], touch) {
return firstLetter ? touch(firstLetter) + rest.join('') : '';
}
function firstUpper(value) {
return touchFirstLetter(value, first => first.toUpperCase());
}
// TODO: Warn if multiple composers with the same name were found
function collectRouteComposersByAutoNavigatorName(route) {
const composer = route[_RouteComposer_];
const autoNavigatorName = `to${firstUpper(composer.name)}`;
const childComposers = collectArrayRouteComposersByAutoNavigatioName(route.children);
return new Map([
[autoNavigatorName, composer],
...(childComposers !== null && childComposers !== void 0 ? childComposers : [])
]);
}
function collectArrayRouteComposersByAutoNavigatioName(routes) {
return routes === null || routes === void 0 ? void 0 : routes.map(collectRouteComposersByAutoNavigatorName).reduce((allNestedComposers, childComposers) => new Map([...allNestedComposers, ...childComposers]), new Map());
}
function extractArgsFromPath(path) {
return path
.split('/')
.filter(segment => segment.startsWith(':'));
}
function attemptToProduceAutoNavigationFunctionFor(router, composer) {
if (!composer)
return undefined;
if (composer.hasArgs) {
const compose = composer.compose.bind(composer);
return (entity) => router.navigateByUrl(compose(entity));
}
return () => router.navigateByUrl(composer.compose());
}
function createNavigationXFactoryProvider(route) {
return {
provide: route[_NavigatorXToken_],
deps: [Router],
useFactory: (router) => {
const composers = collectRouteComposersByAutoNavigatorName(route);
return new Proxy({}, {
get: (_, propertyName) => attemptToProduceAutoNavigationFunctionFor(router, composers.get(propertyName)),
has: (_, propertyName) => composers.has(propertyName)
});
}
};
}
function provideNavigatorsFor(...routes) {
if (!(routes === null || routes === void 0 ? void 0 : routes.length))
throw `No routes were provided.`;
return routes.map(createNavigationXFactoryProvider);
}
/**
* Wraps Angular's `provideRouter` function and adds providers for the navigation-x module.
*
* @export
* @param {NavigationXRoute<any>[]} routes The Angular routes config tree to initialize navigation-x for.
* @param {...NoHead<Parameters<typeof provideRouter>>} features Addtional features to pass into Angular's `provideRouter` function.
* @return {Provider[]} The providers returned by `provideRouter`, along with other providers needed for navigation-x to work.
*/
function provideRouterX(routes, ...features) {
return [
...provideRouter(routes, ...features),
...provideNavigatorsFor(...routes)
];
}
/**
* Wraps Angular's `provideRoutes` function and adds providers for the navigation-x module.
*
* @export
* @param {...NavigationXRoute<any>[]} routes The Angular routes config tree to initialize navigation-x for.
* @return {Provider[]} The providers returned by `provideRoutes`, along with other providers needed for navigation-x to work.
*/
function provideRoutesX(...routes) {
return [
...provideRoutes(routes),
...provideNavigatorsFor(routes)
];
}
class RouteComposer {
constructor(path, name) {
this.path = path;
this.name = name;
this.args = extractArgsFromPath(path);
this.hasArgs = this.args.length > 0;
this.compose = (this.hasArgs ? this.routePathWithArgs.bind(this) : this.routePath.bind(this));
}
routePathWithArgs(entity) {
return this.args.reduce((route, arg) => {
const argName = arg.substring(1);
const value = entity[argName];
const formattedValue = value instanceof Date ? value.toUTCString() : String(value);
return route.replace(arg, formattedValue);
}, this.path);
}
routePath() { return this.path; }
}
const autoNavigatorNameSeparator = '';
function generateRouteComposerName(path) {
return path
.replace(/\/:/g, '/')
.split('/')
.map(firstUpper)
.join(autoNavigatorNameSeparator);
}
/**
* Creates a strongly-typed configurator for Angular routes.
*
* Call this with an entity, then generate a strongly typed Angular route config tree using one of the
* configurator's functions.
*
* Store the generated route somewhere and use `provideRouterX()` or `provideRoutesX()` to tell Angular about it.
*
* @export
* @template Entity The entity (or data structure) route arguments should match with.
* @return {RouteConfigurator<Entity>} An object which allows generating strongly typed routes.
*/
function routeConfigFor() {
function combinePath(root, segment) {
return (segment ? `${root}/${segment}` : root);
}
function prefixedRouteCore(route, root) {
var _a, _b;
const path = combinePath(root, route.path);
const composerName = ((_a = route.friendlyName) !== null && _a !== void 0 ? _a : generateRouteComposerName(path));
const composer = new RouteComposer(path, composerName);
const children = (_b = route.children) === null || _b === void 0 ? void 0 : _b.map(child => prefixedRoute(child, path));
return Object.assign(Object.assign({}, route), { children, [_RouteComposer_]: composer });
}
function prefixedRoute(route, root) {
return Object.assign(Object.assign({}, prefixedRouteCore(route, root)), { [_NavigatorXToken_]: new InjectionToken(`_ROUTER_X_NAVIGATION__${route.path}`) });
}
function route(route) {
// When the `root` arg was optional with a default value of `''`, TS appended `${string}` (e.g. `${string}/theaters/:theaterId`)
// to the route template for some reason, which breaks the arg extraction later on.
// Passing `''` manually however, leaves the template as is should be (e.g. `/theaters/:theaterId`) and allows the string template to be interpreted well.
// `route()` and `prefixedRoute()` were seperated to allow the developer to call `route()` without providing `''`.
// The `prefixedRoute()` is exposed to the world as it might be usefull in the future (e.g. lazy loaded routes? maybe...?)
return prefixedRoute(route, '');
}
return {
route,
prefixedRoute
};
}
/**
* Generated bundle index. Do not edit.
*/
export { autoNavigatorNameSeparator, provideRouterX, provideRoutesX, routeConfigFor, useNavigationX };
//# sourceMappingURL=bespunky-angular-zen-router-x-navigation.mjs.map