Skip to content

Commit f117c57

Browse files
committed
feat: support absolute paths for nested routes
1 parent 2fd4747 commit f117c57

2 files changed

Lines changed: 122 additions & 14 deletions

File tree

modules/RouteNode.js

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,62 @@ import { getSearch, getPath, omit, withoutBrackets, parse } from 'search-params'
44
const noop = () => {};
55

66
export default class RouteNode {
7-
constructor(name = '', path = '', childRoutes = [], cb) {
7+
constructor(name = '', path = '', childRoutes = [], cb, parent) {
88
this.name = name;
9-
this.path = path;
10-
this.parser = path ? new Path(path) : null;
9+
this.absolute = /^~/.test(path);
10+
this.path = this.absolute ? path.slice(1) : path;
11+
this.parser = this.path ? new Path(this.path) : null;
1112
this.children = [];
13+
this.parent = parent;
14+
15+
this.checkParents();
1216

1317
this.add(childRoutes, cb);
1418

1519
return this;
1620
}
1721

22+
checkParents() {
23+
if (this.absolute && this.haveParentsParams()) {
24+
throw new Error('[RouteNode] A RouteNode with an abolute path cannot have parents with route parameters');
25+
}
26+
}
27+
28+
haveParentsParams() {
29+
if (this.parent && this.parent.parser) {
30+
const parser = this.parent.parser;
31+
const hasParams = parser.hasUrlParams || parser.hasSpatParam || parser.hasMatrixParams || parser.hasQueryParams;
32+
33+
return hasParams || this.parent.haveParentsParams();
34+
}
35+
36+
return false;
37+
}
38+
39+
getNonAbsoluteChildren() {
40+
return this.children.filter((child) => !child.absolute);
41+
}
42+
43+
findAbsoluteChildren() {
44+
return this.children.reduce((absoluteChildren, child) =>
45+
absoluteChildren
46+
.concat(child.absolute ? child : [])
47+
.concat(child.findAbsoluteChildren()),
48+
[]
49+
);
50+
}
51+
52+
getParentSegments(segments = []) {
53+
return this.parent && this.parent.parser
54+
? this.parent.getParentSegments(segments.concat(this.parent))
55+
: segments.reverse();
56+
}
57+
58+
setParent(parent) {
59+
this.parent = parent;
60+
this.checkParents();
61+
}
62+
1863
setPath(path = '') {
1964
this.path = path;
2065
this.parser = path ? new Path(path) : null;
@@ -31,13 +76,14 @@ export default class RouteNode {
3176

3277
if (!(route instanceof RouteNode) && !(route instanceof Object)) {
3378
throw new Error('RouteNode.add() expects routes to be an Object or an instance of RouteNode.');
34-
}
35-
if (route instanceof Object) {
79+
} else if (route instanceof RouteNode) {
80+
route.setParent(this);
81+
} else {
3682
if (!route.name || !route.path) {
3783
throw new Error('RouteNode.add() expects routes to have a name and a path defined.');
3884
}
3985
originalRoute = route;
40-
route = new RouteNode(route.name, route.path, route.children, cb);
86+
route = new RouteNode(route.name, route.path, route.children, cb, this);
4187
}
4288

4389
let names = route.name.split('.');
@@ -167,19 +213,26 @@ export default class RouteNode {
167213
remainingQueryParams.forEach(({ name, value} ) => segments.params[name] = value);
168214
return segments;
169215
}
216+
// Continue matching on non absolute children
217+
const children = child.getNonAbsoluteChildren();
170218
// If no children to match against but unmatched path left
171-
if (!child.children.length) {
219+
if (!children.length) {
172220
return null;
173221
}
174222
// Else: remaining path and children
175-
return matchChildren(child.children, remainingPath, segments);
223+
return matchChildren(children, remainingPath, segments);
176224
}
177225
}
178226

179227
return null;
180228
};
181229

182-
let startingNodes = this.parser ? [this] : this.children;
230+
const topLevelNodes = this.parser ? [ this ] : this.children;
231+
const startingNodes = topLevelNodes.reduce(
232+
(nodes, node) => nodes.concat(node, node.findAbsoluteChildren()),
233+
[]
234+
);
235+
183236
let segments = [];
184237
segments.params = {};
185238

@@ -228,9 +281,14 @@ export default class RouteNode {
228281
})
229282
.join('&');
230283

231-
return segments
232-
.map(segment => segment.parser.build(params, {ignoreSearch: true}))
233-
.join('') + (searchPart ? '?' + searchPart : '');
284+
const path = segments
285+
.reduce((path, segment) => {
286+
const segmentPath = segment.parser.build(params, {ignoreSearch: true});
287+
288+
return segment.absolute ? segmentPath : path + segmentPath;
289+
}, '');
290+
291+
return path + (searchPart ? '?' + searchPart : '');
234292
}
235293

236294
getMetaFromSegments(segments) {
@@ -288,7 +346,17 @@ export default class RouteNode {
288346

289347
matchPath(path, options) {
290348
const defaultOptions = { trailingSlash: false, strictQueryParams: true };
291-
options = { ...defaultOptions, ...options };
292-
return this.buildStateFromSegments(this.getSegmentsMatchingPath(path, options));
349+
const opts = { ...defaultOptions, ...options };
350+
let matchedSegments = this.getSegmentsMatchingPath(path, opts);
351+
352+
if (matchedSegments && matchedSegments[0].absolute) {
353+
const firstSegmentParams = matchedSegments[0].getParentSegments();
354+
355+
matchedSegments.reverse();
356+
matchedSegments.push(...firstSegmentParams);
357+
matchedSegments.reverse();
358+
}
359+
360+
return this.buildStateFromSegments(matchedSegments);
293361
}
294362
}

test/main.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,46 @@ describe('RouteNode', function () {
408408
name: 'route',
409409
params: { a: '1', b: '2', c: '3', d: true }
410410
});
411+
412+
});
413+
414+
it('should throw an error when adding an absolute path below nodes with params', () => {
415+
function createNode() {
416+
return new RouteNode('', '', [
417+
new RouteNode('path', '/path/:path', [
418+
new RouteNode('absolute', '~/absolute')
419+
])
420+
]);
421+
}
422+
423+
createNode.should.throw();
424+
});
425+
426+
it('should build absolute paths', function () {
427+
var node = new RouteNode('', '', [
428+
new RouteNode('path', '/path', [
429+
new RouteNode('relative', '/relative'),
430+
new RouteNode('absolute', '~/absolute')
431+
])
432+
]);
433+
434+
node.buildPath('path.relative').should.equal('/path/relative');
435+
node.buildPath('path.absolute').should.equal('/absolute');
436+
});
437+
438+
it('should match absolute paths', function () {
439+
const absolute = new RouteNode('absolute', '~/absolute');
440+
441+
var node = new RouteNode('', '', [
442+
new RouteNode('path', '/path', [
443+
new RouteNode('relative', '/relative'),
444+
absolute
445+
])
446+
]);
447+
448+
withoutMeta(node.matchPath('/path/relative')).should.eql({ name: 'path.relative', params: {}});
449+
should.not.exist(node.matchPath('/path/absolute'));
450+
withoutMeta(node.matchPath('/absolute')).should.eql({ name: 'path.absolute', params: {}});
411451
});
412452
});
413453

0 commit comments

Comments
 (0)