Skip to content

Commit 8e4a60c

Browse files
committed
sneak peek preview release
1 parent d5605e1 commit 8e4a60c

14 files changed

+458
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.idea
22
node_modules
3+
dev_modules
34
*.log
45
lib
56
typings

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# React Router Async
2+
[![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges)
3+
[![Latest Stable Version](https://img.shields.io/npm/v/react-router-async.svg?style=flat-square)](https://www.npmjs.com/package/react-router-async)
4+
[![License](https://img.shields.io/npm/l/react-router-async.svg?style=flat-square)](https://www.npmjs.com/package/react-router-async)
5+
6+
``IMPORTANT NOTE! STATUS FOR THIS PROJECT IS EXPERIMENTAL AND THINGS CAN CHANGE, IT'S NOT READY FOR PRODUCTION``
7+
8+
React Router Async is ultimate routing solution for your React applications, especially it's good in universal applications.
9+
10+
## Installation
11+
```bash
12+
yarn add react-router-async
13+
```
14+
15+
## Usage
16+
On client:
17+
```javascript
18+
import React from 'react';
19+
import ReactDOM from 'react-dom';
20+
import { Router } from 'react-router-async';
21+
import { routes, hooks } from './common';
22+
import createHistory from 'history/createBrowserHistory';
23+
24+
const history = createHistory();
25+
const mountNode = document.getElementById('app');
26+
27+
Router.init({ path: history.location.pathname, routes, hooks }).then(({ Router, Component, router, callback }) => {
28+
ReactDOM.render(<Router {...{ Component, router, history }} />, mountNode, callback);
29+
}).catch(error => console.log(error));
30+
```
31+
32+
On server (for example as express middleware):
33+
```javascript
34+
export default function (req, res, next) {
35+
Router.init({ path: req.path, routes, hooks }).then(({ Component, status, redirect }) => {
36+
if (redirect) {
37+
res.redirect(status, redirect);
38+
} else {
39+
const html = ReactDOM.renderToStaticMarkup(HtmlComponent({
40+
markup: ReactDOM.renderToString(<Component />),
41+
assets: assets
42+
}));
43+
res.status(status).send('<!DOCTYPE html>' + html);
44+
}
45+
}).catch(error => {
46+
if (error.name === 'RouterError') {
47+
res.status(error.code).send(error.message);
48+
} else {
49+
res.status(500).send('Internal error');
50+
}
51+
});
52+
}
53+
```
54+

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-router-async",
3-
"version": "0.0.1",
3+
"version": "0.0.2",
44
"author": {
55
"name": "Oleg Martynov",
66
"email": "[email protected]"
@@ -23,5 +23,9 @@
2323
},
2424
"devDependencies": {
2525
"typescript": "next"
26+
},
27+
"dependencies": {
28+
"react": "^15.3.2",
29+
"router-async": "0.0.2"
2630
}
27-
}
31+
}

src/helpers.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export const jsxInstanceOf = cls => {
2+
return (props, propName, componentName) => {
3+
if (props[propName].type !== cls.default) {
4+
return new Error(
5+
'Invalid prop `' + propName + '` supplied to' +
6+
' `' + componentName + '`. Validation failed.'
7+
);
8+
}
9+
}
10+
};
11+
12+
export function deepMap(arr, f, ctx = null) {
13+
return arr.map((val, key) => {
14+
let obj = f.call(ctx, val, key);
15+
if (Array.isArray(obj.childs)) {
16+
obj.childs = deepMap(obj.childs, f, ctx);
17+
return obj;
18+
} else {
19+
return obj;
20+
}
21+
})
22+
}

src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
console.log('nooooop');
1+
import Router from './router';
2+
import Route from './route';
3+
import Link from './link';
4+
import RootRoute from './root-route';
5+
import Middleware from './middleware';
6+
import Redirect from './redirect';
7+
8+
export { Router, Route, Link, RootRoute, Middleware, Redirect };

src/link.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as React from 'react';
2+
3+
export interface Props {
4+
to: any,
5+
[propName: string]: any;
6+
}
7+
export interface State {
8+
[propName: string]: any;
9+
}
10+
11+
export default class Link extends React.Component<Props, State> {
12+
context: any;
13+
static contextTypes = {
14+
router: React.PropTypes.object
15+
};
16+
navigate = e => {
17+
this.context.router.navigate(this.props.to);
18+
e.preventDefault();
19+
};
20+
render() {
21+
return <a href={this.props.to} onClick={this.navigate}>{this.props.children}</a>;
22+
}
23+
}

src/middleware.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
import { jsxInstanceOf } from './helpers';
3+
import * as Route from './route';
4+
import * as Redirect from './redirect';
5+
6+
export interface Props {
7+
[propName: string]: any;
8+
}
9+
export interface State {
10+
[propName: string]: any;
11+
}
12+
13+
export default class Middleware extends React.Component<Props, State> {
14+
static propTypes = {
15+
children: React.PropTypes.arrayOf(React.PropTypes.oneOfType([
16+
jsxInstanceOf(Route),
17+
jsxInstanceOf(Redirect)
18+
])).isRequired,
19+
path: React.PropTypes.string.isRequired,
20+
action: React.PropTypes.func
21+
};
22+
render() {
23+
return React.Children.only(this.props.children);
24+
}
25+
}

src/redirect.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as React from 'react';
2+
3+
export interface Props {
4+
[propName: string]: any;
5+
}
6+
export interface State {
7+
[propName: string]: any;
8+
}
9+
10+
export default class Redirect extends React.Component<Props, State> {
11+
static propTypes = {
12+
path: React.PropTypes.string.isRequired,
13+
to: React.PropTypes.string.isRequired,
14+
status: React.PropTypes.oneOf([301, 302])
15+
};
16+
static defaultProps = {
17+
status: 302
18+
};
19+
render() {
20+
console.error('Redirect can\'t render');
21+
return null;
22+
}
23+
}

src/root-route.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
import { jsxInstanceOf } from './helpers';
3+
import * as Route from './route';
4+
import * as Middleware from './middleware';
5+
import * as Redirect from './redirect';
6+
7+
export interface Props {
8+
[propName: string]: any;
9+
}
10+
export interface State {
11+
[propName: string]: any;
12+
}
13+
14+
export default class RootRoute extends React.Component<Props, State> {
15+
static propTypes = {
16+
children: React.PropTypes.arrayOf(React.PropTypes.oneOfType([
17+
jsxInstanceOf(Route),
18+
jsxInstanceOf(Middleware),
19+
jsxInstanceOf(Redirect)
20+
])).isRequired
21+
};
22+
render() {
23+
return React.Children.only(this.props.children);
24+
}
25+
}

src/route.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from 'react';
2+
3+
export interface Props {
4+
[propName: string]: any;
5+
}
6+
export interface State {
7+
[propName: string]: any;
8+
}
9+
10+
export default class Route extends React.Component<Props, State> {
11+
static propTypes = {
12+
path: React.PropTypes.string.isRequired,
13+
action: React.PropTypes.func.isRequired,
14+
status: React.PropTypes.oneOf([200, 404, 500])
15+
};
16+
render() {
17+
console.error('Route can\'t render');
18+
return null;
19+
}
20+
}

src/router.tsx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import RouterAsync from 'router-async';
2+
import * as React from 'react';
3+
import { deepMap } from './helpers';
4+
5+
export interface Props {
6+
Component: React.ReactElement<Props>,
7+
router: any,
8+
histroy: any,
9+
errorHandler: any,
10+
[propName: string]: any;
11+
}
12+
export interface State {
13+
location: any,
14+
Component: React.ReactElement<Props>;
15+
}
16+
export interface Action {
17+
(): React.ReactElement<Props>;
18+
}
19+
interface Childs {
20+
[index: number]: Route;
21+
}
22+
export interface Route {
23+
path?: string,
24+
action?: Action,
25+
status?: number,
26+
to?: string,
27+
childs?: Childs
28+
}
29+
30+
export default class Router extends React.Component<Props, State> {
31+
private router: any;
32+
private history: any;
33+
private unlistenHistroy: any;
34+
constructor(props) {
35+
super();
36+
this.state = {
37+
Component: props.Component,
38+
location: props.history.location
39+
};
40+
41+
this.router = props.router;
42+
this.history = props.history;
43+
}
44+
static async init({ path, routes, hooks }) {
45+
const plainRoutes = Router.buildRoutes(routes);
46+
const router = new RouterAsync({ routes: plainRoutes, hooks });
47+
const { result, redirect, status } = await router.resolve({ path });
48+
return {
49+
Router,
50+
Component: result,
51+
redirect,
52+
status,
53+
router,
54+
callback: this.makeCallback(router)
55+
}
56+
}
57+
static buildRoutes(routes) {
58+
if (!Array.isArray(routes)) {
59+
routes = routes.props.children;
60+
}
61+
return deepMap(routes, route => {
62+
const result: Route = {};
63+
if (route.props.path) {
64+
result.path = route.props.path;
65+
}
66+
if (route.props.action) {
67+
result.action = route.props.action;
68+
}
69+
if (route.props.status) {
70+
result.status = route.props.status;
71+
}
72+
if (route.props.to) {
73+
result.to = route.props.to;
74+
}
75+
if (route.props.children) {
76+
result.childs = Array.isArray(route.props.children) ? route.props.children : [route.props.children];
77+
}
78+
return result;
79+
});
80+
}
81+
static makeCallback(router, options = {}) {
82+
return () => {
83+
return Router.runRenderHooks(router, options);
84+
}
85+
}
86+
static runRenderHooks(router, options = {}) {
87+
return router.runHooks('render', options);
88+
}
89+
static childContextTypes = {
90+
router: React.PropTypes.object
91+
};
92+
getChildContext() {
93+
return {
94+
router: this
95+
};
96+
}
97+
get location() {
98+
return this.state.location;
99+
}
100+
async navigate(path) {
101+
try {
102+
const { redirect } = await this.router.match({ path, ctx: {} });
103+
if (redirect) {
104+
this.history.push(redirect);
105+
} else {
106+
this.history.push(path);
107+
}
108+
} catch (error) {
109+
if (this.props.errorHandler) {
110+
this.props.errorHandler(error, this);
111+
} else {
112+
console.error('Navigate Error', error);
113+
throw error;
114+
}
115+
}
116+
}
117+
private _locationChanged = async (location, action) => {
118+
const { result } = await this.router.resolve({ path: location.pathname, ctx: {} });
119+
this.setState({
120+
Component: result,
121+
location
122+
}, Router.makeCallback(this.router));
123+
};
124+
componentDidMount() {
125+
this.unlistenHistroy = this.history.listen(this._locationChanged)
126+
}
127+
componentWillUnmount() {
128+
this.unlistenHistroy();
129+
}
130+
render() {
131+
return <this.state.Component/>
132+
}
133+
}

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"target": "es5",
44
"module": "commonjs",
55
"outDir": "./lib",
6-
"declaration": true
6+
// "declaration": true,
7+
"jsx": "react"
78
},
89
"include": [
910
"src/**/*",

typings.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
"globalDependencies": {
3-
"es2015": "registry:env/es2015#1.0.0+20160526151700"
3+
"es6-promise": "registry:dt/es6-promise#0.0.0+20160614011821",
4+
"node": "registry:env/node#6.0.0+20161011193755"
5+
},
6+
"dependencies": {
7+
"react": "registry:npm/react#15.0.1+20160601175240"
48
}
5-
}
9+
}

0 commit comments

Comments
 (0)