Skip to content

Commit 5ae09e8

Browse files
committed
Add Router.match
Also, added a transition object that is passed to transition hooks that can be used to abort the transition or send it off somewhere else.
1 parent efa02a1 commit 5ae09e8

File tree

6 files changed

+469
-217
lines changed

6 files changed

+469
-217
lines changed

modules/PropTypes.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import Location from './Location';
33
import History from './History';
44

5-
var { any, func, object, arrayOf, instanceOf, oneOfType, oneOf, element } = React.PropTypes;
5+
var { func, object, arrayOf, instanceOf, oneOfType, element } = React.PropTypes;
66

77
function falsy(props, propName, componentName) {
88
if (props[propName])
@@ -13,8 +13,8 @@ var component = func;
1313
var components = oneOfType([ component, object ]);
1414
var history = instanceOf(History);
1515
var location = instanceOf(Location);
16-
var route = any; //oneOf([object, element]);
17-
var routes = any; //oneOf([route, arrayOf(route), object]);
16+
var route = oneOfType([ object, element ]);
17+
var routes = oneOfType([ route, arrayOf(route) ]);
1818

1919
module.exports = {
2020
falsy,

modules/Router.js

Lines changed: 136 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import invariant from 'invariant';
44
import { loopAsync } from './AsyncUtils';
55
import { createRoutes } from './RouteUtils';
66
import { pathnameIsActive, queryIsActive } from './ActiveUtils';
7-
import { getState, getTransitionHooks, createTransitionHook, getComponents, getRouteParams } from './RoutingUtils';
7+
import { getState, getTransitionHooks, getComponents, createTransitionHook, getRouteParams, runTransition } from './RoutingUtils';
88
import { routes, component, components, history, location } from './PropTypes';
99
import Location from './Location';
1010

11-
var { any, array, func, object, instanceOf } = React.PropTypes;
11+
var { arrayOf, func, object } = React.PropTypes;
1212

13-
var ContextMixin = {
13+
var ServerTransitionDelegate = {
14+
getState,
15+
getTransitionHooks,
16+
getComponents
17+
};
18+
19+
var RoutingContextMixin = {
1420

1521
childContextTypes: {
1622
router: object.isRequired
@@ -72,32 +78,89 @@ var ContextMixin = {
7278

7379
};
7480

81+
var TransitionDelegateMixin = {
82+
83+
/**
84+
* Adds a transition hook that runs before all route hooks in a
85+
* transition. The signature is the same as route transition hooks.
86+
*/
87+
addTransitionHook(hook) {
88+
if (!this.transitionHooks)
89+
this.transitionHooks = [];
90+
91+
this.transitionHooks.push(hook);
92+
},
93+
94+
/**
95+
* Removes the given transition hook.
96+
*/
97+
removeTransitionHook(hook) {
98+
if (this.transitionHooks)
99+
this.transitionHooks = this.transitionHooks.filter(h => h !== hook);
100+
},
101+
102+
getState(routes, location, callback) {
103+
var { branch, params } = this.props;
104+
105+
if (branch && params) {
106+
callback(null, { branch, params });
107+
} else {
108+
getState(routes, location, callback);
109+
}
110+
},
111+
112+
getTransitionHooks(prevState, nextState) {
113+
var hooks = [];
114+
115+
// Run component hooks before route hooks.
116+
if (this.transitionHooks)
117+
hooks.push.apply(hooks, this.transitionHooks.map(hook => createTransitionHook(hook, this)));
118+
119+
hooks.push.apply(hooks, getTransitionHooks(prevState, nextState));
120+
121+
return hooks;
122+
},
123+
124+
getComponents(nextState, callback) {
125+
var { components } = this.props;
126+
127+
if (components) {
128+
callback(null, components);
129+
} else {
130+
getComponents(nextState, callback);
131+
}
132+
}
133+
134+
};
135+
75136
export var Router = React.createClass({
76137

77-
mixins: [ ContextMixin ],
138+
mixins: [ RoutingContextMixin, TransitionDelegateMixin ],
78139

79140
statics: {
80141

81-
match(routes, history, callback) {
82-
// TODO: Mimic what we're doing in _updateState, but statically
83-
// so we can get the right props for doing server-side rendering.
142+
match(routes, location, callback) {
143+
runTransition(null, routes, location, ServerTransitionDelegate, callback);
84144
}
85145

86146
},
87147

88148
propTypes: {
89-
history: history.isRequired,
90-
children: routes,
91-
routes, // Alias for children
92149
createElement: func.isRequired,
93150
onError: func.isRequired,
94151
onUpdate: func,
95152

96-
// For server-side rendering
97-
location: any,
153+
// Client-side
154+
history,
155+
routes,
156+
// Routes may also be given as children (JSX)
157+
children: routes,
158+
159+
// Server-side
160+
location,
98161
branch: routes,
99162
params: object,
100-
components
163+
components: arrayOf(components)
101164
},
102165

103166
getDefaultProps() {
@@ -126,133 +189,44 @@ export var Router = React.createClass({
126189
'A <Router> needs a valid Location'
127190
);
128191

129-
this.nextLocation = location;
130192
this.setState({ isTransitioning: true });
131193

132-
this._getState(this.routes, location, (error, state) => {
133-
if (error || this.nextLocation !== location) {
134-
this._finishTransition(error);
194+
runTransition(this.state, this.routes, location, this, (error, transition, state) => {
195+
this.setState({ isTransitioning: false });
196+
197+
if (error) {
198+
this.handleError(error);
199+
} else if (transition.isCancelled) {
200+
if (transition.redirectInfo) {
201+
var { pathname, query, state } = transition.redirectInfo;
202+
this.replaceWith(pathname, query, state);
203+
} else {
204+
invariant(
205+
this.state.location,
206+
'You may not abort the initial transition'
207+
);
208+
209+
// TODO: Do something with transition.abortReason ?
210+
211+
// The best we can do here is goBack so the location state reverts
212+
// to what it was. However, we also set a flag so that we know not
213+
// to run through _updateState again since state did not change.
214+
this._ignoreNextHistoryChange = true;
215+
this.goBack();
216+
}
135217
} else if (state == null) {
136218
warning(false, 'Location "%s" did not match any routes', location.pathname);
137-
this._finishTransition();
138219
} else {
139-
state.location = location;
140-
141-
this._runTransitionHooks(state, (error) => {
142-
if (error || this.nextLocation !== location) {
143-
this._finishTransition(error);
144-
} else {
145-
this._getComponents(state, (error, components) => {
146-
if (error || this.nextLocation !== location) {
147-
this._finishTransition(error);
148-
} else {
149-
state.components = components;
150-
151-
this._finishTransition(null, state);
152-
}
153-
});
154-
}
155-
});
220+
this.setState(state, this.props.onUpdate);
221+
this._alreadyUpdated = true;
156222
}
157223
});
158224
},
159225

160-
_finishTransition(error, state) {
161-
this.setState({ isTransitioning: false });
162-
this.nextLocation = null;
163-
164-
if (error) {
165-
this.handleError(error);
166-
} else if (state) {
167-
this.setState(state, this.props.onUpdate);
168-
this._alreadyUpdated = true;
169-
}
170-
},
171-
172-
_getState(routes, location, callback) {
173-
var { branch, params } = this.props;
174-
175-
if (branch && params && query) {
176-
callback(null, { branch, params });
177-
} else {
178-
getState(routes, location, callback);
179-
}
180-
},
181-
182-
_runTransitionHooks(nextState, callback) {
183-
// Run component hooks before route hooks.
184-
var hooks = this.transitionHooks.map(hook => createTransitionHook(hook, this));
185-
186-
hooks.push.apply(
187-
hooks,
188-
getTransitionHooks(this.state, nextState)
189-
);
190-
191-
var nextLocation = this.nextLocation;
192-
193-
loopAsync(hooks.length, (index, next, done) => {
194-
var hook = hooks[index];
195-
196-
hooks[index].call(this, nextState, this, (error) => {
197-
if (error || this.nextLocation !== nextLocation) {
198-
done.call(this, error); // No need to continue.
199-
} else {
200-
next.call(this);
201-
}
202-
});
203-
}, callback);
204-
},
205-
206-
_getComponents(nextState, callback) {
207-
if (this.props.components) {
208-
callback(null, this.props.components);
209-
} else {
210-
getComponents(nextState, callback);
211-
}
212-
},
213-
214226
_createElement(component, props) {
215227
return typeof component === 'function' ? this.props.createElement(component, props) : null;
216228
},
217229

218-
/**
219-
* Adds a transition hook that runs before all route hooks in a
220-
* transition. The signature is the same as route transition hooks.
221-
*/
222-
addTransitionHook(hook) {
223-
this.transitionHooks.push(hook);
224-
},
225-
226-
/**
227-
* Removes the given transition hook.
228-
*/
229-
removeTransitionHook(hook) {
230-
this.transitionHooks = this.transitionHooks.filter(h => h !== hook);
231-
},
232-
233-
/**
234-
* Cancels the current transition, preventing any subsequent transition
235-
* hooks from running and restoring the previous location.
236-
*/
237-
cancelTransition() {
238-
invariant(
239-
this.state.location,
240-
'Router#cancelTransition: You may not cancel the initial transition'
241-
);
242-
243-
if (this.nextLocation) {
244-
this.nextLocation = null;
245-
246-
// The best we can do here is goBack so the location state reverts
247-
// to what it was. However, we also set a flag so that we know not
248-
// to run through _updateState again.
249-
this._ignoreNextHistoryChange = true;
250-
this.goBack();
251-
} else {
252-
warning(false, 'Router#cancelTransition: Router is not transitioning');
253-
}
254-
},
255-
256230
handleHistoryChange() {
257231
if (this._ignoreNextHistoryChange) {
258232
this._ignoreNextHistoryChange = false;
@@ -264,23 +238,34 @@ export var Router = React.createClass({
264238
componentWillMount() {
265239
var { history, routes, children } = this.props;
266240

267-
invariant(
268-
routes || children,
269-
'A <Router> needs some routes'
270-
);
241+
if (history) {
242+
invariant(
243+
routes || children,
244+
'A client-side <Router> needs some routes. Try using <Router routes> or ' +
245+
'passing your routes as nested <Route> children'
246+
);
247+
248+
this.routes = createRoutes(routes || children);
249+
250+
if (typeof history.setup === 'function')
251+
history.setup();
271252

272-
this.routes = createRoutes(routes || children);
273-
this.transitionHooks = [];
274-
this.nextLocation = null;
253+
// We need to listen first in case we redirect immediately.
254+
if (history.addChangeListener)
255+
history.addChangeListener(this.handleHistoryChange);
275256

276-
if (typeof history.setup === 'function')
277-
history.setup();
257+
this._updateState(history.location);
258+
} else {
259+
var { location, branch, params, components } = this.props;
278260

279-
// We need to listen first in case we redirect immediately.
280-
if (history.addChangeListener)
281-
history.addChangeListener(this.handleHistoryChange);
261+
invariant(
262+
location && branch && params && components,
263+
'A server-side <Router> needs location, branch, params, and components ' +
264+
'props. Try using Router.match to get all the props you need'
265+
);
282266

283-
this._updateState(history.location);
267+
this.setState({ location, branch, params, components });
268+
}
284269
},
285270

286271
componentDidMount() {
@@ -298,23 +283,25 @@ export var Router = React.createClass({
298283
'<Router history> may not be changed'
299284
);
300285

301-
var currentRoutes = this.props.routes || this.props.children;
302-
var nextRoutes = nextProps.routes || nextProps.children;
286+
if (nextProps.history) {
287+
var currentRoutes = this.props.routes || this.props.children;
288+
var nextRoutes = nextProps.routes || nextProps.children;
303289

304-
if (currentRoutes !== nextRoutes) {
305-
this.routes = createRoutes(nextRoutes);
290+
if (currentRoutes !== nextRoutes) {
291+
this.routes = createRoutes(nextRoutes);
306292

307-
// Call this here because _updateState
308-
// uses this.routes to determine state.
309-
if (nextProps.history.location)
310-
this._updateState(nextProps.history.location);
293+
// Call this here because _updateState
294+
// uses this.routes to determine state.
295+
if (nextProps.history.location)
296+
this._updateState(nextProps.history.location);
297+
}
311298
}
312299
},
313300

314301
componentWillUnmount() {
315302
var { history } = this.props;
316303

317-
if (history.removeChangeListener)
304+
if (history && history.removeChangeListener)
318305
history.removeChangeListener(this.handleHistoryChange);
319306
},
320307

0 commit comments

Comments
 (0)