diff --git a/README.md b/README.md
index 370cd02b..cc9dad6a 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,21 @@ You can also make params optional by adding a `?` to it.
```
+### Nesting routers
+
+Routers will append the parent Routers' URLs together to come up with the matching route for children.
+
+```js
+
+ //will route '/'
+ //will route '/app/*' (could also use default here)
+ //will route '/app/b'
+ //will route '/app/c'
+
+ //will route '/d'
+ //will route anything not listed above
+
+```
### Lazy Loading
diff --git a/src/index.js b/src/index.js
index a34bd2d3..fb983482 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,5 +1,5 @@
import { cloneElement, createElement, Component, toChildArray } from 'preact';
-import { exec, prepareVNodeForRanking, assign, pathRankSort } from './util';
+import { exec, prepareVNodeForRanking, assign, pathRankSort, segmentize } from './util';
let customHistory = null;
@@ -142,12 +142,27 @@ function initEventListeners() {
class Router extends Component {
- constructor(props) {
+ constructor(props, context) {
super(props);
+
+ this.baseUrl = props.base || '';
+ if (props.path) {
+ let segments = segmentize(props.path);
+ segments.forEach(segment => {
+ if (segment.indexOf(':') == -1) {
+ this.baseUrl = this.baseUrl + '/' + segment;
+ }
+ });
+ }
+
if (props.history) {
customHistory = props.history;
}
+ if (context && context['preact-router-base'] && !props.base) {
+ this.baseUrl = context['preact-router-base'] + this.baseUrl;
+ }
+
this.state = {
url: props.url || getCurrentUrl()
};
@@ -155,6 +170,10 @@ class Router extends Component {
initEventListeners();
}
+ getChildContext() {
+ return {['preact-router-base']: this.baseUrl};
+ }
+
shouldComponentUpdate(props) {
if (props.static!==true) return true;
return props.url!==this.props.url || props.onChange!==this.props.onChange;
@@ -210,7 +229,7 @@ class Router extends Component {
.filter(prepareVNodeForRanking)
.sort(pathRankSort)
.map( vnode => {
- let matches = exec(url, vnode.props.path, vnode.props);
+ let matches = exec(url, this.baseUrl + vnode.props.path, vnode.props);
if (matches) {
if (invoke !== false) {
let newProps = { url, matches };
diff --git a/src/match.js b/src/match.js
index 8ab75f12..e8268a50 100644
--- a/src/match.js
+++ b/src/match.js
@@ -1,7 +1,26 @@
-import { h, Component } from 'preact';
+import { h, Component, cloneElement } from 'preact';
import { subscribers, getCurrentUrl, Link as StaticLink } from 'preact-router';
+import { exec, segmentize } from './util';
export class Match extends Component {
+ constructor(props, context) {
+ super(props);
+
+ this.baseUrl = props.base || '';
+ if (props.path) {
+ let segments = segmentize(props.path);
+ segments.forEach(segment => {
+ if (segment.indexOf(':') == -1) {
+ this.baseUrl = this.baseUrl + '/' + segment;
+ }
+ });
+ }
+
+ if (context && context['preact-router-base']) {
+ this.baseUrl = context['preact-router-base'] + this.baseUrl;
+ }
+ }
+
update = url => {
this.nextUrl = url;
this.setState({});
@@ -12,15 +31,21 @@ export class Match extends Component {
componentWillUnmount() {
subscribers.splice(subscribers.indexOf(this.update)>>>0, 1);
}
+ getChildContext() {
+ return {['preact-router-base']: this.baseUrl};
+ }
render(props) {
let url = this.nextUrl || getCurrentUrl(),
path = url.replace(/\?.+$/,'');
this.nextUrl = null;
- return props.children({
+
+ const newProps = {
url,
path,
- matches: path===props.path
- });
+ matches: path===props.path || exec(path, context['preact-router-base'] + props.path, {})
+ };
+
+ return typeof props.children === 'function' ? props.children(newProps) : cloneElement(props.children, newProps);
}
}
diff --git a/test/dom.js b/test/dom.js
index 67aef733..67409ec3 100644
--- a/test/dom.js
+++ b/test/dom.js
@@ -121,6 +121,193 @@ describe('dom', () => {
expect(A.prototype.componentWillUnmount).to.have.been.calledOnce;
});
+ it('should support nested routers with default', () => {
+ class X {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return
; }
+ }
+ sinon.spy(X.prototype, 'componentWillMount');
+ sinon.spy(X.prototype, 'componentWillUnmount');
+ class Y {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(Y.prototype, 'componentWillMount');
+ sinon.spy(Y.prototype, 'componentWillUnmount');
+ mount(
+
+
+
+
+
+
+ );
+ expect(X.prototype.componentWillMount).not.to.have.been.called;
+ expect(Y.prototype.componentWillMount).not.to.have.been.called;
+ route('/app/x');
+ expect(X.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount).not.to.have.been.called;
+ expect(Y.prototype.componentWillMount).not.to.have.been.called;
+ expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
+ route('/app/y');
+ expect(X.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount).to.have.been.calledOnce;
+ expect(Y.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
+ });
+
+ it('should support nested routers with path', () => {
+ class X {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(X.prototype, 'componentWillMount');
+ sinon.spy(X.prototype, 'componentWillUnmount');
+ class Y {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(Y.prototype, 'componentWillMount');
+ sinon.spy(Y.prototype, 'componentWillUnmount');
+ mount(
+
+
+
+
+
+
+ );
+ expect(X.prototype.componentWillMount).not.to.have.been.called;
+ expect(Y.prototype.componentWillMount).not.to.have.been.called;
+ route('/baz/j');
+ expect(X.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount).not.to.have.been.called;
+ expect(Y.prototype.componentWillMount).not.to.have.been.called;
+ expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
+ route('/baz/box/k');
+ expect(X.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount).to.have.been.calledOnce;
+ expect(Y.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
+ });
+
+ it('should support deeply nested routers', () => {
+ class X {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(X.prototype, 'componentWillMount');
+ sinon.spy(X.prototype, 'componentWillUnmount');
+ class Y {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(Y.prototype, 'componentWillMount');
+ sinon.spy(Y.prototype, 'componentWillUnmount');
+ mount(
+
+
+
+
+
+
+
+
+ );
+ expect(X.prototype.componentWillMount).not.to.have.been.called;
+ expect(Y.prototype.componentWillMount).not.to.have.been.called;
+ route('/baz/j');
+ expect(X.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount).not.to.have.been.called;
+ expect(Y.prototype.componentWillMount).not.to.have.been.called;
+ expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
+ route('/baz/box/k');
+ expect(X.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount).to.have.been.calledOnce;
+ expect(Y.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
+ });
+
+ it('should support nested routers and Match(s)', () => {
+ class X {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(X.prototype, 'componentWillMount');
+ sinon.spy(X.prototype, 'componentWillUnmount');
+ class Y {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(Y.prototype, 'componentWillMount');
+ sinon.spy(Y.prototype, 'componentWillUnmount');
+ mount(
+
+
+
+
+
+
+ );
+ expect(X.prototype.componentWillMount, 'X1').not.to.have.been.called;
+ expect(Y.prototype.componentWillMount, 'Y1').not.to.have.been.called;
+ route('/ccc/jjj');
+ expect(X.prototype.componentWillMount, 'X2').to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount, 'X3').not.to.have.been.called;
+ expect(Y.prototype.componentWillMount, 'Y2').not.to.have.been.called;
+ expect(Y.prototype.componentWillUnmount, 'Y3').not.to.have.been.called;
+ route('/ccc/xxx/kkk');
+ expect(X.prototype.componentWillMount, 'X4').to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount, 'X5').to.have.been.calledOnce;
+ expect(Y.prototype.componentWillMount, 'Y4').to.have.been.calledOnce;
+ expect(Y.prototype.componentWillUnmount, 'Y5').not.to.have.been.called;
+ });
+
+ it('should support nested router reset via base attr', () => {
+ class X {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(X.prototype, 'componentWillMount');
+ sinon.spy(X.prototype, 'componentWillUnmount');
+ class Y {
+ componentWillMount() {}
+ componentWillUnmount() {}
+ render(){ return ; }
+ }
+ sinon.spy(Y.prototype, 'componentWillMount');
+ sinon.spy(Y.prototype, 'componentWillUnmount');
+ mount(
+
+
+
+
+
+
+ );
+ expect(X.prototype.componentWillMount).not.to.have.been.called;
+ expect(Y.prototype.componentWillMount).not.to.have.been.called;
+ route('/baz/j');
+ expect(X.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount).not.to.have.been.called;
+ expect(Y.prototype.componentWillMount).not.to.have.been.called;
+ expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
+ route('/baz/foo/k');
+ expect(X.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(X.prototype.componentWillUnmount).to.have.been.calledOnce;
+ expect(Y.prototype.componentWillMount).to.have.been.calledOnce;
+ expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
+ });
+
it('should support re-routing', done => {
class A {
componentWillMount() {