Skip to content

Commit 7b1813e

Browse files
committed
feat: create <Router> and <Route> components
1 parent 254c772 commit 7b1813e

File tree

4 files changed

+257
-2
lines changed

4 files changed

+257
-2
lines changed

docs/route.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Route
2+
3+
Routing components allow you to create implement single page app routing functionality.
4+
5+
Contents:
6+
7+
- `<RouteProvider>`
8+
- `<Route>`
9+
- `go()`
10+
- `withRoute()`
11+
12+
## `<RouteProvider>`
13+
14+
15+
16+
## `<Route>`
17+
18+

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"to-string-loader": "1.1.5",
4646
"gulp": "3.9.1",
4747
"gulp-typescript": "3",
48+
"react-syntax-highlighter": "^6.1.2",
4849
"@types/node": "9.3.0",
4950
"@types/jest": "22.0.1",
5051
"@types/chai": "4.1.1",
@@ -67,8 +68,9 @@
6768
"test:client:coverage": "jest --coverage",
6869
"test:watch": "jest --watch",
6970
"pub": "npm publish --registry http://sinopia.devhsk.mol.dmgt.net:4873",
70-
"storybook": "start-storybook -p 6006",
71-
"build-storybook": "build-storybook"
71+
"storybook": "start-storybook -p 6007",
72+
"build-storybook": "build-storybook",
73+
"start": "npm run storybook"
7274
},
7375
"jest": {
7476
"moduleFileExtensions": [

src/route/index.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {Component} from 'react';
2+
import {LocationSensor} from '../LocationSensor';
3+
import {Provider, Consumer} from '../context';
4+
import {h, sym} from '../util';
5+
6+
const $$location = sym('location');
7+
8+
export interface IRouteProviderProps {
9+
children?: any;
10+
route?: string;
11+
parent?: TRouteMatchResult;
12+
}
13+
14+
export class Router extends Component<IRouteProviderProps, any> {
15+
matches: number = 0;
16+
17+
onMatch = () => {
18+
this.matches++;
19+
};
20+
21+
renderProvider (route) {
22+
this.matches = 0;
23+
24+
const element = h(Provider, {
25+
name: $$location,
26+
value: {
27+
route,
28+
onMatch: this.onMatch,
29+
getMathces: () => this.matches,
30+
parent: this.props.parent
31+
}
32+
}, this.props.children);
33+
34+
return element;
35+
}
36+
37+
render () {
38+
const {props} = this;
39+
const {children, route} = props;
40+
41+
if(typeof route === 'string') {
42+
return this.renderProvider(route);
43+
}
44+
45+
return h(LocationSensor, {}, (value) => this.renderProvider(value.pathname));
46+
}
47+
}
48+
49+
export interface TRouteMatchResult {
50+
length: number; // Length how many characters to truncate from route.
51+
matches?: RegExpMatchArray;
52+
}
53+
54+
export type TRouteMatcher = (route: string) => TRouteMatchResult;
55+
56+
export interface IRouteMatch {
57+
children?: React.ReactElement<any> | ((params) => React.ReactElement<any>);
58+
comp?: React.ComponentClass<{}> | React.StatelessComponent<{}>;
59+
exact?: boolean;
60+
match?: TRouteMatcher | RegExp | string;
61+
cnt?: number;
62+
preserve?: boolean;
63+
}
64+
65+
export class Route extends Component<IRouteMatch, any> {
66+
static defaultProps = {
67+
match: /.*/,
68+
cnt: 0
69+
};
70+
71+
matcher (): TRouteMatcher {
72+
const {match} = this.props;
73+
let matcher: TRouteMatcher;
74+
75+
if (typeof match === 'function') {
76+
return match;
77+
}
78+
79+
let regex: RegExp;
80+
81+
if (typeof match === 'string') {
82+
regex = new RegExp(`^(${match}${this.props.exact ? '$' : ''})`);
83+
} else {
84+
regex = match;
85+
}
86+
87+
return (route: string) => {
88+
const matches = route.match(regex);
89+
90+
if (!matches) {
91+
return null;
92+
}
93+
94+
return {
95+
length: (matches && matches[1]) ? matches[1].length : 0,
96+
matches
97+
};
98+
};
99+
}
100+
101+
render () {
102+
return h(Consumer, {name: $$location}, ({route, onMatch, getMathces, parent}) => {
103+
const {children, match} = this.props;
104+
105+
if (getMathces() <= this.props.cnt) {
106+
const matchResult = this.matcher()(route);
107+
108+
if (matchResult) {
109+
(matchResult as any).parent = parent;
110+
const {matches, length} = matchResult;
111+
112+
// Notify <RouteProvider> that we matched.
113+
onMatch(this, matchResult);
114+
115+
if (!this.props.preserve && length) {
116+
route = route.substr(length);
117+
}
118+
119+
return h(Router, {route, parent: matchResult},
120+
this.props.comp ?
121+
h(this.props.comp) :
122+
typeof children === 'function' ? children({matches, route, parent}) : children
123+
);
124+
}
125+
}
126+
127+
return null;
128+
});
129+
}
130+
}

src/route/story.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {createElement as h} from 'react';
2+
import {storiesOf} from '@storybook/react';
3+
import {action} from '@storybook/addon-actions';
4+
import {linkTo} from '@storybook/addon-links';
5+
import SyntaxHighlighter from 'react-syntax-highlighter';
6+
import {docco} from 'react-syntax-highlighter/styles/hljs'
7+
import {LocationSensor} from '../LocationSensor';
8+
import {Router, Route} from '.';
9+
import ShowDocs from '../../.storybook/ShowDocs'
10+
11+
storiesOf('Context/route', module)
12+
.add('Documentation', () => h(ShowDocs, {name: 'route'}))
13+
.add('Example', () =>
14+
<Router>
15+
<div>
16+
<ul>
17+
<li onClick={() => history.pushState(null, '', '/home.html')}>Home</li>
18+
<li onClick={() => history.pushState(null, '', '/home/intro.html')}>Home / Intro</li>
19+
<li onClick={() => history.pushState(null, '', '/home/more.html')}>Home / More</li>
20+
<li onClick={() => history.pushState(null, '', '/page.html')}>Page</li>
21+
<li onClick={() => history.pushState(null, '', '/404.html')}>404</li>
22+
</ul>
23+
24+
<Route match='/home'>{() =>
25+
<div>
26+
<div>HOME</div>
27+
<Route match='/intro'>
28+
<div>INTRO</div>
29+
</Route>
30+
<Route children={<div>404</div>} />
31+
</div>
32+
}</Route>
33+
34+
<Route match={/^\/page\.html/} children={<div>PAGE</div>} />
35+
<Route children={<div>404</div>} />
36+
37+
<br />
38+
<hr />
39+
40+
<LocationSensor>{(state) =>
41+
<pre style={{fontFamily: 'monospace'}}>
42+
{JSON.stringify(state, null, 4)}
43+
</pre>
44+
}</LocationSensor>
45+
</div>
46+
</Router>
47+
)
48+
.add('Truncates route', () =>
49+
<div>
50+
<Router route='/api/users/43f23f-23f34f43r.json'>
51+
<Route match='/api'>
52+
<Route match='/users'>
53+
<Route match={/.*/}>{(result) =>
54+
<pre style={{fontFamily: 'monospace'}}>{JSON.stringify(result, null, 4)}</pre>
55+
}</Route>
56+
</Route>
57+
</Route>
58+
</Router>
59+
60+
<hr />
61+
62+
<pre style={{fontFamily: 'monospace'}}>{`
63+
<Router route='/api/users/43f23f-23f34f43r.json'>
64+
<Route match='/api'>
65+
<Route match='/users'>
66+
<Route match={/.*/}>{(result) =>
67+
<pre style={{fontFamily: 'monospace'}}>{JSON.stringify(result, null, 4)}</pre>
68+
}</Route>
69+
</Route>
70+
</Route>
71+
</Router>
72+
`}</pre>
73+
</div>
74+
)
75+
.add('Preserve route', () =>
76+
<div>
77+
<Router route='/api/users/id/123'>
78+
<Route preserve match='/api'>
79+
<Route preserve match='/api/users'>
80+
<Route preserve match='/api/users/id'>
81+
<Route preserve match={/\/api\/users\/id\/(.*)/}>{(result) =>
82+
<pre style={{fontFamily: 'monospace'}}>{JSON.stringify(result, null, 4)}</pre>
83+
}</Route>
84+
</Route>
85+
</Route>
86+
</Route>
87+
</Router>
88+
89+
<hr />
90+
91+
<pre style={{fontFamily: 'monospace'}}>{`
92+
<Router route='/api/users/id/123'>
93+
<Route preserve match='/api'>
94+
<Route preserve match='/api/users'>
95+
<Route preserve match='/api/users/id'>
96+
<Route preserve match={/\/api\/users\/id\/(.*)/}>{(result) =>
97+
<pre style={{fontFamily: 'monospace'}}>{JSON.stringify(result, null, 4)}</pre>
98+
}</Route>
99+
</Route>
100+
</Route>
101+
</Route>
102+
</Router>
103+
`}</pre>
104+
</div>
105+
);

0 commit comments

Comments
 (0)