From a8a300578efad4844f0f725c078704433a1d02a5 Mon Sep 17 00:00:00 2001 From: Saket Patel Date: Wed, 18 Sep 2019 12:38:47 +0530 Subject: [PATCH] Rebase https://github.com/preactjs/preact-router/pull/100 with latest changes --- README.md | 15 +++++ src/index.js | 25 ++++++- src/match.js | 33 +++++++-- test/dom.js | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 253 insertions(+), 7 deletions(-) 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() {