Skip to content

Commit cc6439c

Browse files
committed
Implement route generator service for type safe and url save router navigations
1 parent c81ed92 commit cc6439c

File tree

7 files changed

+229
-44
lines changed

7 files changed

+229
-44
lines changed

src/app/app.routes.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { PLATFORM } from 'aurelia-framework';
2+
3+
export type RouteNames = 'welcome' | 'users' | 'child-router';
4+
5+
export const welcome = {
6+
route: ['', 'welcome'],
7+
name: 'welcome',
8+
moduleId: PLATFORM.moduleName('modules/welcome/welcome.vm', 'welcome'),
9+
nav: true,
10+
title: 'Welcome'
11+
};
12+
13+
export const users = {
14+
route: 'users',
15+
name: 'users',
16+
moduleId: PLATFORM.moduleName('modules/users/users.vm', 'users'),
17+
nav: true,
18+
title: 'Github Users'
19+
};
20+
21+
export const childRouter = {
22+
route: 'child-router',
23+
name: 'child-router',
24+
moduleId: PLATFORM.moduleName('modules/child-router/child-router.vm', 'child-router'),
25+
nav: true,
26+
title: 'Child Router'
27+
};

src/app/app.vm.ts

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Lazy, inject, PLATFORM } from 'aurelia-framework';
1+
import { Lazy, inject } from 'aurelia-framework';
22
import { Router, RouterConfiguration } from 'aurelia-router';
33
import { I18N } from 'aurelia-i18n';
44
import { HttpClient } from 'aurelia-fetch-client';
@@ -10,8 +10,9 @@ import { CordovaService } from './services/cordova.service';
1010
import { EventBusService, EventBusEvents } from './services/event-bus.service';
1111
import { LanguageService } from './services/language.service';
1212
import { ExampleStep } from './piplines/example.step';
13+
import { RouteGeneratorService } from './services/route-generator.service';
1314

