Skip to content

Commit 8fff0f4

Browse files
committed
Rebase preactjs#100 with latest changes
1 parent f7dbe6d commit 8fff0f4

File tree

4 files changed

+253
-7
lines changed

4 files changed

+253
-7
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ You can also make params optional by adding a `?` to it.
5858
</Router>
5959
```
6060

61+
### Nesting routers
62+
63+
Routers will append the parent Routers' URLs together to come up with the matching route for children.
64+
65+
```js
66+
<Router>
67+
<A path="/" /> //will route '/'
68+
<Router path="/app/:bar*"> //will route '/app/*' (could also use default here)
69+
<B path="/b"/> //will route '/app/b'
70+
<C path="/c" /> //will route '/app/c'
71+
</Router>
72+
<D path="/d" /> //will route '/d'
73+
<E default /> //will route anything not listed above
74+
</Router>
75+
```
6176

6277
### Lazy Loading
6378

src/index.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cloneElement, createElement, Component, toChildArray } from 'preact';
2-
import { exec, prepareVNodeForRanking, assign, pathRankSort } from './util';
2+
import { exec, prepareVNodeForRanking, assign, pathRankSort, segmentize } from './util';
33

44
let customHistory = null;
55

@@ -142,19 +142,38 @@ function initEventListeners() {
142142

143143

144144
class Router extends Component {
145-
constructor(props) {
145+
constructor(props, context) {
146146
super(props);
147+
148+
this.baseUrl = props.base || '';
149+
if (props.path) {
150+
let segments = segmentize(props.path);
151+
segments.forEach(segment => {
152+
if (segment.indexOf(':') == -1) {
153+
this.baseUrl = this.baseUrl + '/' + segment;
154+
}
155+
});
156+
}
157+
147158
if (props.history) {
148159
customHistory = props.history;
149160
}
150161

162+
if (context && context['preact-router-base'] && !props.base) {
163+
this.baseUrl = context['preact-router-base'] + this.baseUrl;
164+
}
165+
151166
this.state = {
152167
url: props.url || getCurrentUrl()
153168
};
154169

155170
initEventListeners();
156171
}
157172

173+
getChildContext() {
174+
return {['preact-router-base']: this.baseUrl};
175+
}
176+
158177
shouldComponentUpdate(props) {
159178
if (props.static!==true) return true;
160179
return props.url!==this.props.url || props.onChange!==this.props.onChange;
@@ -210,7 +229,7 @@ class Router extends Component {
210229
.filter(prepareVNodeForRanking)
211230
.sort(pathRankSort)
212231
.map( vnode => {
213-
let matches = exec(url, vnode.props.path, vnode.props);
232+
let matches = exec(url, this.baseUrl + vnode.props.path, vnode.props);
214233
if (matches) {
215234
if (invoke !== false) {
216235
let newProps = { url, matches };

src/match.js

+29-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
1-
import { h, Component } from 'preact';
2-
import { subscribers, getCurrentUrl, Link as StaticLink, exec } from 'preact-router';
1+
import { h, Component, cloneElement } from 'preact';
2+
import { subscribers, getCurrentUrl, Link as StaticLink, exec, segmentize } from 'preact-router';
33

44
export class Match extends Component {
5+
constructor(props, context) {
6+
super(props);
7+
8+
this.baseUrl = props.base || '';
9+
if (props.path) {
10+
let segments = segmentize(props.path);
11+
segments.forEach(segment => {
12+
if (segment.indexOf(':') == -1) {
13+
this.baseUrl = this.baseUrl + '/' + segment;
14+
}
15+
});
16+
}
17+
18+
if (context && context['preact-router-base']) {
19+
this.baseUrl = context['preact-router-base'] + this.baseUrl;
20+
}
21+
}
22+
523
update = url => {
624
this.nextUrl = url;
725
this.setState({});
@@ -12,15 +30,22 @@ export class Match extends Component {
1230
componentWillUnmount() {
1331
subscribers.splice(subscribers.indexOf(this.update)>>>0, 1);
1432
}
33+
getChildContext() {
34+
return {['preact-router-base']: this.baseUrl};
35+
}
1536
render(props) {
1637
let url = this.nextUrl || getCurrentUrl(),
1738
path = url.replace(/\?.+$/,'');
1839
this.nextUrl = null;
19-
return props.children({
40+
41+
const newProps = {
2042
url,
2143
path,
44+
//matches: exec(path, context['preact-router-base'] + props.path, {}) !== false
2245
matches: exec(path, props.path, {}) !== false
23-
});
46+
};
47+
48+
return typeof props.children === 'function' ? props.children(newProps) : cloneElement(props.children, newProps);
2449
}
2550
}
2651

test/dom.js

+187
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,193 @@ describe('dom', () => {
121121
expect(A.prototype.componentWillUnmount).to.have.been.calledOnce;
122122
});
123123

124+
it('should support nested routers with default', () => {
125+
class X {
126+
componentWillMount() {}
127+
componentWillUnmount() {}
128+
render(){ return <div />; }
129+
}
130+
sinon.spy(X.prototype, 'componentWillMount');
131+
sinon.spy(X.prototype, 'componentWillUnmount');
132+
class Y {
133+
componentWillMount() {}
134+
componentWillUnmount() {}
135+
render(){ return <div />; }
136+
}
137+
sinon.spy(Y.prototype, 'componentWillMount');
138+
sinon.spy(Y.prototype, 'componentWillUnmount');
139+
mount(
140+
<Router base="/app">
141+
<X path="/x" />
142+
<Router default>
143+
<Y path="/y"/>
144+
</Router>
145+
</Router>
146+
);
147+
expect(X.prototype.componentWillMount).not.to.have.been.called;
148+
expect(Y.prototype.componentWillMount).not.to.have.been.called;
149+
route('/app/x');
150+
expect(X.prototype.componentWillMount).to.have.been.calledOnce;
151+
expect(X.prototype.componentWillUnmount).not.to.have.been.called;
152+
expect(Y.prototype.componentWillMount).not.to.have.been.called;
153+
expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
154+
route('/app/y');
155+
expect(X.prototype.componentWillMount).to.have.been.calledOnce;
156+
expect(X.prototype.componentWillUnmount).to.have.been.calledOnce;
157+
expect(Y.prototype.componentWillMount).to.have.been.calledOnce;
158+
expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
159+
});
160+
161+
it('should support nested routers with path', () => {
162+
class X {
163+
componentWillMount() {}
164+
componentWillUnmount() {}
165+
render(){ return <div />; }
166+
}
167+
sinon.spy(X.prototype, 'componentWillMount');
168+
sinon.spy(X.prototype, 'componentWillUnmount');
169+
class Y {
170+
componentWillMount() {}
171+
componentWillUnmount() {}
172+
render(){ return <div />; }
173+
}
174+
sinon.spy(Y.prototype, 'componentWillMount');
175+
sinon.spy(Y.prototype, 'componentWillUnmount');
176+
mount(
177+
<Router base='/baz'>
178+
<X path="/j" />
179+
<Router path="/box/:bar*">
180+
<Y path="/k"/>
181+
</Router>
182+
</Router>
183+
);
184+
expect(X.prototype.componentWillMount).not.to.have.been.called;
185+
expect(Y.prototype.componentWillMount).not.to.have.been.called;
186+
route('/baz/j');
187+
expect(X.prototype.componentWillMount).to.have.been.calledOnce;
188+
expect(X.prototype.componentWillUnmount).not.to.have.been.called;
189+
expect(Y.prototype.componentWillMount).not.to.have.been.called;
190+
expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
191+
route('/baz/box/k');
192+
expect(X.prototype.componentWillMount).to.have.been.calledOnce;
193+
expect(X.prototype.componentWillUnmount).to.have.been.calledOnce;
194+
expect(Y.prototype.componentWillMount).to.have.been.calledOnce;
195+
expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
196+
});
197+
198+
it('should support deeply nested routers', () => {
199+
class X {
200+
componentWillMount() {}
201+
componentWillUnmount() {}
202+
render(){ return <div />; }
203+
}
204+
sinon.spy(X.prototype, 'componentWillMount');
205+
sinon.spy(X.prototype, 'componentWillUnmount');
206+
class Y {
207+
componentWillMount() {}
208+
componentWillUnmount() {}
209+
render(){ return <div />; }
210+
}
211+
sinon.spy(Y.prototype, 'componentWillMount');
212+
sinon.spy(Y.prototype, 'componentWillUnmount');
213+
mount(
214+
<Router base='/baz'>
215+
<X path="/j" />
216+
<z path="/box/:bar*">
217+
<Router path="/box">
218+
<Y path="/k"/>
219+
</Router>
220+
</z>
221+
</Router>
222+
);
223+
expect(X.prototype.componentWillMount).not.to.have.been.called;
224+
expect(Y.prototype.componentWillMount).not.to.have.been.called;
225+
route('/baz/j');
226+
expect(X.prototype.componentWillMount).to.have.been.calledOnce;
227+
expect(X.prototype.componentWillUnmount).not.to.have.been.called;
228+
expect(Y.prototype.componentWillMount).not.to.have.been.called;
229+
expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
230+
route('/baz/box/k');
231+
expect(X.prototype.componentWillMount).to.have.been.calledOnce;
232+
expect(X.prototype.componentWillUnmount).to.have.been.calledOnce;
233+
expect(Y.prototype.componentWillMount).to.have.been.calledOnce;
234+
expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
235+
});
236+
237+
it('should support nested routers and Match(s)', () => {
238+
class X {
239+
componentWillMount() {}
240+
componentWillUnmount() {}
241+
render(){ return <div />; }
242+
}
243+
sinon.spy(X.prototype, 'componentWillMount');
244+
sinon.spy(X.prototype, 'componentWillUnmount');
245+
class Y {
246+
componentWillMount() {}
247+
componentWillUnmount() {}
248+
render(){ return <div />; }
249+
}
250+
sinon.spy(Y.prototype, 'componentWillMount');
251+
sinon.spy(Y.prototype, 'componentWillUnmount');
252+
mount(
253+
<Router base='/ccc'>
254+
<X path="/jjj" />
255+
<Match path="/xxx/:bar*">
256+
<Y path="/kkk"/>
257+
</Match>
258+
</Router>
259+
);
260+
expect(X.prototype.componentWillMount, 'X1').not.to.have.been.called;
261+
expect(Y.prototype.componentWillMount, 'Y1').not.to.have.been.called;
262+
route('/ccc/jjj');
263+
expect(X.prototype.componentWillMount, 'X2').to.have.been.calledOnce;
264+
expect(X.prototype.componentWillUnmount, 'X3').not.to.have.been.called;
265+
expect(Y.prototype.componentWillMount, 'Y2').not.to.have.been.called;
266+
expect(Y.prototype.componentWillUnmount, 'Y3').not.to.have.been.called;
267+
route('/ccc/xxx/kkk');
268+
expect(X.prototype.componentWillMount, 'X4').to.have.been.calledOnce;
269+
expect(X.prototype.componentWillUnmount, 'X5').to.have.been.calledOnce;
270+
expect(Y.prototype.componentWillMount, 'Y4').to.have.been.calledOnce;
271+
expect(Y.prototype.componentWillUnmount, 'Y5').not.to.have.been.called;
272+
});
273+
274+
it('should support nested router reset via base attr', () => {
275+
class X {
276+
componentWillMount() {}
277+
componentWillUnmount() {}
278+
render(){ return <div />; }
279+
}
280+
sinon.spy(X.prototype, 'componentWillMount');
281+
sinon.spy(X.prototype, 'componentWillUnmount');
282+
class Y {
283+
componentWillMount() {}
284+
componentWillUnmount() {}
285+
render(){ return <div />; }
286+
}
287+
sinon.spy(Y.prototype, 'componentWillMount');
288+
sinon.spy(Y.prototype, 'componentWillUnmount');
289+
mount(
290+
<Router base='/baz'>
291+
<X path="/j" />
292+
<Router path="/:bar*" base="/baz/foo">
293+
<Y path="/k"/>
294+
</Router>
295+
</Router>
296+
);
297+
expect(X.prototype.componentWillMount).not.to.have.been.called;
298+
expect(Y.prototype.componentWillMount).not.to.have.been.called;
299+
route('/baz/j');
300+
expect(X.prototype.componentWillMount).to.have.been.calledOnce;
301+
expect(X.prototype.componentWillUnmount).not.to.have.been.called;
302+
expect(Y.prototype.componentWillMount).not.to.have.been.called;
303+
expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
304+
route('/baz/foo/k');
305+
expect(X.prototype.componentWillMount).to.have.been.calledOnce;
306+
expect(X.prototype.componentWillUnmount).to.have.been.calledOnce;
307+
expect(Y.prototype.componentWillMount).to.have.been.calledOnce;
308+
expect(Y.prototype.componentWillUnmount).not.to.have.been.called;
309+
});
310+
124311
it('should support re-routing', done => {
125312
class A {
126313
componentWillMount() {

0 commit comments

Comments
 (0)