14-
@inject(I18N, AppConfigService, Lazy.of(CordovaService), EventBusService, LanguageService, HttpClient)
15+
@inject(I18N, AppConfigService, Lazy.of(CordovaService), EventBusService, LanguageService, HttpClient, RouteGeneratorService)
1516
export class AppViewModel {
1617

1718
private logger: Logger;
@@ -24,7 +25,8 @@ export class AppViewModel {
2425
private cordovaServiceFn: () => CordovaService,
2526
private eventBusService: EventBusService,
2627
private languageService: LanguageService,
27-
private httpClient: HttpClient
28+
private httpClient: HttpClient,
29+
private routeGeneratorService: RouteGeneratorService,
2830
) {
2931
this.logger = LogManager.getLogger('AppViewModel');
3032
this.configureHttpClient();
@@ -42,29 +44,7 @@ export class AppViewModel {
4244
if (this.appConfigService.platformIsMobile()) {
4345
this.cordovaServiceFn();
4446
}
45-
config.map([
46-
{
47-
route: ['', 'welcome'],
48-
name: 'welcome',
49-
moduleId: PLATFORM.moduleName('./modules/welcome/welcome.vm', 'welcome'),
50-
nav: true,
51-
title: 'Welcome'
52-
},
53-
{
54-
route: 'users',
55-
name: 'users',
56-
moduleId: PLATFORM.moduleName('./modules/users/users.vm', 'users'),
57-
nav: true,
58-
title: 'Github Users'
59-
},
60-
{
61-
route: 'child-router',
62-
name: 'child-router',
63-
moduleId: PLATFORM.moduleName('./modules/child-router/child-router.vm', 'child-router'),
64-
nav: true,
65-
title: 'Child Router'
66-
}
67-
]);
47+
config.map(this.routeGeneratorService.getRootRoutesConfig());
6848
config.mapUnknownRoutes({ route: '', redirect: '' });
6949

7050
config.addAuthorizeStep(ExampleStep);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { PLATFORM } from 'aurelia-framework';
2+
3+
export type RouteNames = 'welcome' | 'users' | 'child-router';
4+
5+
export const welcome = {
6+
route: ['', 'welcome'],
7+
name: 'welcome',
8+
moduleId: PLATFORM.moduleName('modules/welcome/welcome.vm', 'welcome'),
9+
nav: true,
10+
title: 'Welcome'
11+
};
12+
13+
export const users = {
14+
route: 'users',
15+
name: 'users',
16+
moduleId: PLATFORM.moduleName('modules/users/users.vm', 'users'),
17+
nav: true,
18+
title: 'Github Users'
19+
};
20+
21+
export const childRouter = {
22+
route: 'child-router',
23+
name: 'child-router',
24+
moduleId: PLATFORM.moduleName('modules/child-router/child-router.vm', 'child-router'),
25+
nav: true,
26+
title: 'Child Router'
27+
};

src/app/modules/child-router/child-router.vm.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import { PLATFORM } from 'aurelia-framework';
1+
import { autoinject } from 'aurelia-framework';
22
import { Router, RouterConfiguration } from 'aurelia-router';
33

4+
import { RouteGeneratorService } from './../../services/route-generator.service';
5+
6+
@autoinject
47
export class ChildRouterViewModel {
58

69
public router!: Router;
7-
public heading = 'Child Router';
10+
public heading = 'Child Router';
11+
12+
constructor(
13+
private routeGeneratorService: RouteGeneratorService,
14+
) { }
815

916
public configureRouter(config: RouterConfiguration, router: Router): void {
10-
config.map([
11-
{ route: ['', 'welcome'], name: 'welcome', moduleId: PLATFORM.moduleName('./../welcome/welcome.vm'), nav: true, title: 'Welcome' },
12-
{ route: 'users', name: 'users', moduleId: PLATFORM.moduleName('./../users/users.vm'), nav: true, title: 'Github Users' },
13-
{ route: 'child-router', name: 'child-router',
14-
moduleId: PLATFORM.moduleName('./../child-router/child-router.vm'), nav: true, title: 'Child Router' }
15-
]);
17+
config.map(this.routeGeneratorService.getRoutesConfigByParentRouteName('child-router'));
1618

1719
this.router = router;
1820
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { autoinject } from 'aurelia-framework';
2+
import { Router, RouteConfig } from 'aurelia-router';
3+
import { RouteRecognizer, RouteHandler } from 'aurelia-route-recognizer';
4+
import * as _ from 'lodash';
5+
6+
import * as appRoutes from './../app.routes';
7+
import * as childRouterRoutes from './../modules/child-router/child-router.routes';
8+
9+
export type TAllowedRouteNames = appRoutes.RouteNames | childRouterRoutes.RouteNames;
10+
11+
export type TRouteConfigItem = { routeName: TAllowedRouteNames, params?: { [key: string]: any } };
12+
export type TRouteConfig = [TRouteConfigItem];
13+
export type ITreeConfig = RouteConfig & { subroutes?: ITreeConfig[] };
14+
15+
@autoinject()
16+
export class RouteGeneratorService {
17+
18+
private routeTree: ITreeConfig[] = [
19+
appRoutes.welcome,
20+
appRoutes.users,
21+
{
22+
...appRoutes.childRouter,
23+
subroutes: [
24+
childRouterRoutes.welcome,
25+
childRouterRoutes.users,
26+
childRouterRoutes.childRouter,
27+
]
28+
}
29+
];
30+
31+
constructor(
32+
private router: Router,
33+
) { }
34+
35+
public getUrlByRouteNames(routes: TRouteConfig): string {
36+
return this.buildUrl(_.cloneDeep(routes), this.routeTree);
37+
}
38+
39+
public navigateByUrl(url: string, options?: any): void {
40+
this.router.navigate(url, options);
41+
}
42+
43+
public navigateByRouteNames(routes: TRouteConfig, options?: any): void {
44+
const url = this.buildUrl(_.cloneDeep(routes), this.routeTree);
45+
this.navigateByUrl(url, options);
46+
}
47+
48+
public getRoutesConfigByParentRouteName(routeName: TAllowedRouteNames): RouteConfig[] {
49+
const parentRoute = this.findNameInRouteTree(routeName, this.routeTree);
50+
51+
if (!parentRoute) {
52+
throw new Error(
53+
`You route name ${routeName} couldn't be found!`
54+
);
55+
}
56+
57+
if (parentRoute && !parentRoute.subroutes) {
58+
throw new Error(
59+
`You route name ${routeName} has no subroutes!`
60+
);
61+
}
62+
63+
return this.removeSubroutes(parentRoute.subroutes as RouteConfig[]);
64+
}
65+
66+
public getRootRoutesConfig(): RouteConfig[] {
67+
return this.removeSubroutes(this.routeTree);
68+
}
69+
70+
public getRouteConfigByRouteName(routeName: TAllowedRouteNames): RouteConfig | undefined {
71+
return this.findNameInRouteTree(routeName, this.routeTree);
72+
}
73+
74+
private removeSubroutes(routes: RouteConfig[]): RouteConfig[] {
75+
return routes.map(route => {
76+
const newRoute = _.cloneDeep(route);
77+
delete newRoute.subroutes;
78+
return newRoute;
79+
});
80+
}
81+
82+
private findNameInRouteTree(routeName: TAllowedRouteNames, treeConfig: ITreeConfig |  ITreeConfig[]): ITreeConfig | undefined {
83+
let result;
84+
if (treeConfig instanceof Array) {
85+
for (let conf of treeConfig) {
86+
result = this.findNameInRouteTree(routeName, conf);
87+
if (result) {
88+
break;
89+
}
90+
}
91+
} else {
92+
if (treeConfig.name === routeName) {
93+
return treeConfig;
94+
}
95+
if (treeConfig.subroutes) {
96+
result = this.findNameInRouteTree(routeName, treeConfig.subroutes);
97+
}
98+
}
99+
return result;
100+
}
101+
102+
private buildUrl(routes: TRouteConfig, treeConfig: ITreeConfig[]): string {
103+
104+
if (routes.length as any === 0) {
105+
return '';
106+
}
107+
108+
const currentRouteConfig = routes.shift() as TRouteConfigItem;
109+
const currentTreeConfig = treeConfig.find(config => config.name === currentRouteConfig.routeName);
110+
111+
if (!currentTreeConfig) {
112+
throw new Error(`The route config with name ${currentRouteConfig.routeName} doesn't exist!`);
113+
}
114+
115+
const url = this.getUrlByTreeConfig(currentTreeConfig, currentRouteConfig);
116+
117+
if (url.includes('?') && routes.length >= 1) {
118+
throw new Error(
119+
`You provided a parameter not used in ${currentRouteConfig.routeName}. Add query parameters to the last route configuration!`
120+
);
121+
}
122+
123+
return url + this.buildUrl(routes, currentTreeConfig.subroutes as ITreeConfig[]);
124+
}
125+
126+
private getUrlByTreeConfig(treeConfig: ITreeConfig, routeConfig: TRouteConfigItem): string {
127+
128+
const routeRecognizer = new RouteRecognizer();
129+
if (Array.isArray(treeConfig.route)) {
130+
treeConfig.route.forEach(value => routeRecognizer.add([{
131+
path: value,
132+
handler: treeConfig as RouteHandler,
133+
caseSensitive: true
134+
}]));
135+
} else {
136+
routeRecognizer.add([{
137+
path: treeConfig.route,
138+
handler: treeConfig as RouteHandler,
139+
caseSensitive: true
140+
}]);
141+
}
142+
143+
return routeRecognizer.generate(routeConfig.routeName, routeConfig.params as any);
144+
}
145+
}

test/unit/app.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { EventAggregator } from 'aurelia-event-aggregator';
55
import { HttpClient } from 'aurelia-fetch-client';
66

77
import { AppViewModel } from '../../src/app/app.vm';
8+
import { RouteGeneratorService } from '../../src/app/services/route-generator.service';
89
let en_USTranslation = require('./../../src/locales/en_US.json');
910
let de_CHTranslation = require('./../../src/locales/de_CH.json');
1011

@@ -37,7 +38,6 @@ class AppConfigStub {
3738
describe('the App module', () => {
3839
let sut;
3940
let mockedRouter;
40-
let appConfigSub;
4141
let i18nMock;
4242

4343
beforeEach(() => {
@@ -60,9 +60,10 @@ describe('the App module', () => {
6060
debug: false
6161
});
6262

63-
appConfigSub = new AppConfigStub();
63+
const appConfigSub = new AppConfigStub();
64+
const routeGeneratorService = new RouteGeneratorService(undefined as any);
6465

65-
sut = new AppViewModel(i18nMock, appConfigSub, AnyMock, AnyMock, AnyMock, new HttpClient());
66+
sut = new AppViewModel(i18nMock, appConfigSub as any, AnyMock, AnyMock, AnyMock, new HttpClient(), routeGeneratorService);
6667
sut.configureRouter(mockedRouter, mockedRouter);
6768
});
6869

@@ -78,7 +79,7 @@ describe('the App module', () => {
7879
expect(sut.router.routes).toContainEqual({
7980
route: ['', 'welcome'],
8081
name: 'welcome',
81-
moduleId: PLATFORM.moduleName('./modules/welcome/welcome.vm'),
82+
moduleId: PLATFORM.moduleName('modules/welcome/welcome.vm'),
8283
nav: true,
8384
title: 'Welcome'
8485
});
@@ -88,7 +89,7 @@ describe('the App module', () => {
8889
expect(sut.router.routes).toContainEqual({
8990
route: 'users',
9091
name: 'users',
91-
moduleId: PLATFORM.moduleName('./modules/users/users.vm'),
92+
moduleId: PLATFORM.moduleName('modules/users/users.vm'),
9293
nav: true,
9394
title: 'Github Users'
9495
});
@@ -98,7 +99,7 @@ describe('the App module', () => {
9899
expect(sut.router.routes).toContainEqual({
99100
route: 'child-router',
100101
name: 'child-router',
101-
moduleId: PLATFORM.moduleName('./modules/child-router/child-router.vm'),
102+
moduleId: PLATFORM.moduleName('modules/child-router/child-router.vm'),
102103
nav: true,
103104
title: 'Child Router'
104105
});

test/unit/child-router.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PLATFORM } from 'aurelia-framework';
22

33
import { ChildRouterViewModel } from '../../src/app/modules/child-router/child-router.vm';
4+
import { RouteGeneratorService } from '../../src/app/services/route-generator.service';
45

56
class RouterStub {
67
public routes;
@@ -19,8 +20,10 @@ describe('the Child Router module', () => {
1920
let mockedRouter;
2021

2122
beforeEach(() => {
23+
const routeGeneratorService = new RouteGeneratorService(undefined as any);
24+
2225
mockedRouter = new RouterStub();
23-
sut = new ChildRouterViewModel();
26+
sut = new ChildRouterViewModel(routeGeneratorService);
2427
sut.configureRouter(mockedRouter, mockedRouter);
2528
});
2629

@@ -36,7 +39,7 @@ describe('the Child Router module', () => {
3639
expect(sut.router.routes).toContainEqual({
3740
route: ['', 'welcome'],
3841
name: 'welcome',
39-
moduleId: PLATFORM.moduleName('./../welcome/welcome.vm'),
42+
moduleId: PLATFORM.moduleName('modules/welcome/welcome.vm'),
4043
nav: true,
4144
title: 'Welcome'
4245
});
@@ -46,7 +49,7 @@ describe('the Child Router module', () => {
4649
expect(sut.router.routes).toContainEqual({
4750
route: 'users',
4851
name: 'users',
49-
moduleId: PLATFORM.moduleName('./../users/users.vm'),
52+
moduleId: PLATFORM.moduleName('modules/users/users.vm'),
5053
nav: true,
5154
title: 'Github Users'
5255
});
@@ -56,7 +59,7 @@ describe('the Child Router module', () => {
5659
expect(sut.router.routes).toContainEqual({
5760
route: 'child-router',
5861
name: 'child-router',
59-
moduleId: PLATFORM.moduleName('./../child-router/child-router.vm'),
62+
moduleId: PLATFORM.moduleName('modules/child-router/child-router.vm'),
6063
nav: true,
6164
title: 'Child Router'
6265
});

0 commit comments

Comments
 (0)