Dashboard
@@ -56,7 +58,6 @@ var Dashboard = React.createClass({
});
var Login = React.createClass({
-
mixins: [ Navigation ],
getInitialState() {
@@ -115,9 +116,9 @@ var Logout = React.createClass({
}
});
-function requireAuth(nextState, transition) {
+function requireAuth(nextState, redirectTo) {
if (!auth.loggedIn())
- transition.to('/login', null, { nextPathname: nextState.location.pathname });
+ redirectTo('/login', null, { nextPathname: nextState.location.pathname });
}
React.render((
diff --git a/examples/dynamic-segments/app.js b/examples/dynamic-segments/app.js
index a46c96bff0..ba01d9fe7f 100644
--- a/examples/dynamic-segments/app.js
+++ b/examples/dynamic-segments/app.js
@@ -1,7 +1,9 @@
import React from 'react';
-import { history } from 'react-router/lib/HashHistory';
+import createHistory from 'history/lib/createHashHistory';
import { Router, Route, Link, Redirect } from 'react-router';
+var history = createHistory();
+
var App = React.createClass({
render() {
return (
@@ -50,8 +52,8 @@ React.render((
-
-
+
+
diff --git a/examples/transitions/app.js b/examples/transitions/app.js
index 8fca7f7e51..c8645b71df 100644
--- a/examples/transitions/app.js
+++ b/examples/transitions/app.js
@@ -1,6 +1,8 @@
-import React, { findDOMNode } from 'react';
-import { Router, Route, Link, Navigation, TransitionHook } from 'react-router';
-import { history } from 'react-router/lib/HashHistory';
+import React from 'react';
+import createHistory from 'history/lib/createHashHistory';
+import { Router, Route, Link, Navigation } from 'react-router';
+
+var history = createHistory();
var App = React.createClass({
render() {
@@ -29,18 +31,43 @@ var Dashboard = React.createClass({
});
var Form = React.createClass({
- mixins: [ Navigation, TransitionHook ],
+ mixins: [ Navigation ],
+
+ getInitialState() {
+ return {
+ textValue: 'ohai'
+ };
+ },
- routerWillLeave(nextState, transition) {
- if (findDOMNode(this.refs.userInput).value !== '')
- if (!confirm('You have unsaved information, are you sure you want to leave this page?'))
- transition.abort();
+ transitionHook() {
+ if (this.state.textValue)
+ return 'You have unsaved information, are you sure you want to leave this page?';
+ },
+
+ componentDidMount() {
+ history.registerTransitionHook(this.transitionHook);
+ },
+
+ componentWillUnmount() {
+ history.unregisterTransitionHook(this.transitionHook);
+ },
+
+ handleChange(event) {
+ var { value } = event.target;
+
+ this.setState({
+ textValue: value
+ });
},
handleSubmit(event) {
event.preventDefault();
- findDOMNode(this.refs.userInput).value = '';
- this.transitionTo('/');
+
+ this.setState({
+ textValue: ''
+ }, () => {
+ this.transitionTo('/');
+ });
},
render() {
@@ -48,7 +75,7 @@ var Form = React.createClass({
diff --git a/modules/ActiveMixin.js b/modules/ActiveMixin.js
new file mode 100644
index 0000000000..4623c113ac
--- /dev/null
+++ b/modules/ActiveMixin.js
@@ -0,0 +1,69 @@
+import { matchPattern } from './PatternUtils';
+
+/**
+ * Returns true if a route and params that match the given
+ * pathname are currently active.
+ */
+function pathnameIsActive(pathname, activePathname, activeRoutes, activeParams) {
+ if (pathname === activePathname || activePathname.indexOf(pathname + '/') === 0)
+ return true;
+
+ var route, pattern, basename;
+ for (var i = 0, len = activeRoutes.length; i < len; ++i) {
+ route = activeRoutes[i];
+ pattern = route.path || '';
+
+ if (pattern.indexOf('/') !== 0)
+ pattern = basename.replace(/\/*$/, '/') + pattern; // Relative paths build on the parent's path.
+
+ var { remainingPathname, paramNames, paramValues } = matchPattern(pattern, pathname);
+
+ if (remainingPathname === '') {
+ return paramNames.every(function (paramName, index) {
+ return String(paramValues[index]) === String(activeParams[paramName]);
+ });
+ }
+
+ basename = pattern;
+ }
+
+ return false;
+}
+
+/**
+ * Returns true if all key/value pairs in the given query are
+ * currently active.
+ */
+function queryIsActive(query, activeQuery) {
+ if (activeQuery == null)
+ return query == null;
+
+ if (query == null)
+ return true;
+
+ for (var p in query)
+ if (query.hasOwnProperty(p) && String(query[p]) !== String(activeQuery[p]))
+ return false;
+
+ return true;
+}
+
+var ActiveMixin = {
+
+ /**
+ * Returns true if a
to the given pathname/query combination is
+ * currently active.
+ */
+ isActive(pathname, query) {
+ var { location, routes, params } = this.state;
+
+ if (location == null)
+ return false;
+
+ return pathnameIsActive(pathname, location.pathname, routes, params) &&
+ queryIsActive(query, location.query);
+ }
+
+};
+
+export default ActiveMixin;
diff --git a/modules/BrowserHistory.js b/modules/BrowserHistory.js
deleted file mode 100644
index bdc05e7731..0000000000
--- a/modules/BrowserHistory.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import DOMHistory from './DOMHistory';
-import { addEventListener, removeEventListener, getWindowPath, supportsHistory } from './DOMUtils';
-
-/**
- * A history implementation for DOM environments that support the
- * HTML5 history API (pushState, replaceState, and the popstate event).
- * Provides the cleanest URLs and should always be used in browser
- * environments if possible.
- *
- * Note: BrowserHistory automatically falls back to using full page
- * refreshes if HTML5 history is not available, so URLs are always
- * the same across browsers.
- */
-class BrowserHistory extends DOMHistory {
-
- constructor(options) {
- super(options);
- this.handlePopState = this.handlePopState.bind(this);
- this.isSupported = supportsHistory();
- }
-
- setup() {
- if (this.location != null)
- return;
-
- var path = getWindowPath();
- var key = null;
- if (this.isSupported && window.history.state)
- key = window.history.state.key;
-
- super.setup(path, { key });
-
- addEventListener(window, 'popstate', this.handlePopState);
- }
-
- teardown() {
- removeEventListener(window, 'popstate', this.handlePopState);
- super.teardown();
- }
-
- handlePopState(event) {
- if (event.state === undefined)
- return; // Ignore extraneous popstate events in WebKit.
-
- var path = getWindowPath();
- var key = event.state && event.state.key;
- this.handlePop(path, { key });
- }
-
- // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-pushstate
- push(path, key) {
- if (this.isSupported) {
- var state = { key };
- window.history.pushState(state, '', path);
- return state;
- }
-
- window.location = path;
- }
-
- // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-replacestate
- replace(path, key) {
- if (this.isSupported) {
- var state = { key };
- window.history.replaceState(state, '', path);
- return state;
- }
- window.location.replace(path);
- }
-}
-
-export var history = new BrowserHistory;
-export default BrowserHistory;
diff --git a/modules/DOMHistory.js b/modules/DOMHistory.js
deleted file mode 100644
index 17c8851998..0000000000
--- a/modules/DOMHistory.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import History from './History';
-import { addEventListener, removeEventListener, getWindowScrollPosition } from './DOMUtils';
-import NavigationTypes from './NavigationTypes';
-
-/**
- * A history interface that assumes a DOM environment.
- */
-class DOMHistory extends History {
- constructor(options={}) {
- super(options);
- this.getScrollPosition = options.getScrollPosition || getWindowScrollPosition;
- this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
- }
-
- setup(path, entry) {
- super.setup(path, entry);
- addEventListener(window, 'beforeunload', this.handleBeforeUnload);
- }
-
- teardown() {
- removeEventListener(window, 'beforeunload', this.handleBeforeUnload);
- super.teardown();
- }
-
- go(n) {
- if (n === 0)
- return;
-
- window.history.go(n);
- }
-
- saveState(key, state) {
- window.sessionStorage.setItem(key, JSON.stringify(state));
- }
-
- readState(key) {
- var json = window.sessionStorage.getItem(key);
-
- if (json) {
- try {
- return JSON.parse(json);
- } catch (error) {
- // Ignore invalid JSON in session storage.
- }
- }
-
- return null;
- }
-
- beforeChange(location, done) {
- super.beforeChange(location, () => {
- if (location.navigationType === NavigationTypes.PUSH && this.canUpdateState()) {
- var scrollPosition = this.getScrollPosition();
- this.updateState(scrollPosition);
- }
- done();
- });
- }
-
- handleBeforeUnload(event) {
- var message = this.beforeChangeListener.call(this);
-
- if (message != null) {
- // cross browser, see https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
- (event || window.event).returnValue = message;
- return message;
- }
- }
-
-}
-
-export default DOMHistory;
diff --git a/modules/DOMUtils.js b/modules/DOMUtils.js
index 3952fa2712..e07aa4096e 100644
--- a/modules/DOMUtils.js
+++ b/modules/DOMUtils.js
@@ -36,8 +36,8 @@ export function getWindowPath() {
export function getWindowScrollPosition() {
return {
- scrollX: window.pageXOffset || document.documentElement.scrollLeft,
- scrollY: window.pageYOffset || document.documentElement.scrollTop
+ x: window.pageXOffset || document.documentElement.scrollLeft,
+ y: window.pageYOffset || document.documentElement.scrollTop
};
}
diff --git a/modules/HashHistory.js b/modules/HashHistory.js
deleted file mode 100644
index 70fdfd00ed..0000000000
--- a/modules/HashHistory.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import warning from 'warning';
-import DOMHistory from './DOMHistory';
-import { addEventListener, removeEventListener, getHashPath, replaceHashPath } from './DOMUtils';
-import { isAbsolutePath } from './URLUtils';
-
-var DefaultQueryKey = '_qk';
-
-function ensureSlash() {
- var path = getHashPath();
-
- if (isAbsolutePath(path))
- return true;
-
- replaceHashPath('/' + path);
-
- return false;
-}
-
-function addQueryStringValueToPath(path, key, value) {
- return path + (path.indexOf('?') === -1 ? '?' : '&') + `${key}=${value}`;
-}
-
-function getQueryStringValueFromPath(path, key) {
- var match = path.match(new RegExp(`\\?.*?\\b${key}=(.+?)\\b`));
- return match && match[1];
-}
-
-/**
- * A history implementation for DOM environments that uses window.location.hash
- * to store the current path. This is essentially a hack for older browsers that
- * do not support the HTML5 history API (IE <= 9).
- *
- * Support for persistence of state across page refreshes is provided using a
- * combination of a URL query string parameter and DOM storage. However, this
- * support is not enabled by default. In order to use it, create your own
- * HashHistory.
- *
- * import HashHistory from 'react-router/lib/HashHistory';
- * var StatefulHashHistory = new HashHistory({ queryKey: '_key' });
- * React.render(
, ...);
- */
-class HashHistory extends DOMHistory {
-
- constructor(options={}) {
- super(options);
- this.handleHashChange = this.handleHashChange.bind(this);
- this.queryKey = options.queryKey;
-
- if (typeof this.queryKey !== 'string')
- this.queryKey = this.queryKey ? DefaultQueryKey : null;
- }
-
- setup() {
- if (this.location != null)
- return;
-
- ensureSlash();
-
- var path = getHashPath();
- var key = getQueryStringValueFromPath(path, this.queryKey);
-
- super.setup(path, { key });
-
- addEventListener(window, 'hashchange', this.handleHashChange);
- }
-
- teardown() {
- removeEventListener(window, 'hashchange', this.handleHashChange);
- super.teardown();
- }
-
- handleHashChange() {
- if (!ensureSlash())
- return;
-
- if (this._ignoreNextHashChange) {
- this._ignoreNextHashChange = false;
- } else {
- var path = getHashPath();
- var key = getQueryStringValueFromPath(path, this.queryKey);
- this.handlePop(path, { key });
- }
- }
-
- push(path, key) {
- var actualPath = path;
- if (this.queryKey)
- actualPath = addQueryStringValueToPath(path, this.queryKey, key);
-
-
- if (actualPath === getHashPath()) {
- warning(
- false,
- 'HashHistory can not push the current path'
- );
- } else {
- this._ignoreNextHashChange = true;
- window.location.hash = actualPath;
- }
-
- return { key: this.queryKey && key };
- }
-
-
- replace(path, key) {
- var actualPath = path;
- if (this.queryKey)
- actualPath = addQueryStringValueToPath(path, this.queryKey, key);
-
-
- if (actualPath !== getHashPath())
- this._ignoreNextHashChange = true;
-
- replaceHashPath(actualPath);
-
- return { key: this.queryKey && key };
- }
-
- makeHref(path) {
- return '#' + path;
- }
-}
-
-export var history = new HashHistory;
-export default HashHistory;
diff --git a/modules/History.js b/modules/History.js
deleted file mode 100644
index 6c3d09de55..0000000000
--- a/modules/History.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import invariant from 'invariant';
-import warning from 'warning';
-import NavigationTypes from './NavigationTypes';
-import { getPathname, getQueryString, parseQueryString } from './URLUtils';
-import Location from './Location';
-
-var RequiredHistorySubclassMethods = [ 'push', 'replace', 'go' ];
-
-/**
- * A history interface that normalizes the differences across
- * various environments and implementations. Requires concrete
- * subclasses to implement the following methods:
- *
- * - pushState(state, path)
- * - replaceState(state, path)
- * - go(n)
- */
-class History {
-
- constructor(options={}) {
- RequiredHistorySubclassMethods.forEach(function (method) {
- invariant(
- typeof this[method] === 'function',
- '%s needs a "%s" method',
- this.constructor.name, method
- );
- }, this);
-
- this.parseQueryString = options.parseQueryString || parseQueryString;
-
- this.changeListeners = [];
- this.beforeChangeListener = null;
-
- this.path = null;
- this.location = null;
- this._pendingLocation = null;
- }
-
- _notifyChange() {
- for (var i = 0, len = this.changeListeners.length; i < len; ++i)
- this.changeListeners[i].call(this);
- }
-
- addChangeListener(listener) {
- this.changeListeners.push(listener);
- }
-
- removeChangeListener(listener) {
- this.changeListeners = this.changeListeners.filter(function (li) {
- return li !== listener;
- });
- }
-
- onBeforeChange(listener) {
- warning(
- this.beforeChangeListener != null,
- 'beforeChange listener of History should not be overwritten'
- );
-
- this.beforeChangeListener = listener;
- }
-
- setup(path, entry = {}) {
- if (this.location)
- return;
-
- if (!entry.key)
- entry = this.replace(path, this.createRandomKey());
-
- var state = null;
- if (typeof this.readState === 'function')
- state = this.readState(entry.key);
-
- var location = this._createLocation(path, state, entry, NavigationTypes.POP);
- this._update(path, location, false);
- }
-
- teardown() {
- this.changeListeners = [];
- this.beforeChangeListener = null;
-
- this.path = null;
- this.location = null;
- this._pendingLocation = null;
- }
-
- handlePop(path, entry={}) {
- var state = null;
- if (entry.key && typeof this.readState === 'function')
- state = this.readState(entry.key);
-
- var pendingLocation = this._createLocation(path, state, entry, NavigationTypes.POP);
-
- this.beforeChange(pendingLocation, () => {
- this._update(path, pendingLocation);
- });
- }
-
- createRandomKey() {
- return Math.random().toString(36).substr(2);
- }
-
- _saveNewState(state) {
- var key = this.createRandomKey();
-
- if (state != null) {
- invariant(
- typeof this.saveState === 'function',
- '%s needs a saveState method in order to store state',
- this.constructor.name
- );
-
- this.saveState(key, state);
- }
-
- return key;
- }
-
- canUpdateState() {
- return typeof this.readState === 'function'
- && typeof this.saveState === 'function'
- && this.location
- && this.location.state
- && this.location.state.key;
- }
-
- updateState(extraState) {
- invariant(
- this.canUpdateState(),
- '%s is unable to update state right now',
- this.constructor.name
- );
-
- var key = this.location.state.key;
- var state = this.readState(key);
- this.saveState(key, { ...state, ...extraState });
- }
-
- beforeChange(location, done) {
- if (!this.beforeChangeListener) {
- done();
- } else {
- this._pendingLocation = location;
-
- this.beforeChangeListener.call(this, location, () => {
- if (this._pendingLocation === location) {
- this._pendingLocation = null;
- done();
- return true;
- }
- return false;
- });
- }
- }
-
- isPending(location) {
- return this._pendingLocation === location;
- }
-
- pushState(state, path) {
- var pendingLocation = this._createLocation(path, state, null, NavigationTypes.PUSH);
- this.beforeChange(pendingLocation, () => {
- this._doPushState(state, path)
- });
- }
-
- _doPushState(state, path) {
- var key = this._saveNewState(state);
- var entry = null;
-
- if (this.path === path) {
- entry = this.replace(path, key) || {};
- } else {
- entry = this.push(path, key) || {};
- }
-
- warning(
- entry.key || state == null,
- '%s does not support storing state',
- this.constructor.name
- );
-
- var location = this._createLocation(path, state, entry, NavigationTypes.PUSH);
- this._update(path, location);
- }
-
- replaceState(state, path) {
- var pendingLocation = this._createLocation(path, state, null, NavigationTypes.REPLACE);
- this.beforeChange(pendingLocation, () => {
- this._doReplaceState(state, path);
- });
- }
-
- _doReplaceState(state, path) {
- var key = this._saveNewState(state);
- var entry = this.replace(path, key) || {};
-
- warning(
- entry.key || state == null,
- '%s does not support storing state',
- this.constructor.name
- );
-
- var location = this._createLocation(path, state, entry, NavigationTypes.REPLACE);
- this._update(path, location);
- }
-
- back() {
- this.go(-1);
- }
-
- forward() {
- this.go(1);
- }
-
- _update(path, location, notify=true) {
- this.path = path;
- this.location = location;
- this._pendingLocation = null;
-
- if (notify)
- this._notifyChange();
- }
-
- _createLocation(path, state, entry, navigationType) {
- var pathname = getPathname(path);
- var queryString = getQueryString(path);
- var query = queryString ? this.parseQueryString(queryString) : null;
- return new Location(pathname, query, {...state, ...entry}, navigationType);
- }
-
-}
-
-export default History;
diff --git a/modules/Link.js b/modules/Link.js
index 92784720a6..48fca93a38 100644
--- a/modules/Link.js
+++ b/modules/Link.js
@@ -1,6 +1,6 @@
-import React from 'react';
+import { createClass, createElement, PropTypes } from 'react';
-var { object, string, func } = React.PropTypes;
+var { object, string, func } = PropTypes;
function isLeftClickEvent(event) {
return event.button === 0;
@@ -26,9 +26,9 @@ function isModifiedEvent(event) {
* Links may pass along query string parameters
* using the `query` prop.
*
- *
+ *
*/
-export var Link = React.createClass({
+var Link = createClass({
contextTypes: {
router: object
@@ -72,23 +72,28 @@ export var Link = React.createClass({
render() {
var { router } = this.context;
- var { to, query } = this.props;
var props = Object.assign({}, this.props, {
- href: router.makeHref(to, query),
onClick: this.handleClick
});
- // ignore if rendered outside of the context of a router, simplifies unit testing
- if (router && router.isActive(to, query)) {
- if (props.activeClassName)
- props.className += props.className !== '' ? ` ${props.activeClassName}` : props.activeClassName;
+ // Ignore if rendered outside of the context of a
+ // router, simplifies unit testing.
+ if (router) {
+ var { to, query } = this.props;
- if (props.activeStyle)
- props.style = Object.assign({}, props.style, props.activeStyle);
+ props.href = router.createHref(to, query);
+
+ if (router.isActive(to, query)) {
+ if (props.activeClassName)
+ props.className += props.className !== '' ? ` ${props.activeClassName}` : props.activeClassName;
+
+ if (props.activeStyle)
+ props.style = Object.assign({}, props.style, props.activeStyle);
+ }
}
- return React.createElement('a', props);
+ return createElement('a', props);
}
});
diff --git a/modules/Location.js b/modules/Location.js
deleted file mode 100644
index 0210b60c96..0000000000
--- a/modules/Location.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import NavigationTypes from './NavigationTypes';
-
-/**
- * A Location answers two important questions:
- *
- * 1. Where am I?
- * 2. How did I get here?
- */
-class Location {
-
- static isLocation(object) {
- return object instanceof Location;
- }
-
- constructor(pathname='/', query=null, state=null, navigationType=NavigationTypes.POP) {
- this.pathname = pathname;
- this.query = query;
- this.state = state;
- this.navigationType = navigationType;
- }
-
-}
-
-export default Location;
diff --git a/modules/MemoryHistory.js b/modules/MemoryHistory.js
deleted file mode 100644
index e6d87373fb..0000000000
--- a/modules/MemoryHistory.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import invariant from 'invariant';
-import History from './History';
-
-/**
- * A concrete History class that doesn't require a DOM. Ideal
- * for testing because it allows you to specify route history
- * entries in the constructor.
- */
-class MemoryHistory extends History {
-
- constructor(entries, current) {
- super();
-
- if (entries == null) {
- entries = [ '/' ];
- } else if (typeof entries === 'string') {
- entries = [ entries ];
- } else if (!Array.isArray(entries)) {
- throw new Error('MemoryHistory needs an array of entries');
- }
-
- entries = entries.map(this._createEntry.bind(this));
-
- if (current == null) {
- current = entries.length - 1;
- } else {
- invariant(
- current >= 0 && current < entries.length,
- '%s current index must be >= 0 and < %s, was %s',
- this.constructor.name, entries.length, current
- );
- }
-
- this.current = current;
- this.entries = entries;
- this.storage = entries
- .filter(entry => entry.state)
- .reduce((all, entry) => {
- all[entry.key] = entry.state;
- return all;
- }, {});
- }
-
- setup() {
- if (this.location)
- return;
-
- var entry = this.entries[this.current];
- var path = entry.path;
- var key = entry.key;
-
- super.setup(path, { key, current: this.current });
- }
-
- _createEntry(object) {
- var key = this.createRandomKey();
- if (typeof object === 'string')
- return { path: object, key };
-
- if (typeof object === 'object' && object)
- return {...object, key};
-
- throw new Error('Unable to create history entry from ' + object);
- }
-
- // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-pushstate
- push(path, key) {
- this.current += 1;
- this.entries = this.entries.slice(0, this.current).concat([{ key, path }]);
-
- return { key, current: this.current };
- }
-
- // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-replacestate
- replace(path, key) {
- this.entries[this.current] = { key, path };
-
- return { key, current: this.current };
- }
-
- readState(key) {
- return this.storage[key];
- }
-
- saveState(key, state){
- this.storage[key] = state;
- }
-
- go(n) {
- if (n === 0)
- return;
-
- invariant(
- this.canGo(n),
- '%s cannot go(%s) because there is not enough history',
- this.constructor.name, n
- );
-
- this.current += n;
- var entry = this.entries[this.current];
-
- this.handlePop(entry.path, { key: entry.key, current: this.current });
- }
-
- canGo(n) {
- var index = this.current + n;
- return index >= 0 && index < this.entries.length;
- }
-
- canGoBack() {
- return this.canGo(-1);
- }
-
- canGoForward() {
- return this.canGo(1);
- }
-
-}
-
-export default MemoryHistory;
diff --git a/modules/NavigationMixin.js b/modules/NavigationMixin.js
new file mode 100644
index 0000000000..71d2faea30
--- /dev/null
+++ b/modules/NavigationMixin.js
@@ -0,0 +1,79 @@
+import invariant from 'invariant';
+
+var NavigationMixin = {
+
+ /**
+ * Returns a string that may safely be used to link to the given
+ * pathname and query.
+ */
+ createHref(pathname, query) {
+ var path = this.createPath(pathname, query);
+ var { history } = this.props;
+
+ if (history && history.createHref)
+ return history.createHref(path);
+
+ return path;
+ },
+
+ /**
+ * Pushes a new Location onto the history stack.
+ */
+ transitionTo(pathname, query, state=null) {
+ var { history } = this.props;
+
+ invariant(
+ history,
+ 'Router#transitionTo needs history'
+ );
+
+ history.pushState(state, this.createPath(pathname, query));
+ },
+
+ /**
+ * Replaces the current Location on the history stack.
+ */
+ replaceWith(pathname, query, state=null) {
+ var { history } = this.props;
+
+ invariant(
+ history,
+ 'Router#replaceWith needs history'
+ );
+
+ history.replaceState(state, this.createPath(pathname, query));
+ },
+
+ /**
+ * Navigates forward/backward n entries in the history stack.
+ */
+ go(n) {
+ var { history } = this.props;
+
+ invariant(
+ history,
+ 'Router#go needs history'
+ );
+
+ history.go(n);
+ },
+
+ /**
+ * Navigates back one entry in the history stack. This is identical to
+ * the user clicking the browser's back button.
+ */
+ goBack() {
+ this.go(-1);
+ },
+
+ /**
+ * Navigates forward one entry in the history stack. This is identical to
+ * the user clicking the browser's forward button.
+ */
+ goForward() {
+ this.go(1);
+ }
+
+};
+
+export default NavigationMixin;
diff --git a/modules/PropTypes.js b/modules/PropTypes.js
index 0ae84959e2..87ed815b11 100644
--- a/modules/PropTypes.js
+++ b/modules/PropTypes.js
@@ -1,27 +1,28 @@
-import React from 'react';
-import Location from './Location';
-import History from './History';
+import { PropTypes } from 'react';
-var { func, object, arrayOf, instanceOf, oneOfType, element } = React.PropTypes;
+var { func, object, arrayOf, oneOfType, element, shape, string } = PropTypes;
-function falsy(props, propName, componentName) {
+export function falsy(props, propName, componentName) {
if (props[propName])
return new Error(`<${componentName}> should not have a "${propName}" prop`);
}
-var component = func;
-var components = oneOfType([ component, object ]);
-var history = instanceOf(History);
-var location = instanceOf(Location);
-var route = oneOfType([ object, element ]);
-var routes = oneOfType([ route, arrayOf(route) ]);
+export var history = shape({
+ listen: func.isRequired,
+ pushState: func.isRequired,
+ replaceState: func.isRequired,
+ go: func.isRequired
+});
-module.exports = {
- falsy,
- component,
- components,
- history,
- location,
- route,
- routes
-};
+export var location = shape({
+ pathname: string.isRequired,
+ search: string.isRequired,
+ state: object,
+ action: string.isRequired,
+ key: string
+});
+
+export var component = func;
+export var components = oneOfType([ component, object ]);
+export var route = oneOfType([ object, element ]);
+export var routes = oneOfType([ route, arrayOf(route) ]);
diff --git a/modules/Redirect.js b/modules/Redirect.js
index 3eacc970ec..9edfe3bfb9 100644
--- a/modules/Redirect.js
+++ b/modules/Redirect.js
@@ -1,12 +1,19 @@
-import React from 'react';
import invariant from 'invariant';
+import { createClass, PropTypes } from 'react';
import { createRouteFromReactElement } from './RouteUtils';
-import { formatPattern } from './URLUtils';
+import { formatPattern } from './PatternUtils';
import { falsy } from './PropTypes';
-var { string, object } = React.PropTypes;
+var { string, object } = PropTypes;
-export var Redirect = React.createClass({
+/**
+ * A
is used to declare another URL path a client should be sent
+ * to when they request a given URL.
+ *
+ * Redirects are placed alongside routes in the route configuration and are
+ * traversed in the same manner.
+ */
+var Redirect = createClass({
statics: {
@@ -16,11 +23,13 @@ export var Redirect = React.createClass({
if (route.from)
route.path = route.from;
- route.onEnter = function (nextState, transition) {
+ route.onEnter = function (nextState, redirectTo) {
var { location, params } = nextState;
+
+ // TODO: Handle relative pathnames.
var pathname = route.to ? formatPattern(route.to, params) : location.pathname;
- transition.to(
+ redirectTo(
pathname,
route.query || location.query,
route.state || location.state
diff --git a/modules/Route.js b/modules/Route.js
index 7f8ca13547..fb95c2bcb4 100644
--- a/modules/Route.js
+++ b/modules/Route.js
@@ -1,10 +1,10 @@
-import React from 'react';
+import warning from 'warning';
import invariant from 'invariant';
+import { createClass, PropTypes } from 'react';
import { createRouteFromReactElement } from './RouteUtils';
import { component, components } from './PropTypes';
-import warning from 'warning';
-var { string, bool, func } = React.PropTypes;
+var { string, bool, func } = PropTypes;
/**
* A is used to declare which components are rendered to the page when
@@ -16,7 +16,7 @@ var { string, bool, func } = React.PropTypes;
* "active" and their components are rendered into the DOM, nested in the same
* order as they are in the tree.
*/
-export var Route = React.createClass({
+var Route = createClass({
statics: {
diff --git a/modules/Router.js b/modules/Router.js
index 7b73e9742c..e00166df11 100644
--- a/modules/Router.js
+++ b/modules/Router.js
@@ -1,105 +1,93 @@
-import React, { createElement, isValidElement } from 'react';
import warning from 'warning';
import invariant from 'invariant';
-import { loopAsync } from './AsyncUtils';
+import { createClass, createElement, isValidElement, PropTypes } from 'react';
+import { component, components, history, location, routes } from './PropTypes';
import { createRoutes } from './RouteUtils';
-import { getState, getTransitionHooks, getComponents, getRouteParams, createTransitionHook } from './RoutingUtils';
-import { routes, component, components, history, location } from './PropTypes';
-import RouterContextMixin from './RouterContextMixin';
+import matchRoutes from './matchRoutes';
+import runTransitionHooks from './runTransitionHooks';
+import getComponents from './getComponents';
+import getRouteParams from './getRouteParams';
+
+import NavigationMixin from './NavigationMixin';
import ScrollManagementMixin from './ScrollManagementMixin';
-import { isLocation } from './Location';
-import Transition from './Transition';
+import ActiveMixin from './ActiveMixin';
-var { arrayOf, func, object } = React.PropTypes;
+var { arrayOf, func, object } = PropTypes;
-function runTransition(prevState, routes, location, hooks, callback) {
- var transition = new Transition;
+import qs from 'qs';
- getState(routes, location, function (error, nextState) {
- if (error || nextState == null || transition.isCancelled) {
- callback(error, null, transition);
- } else {
- nextState.location = location;
-
- var transitionHooks = getTransitionHooks(prevState, nextState);
- if (Array.isArray(hooks))
- transitionHooks.unshift.apply(transitionHooks, hooks);
-
- loopAsync(transitionHooks.length, (index, next, done) => {
- transitionHooks[index](nextState, transition, (error) => {
- if (error || transition.isCancelled) {
- done(error); // No need to continue.
- } else {
- next();
- }
- });
- }, function (error) {
- if (error || transition.isCancelled) {
- callback(error, null, transition);
+function stringifyQuery(query) {
+ return qs.stringify(query, { arrayFormat: 'brackets' });
+}
+
+function parseQueryString(queryString) {
+ return qs.parse(queryString);
+}
+
+var Router = createClass({
+
+ mixins: [ NavigationMixin, ScrollManagementMixin, ActiveMixin ],
+
+ statics: {
+
+ run(routes, location, callback, prevState=null) {
+ matchRoutes(routes, location, function (error, nextState) {
+ if (error || nextState == null) {
+ callback(error, null);
} else {
- getComponents(nextState.branch, function (error, components) {
- if (error || transition.isCancelled) {
- callback(error, null, transition);
+ nextState.location = location;
+ runTransitionHooks(prevState, nextState, function (error, redirectInfo) {
+ if (error || redirectInfo) {
+ callback(error, null, redirectInfo);
} else {
- nextState.components = components;
- callback(null, nextState, transition);
+ getComponents(nextState, function (error, components) {
+ if (error) {
+ callback(error);
+ } else {
+ nextState.components = components;
+ callback(null, nextState);
+ }
+ });
}
});
}
});
}
- });
-}
-
-var Router = React.createClass({
-
- mixins: [ RouterContextMixin, ScrollManagementMixin ],
-
- statics: {
-
- /**
- * Runs a transition to the given location using the given routes and
- * transition hooks (optional) and calls callback(error, state, transition)
- * when finished. This is primarily useful for server-side rendering.
- */
- run(routes, location, transitionHooks, callback) {
- if (typeof transitionHooks === 'function') {
- callback = transitionHooks;
- transitionHooks = null;
- }
- invariant(
- typeof callback === 'function',
- 'Router.run needs a callback'
- );
+ },
- runTransition(null, routes, location, transitionHooks, callback);
- }
+ childContextTypes: {
+ router: object
+ },
+ getChildContext() {
+ return {
+ router: this
+ };
},
propTypes: {
- createElement: func.isRequired,
- onAbort: func,
+ createElement: func,
+ parseQueryString: func,
+ stringifyQuery: func,
onError: func,
onUpdate: func,
-
- // Client-side
- history,
routes,
// Routes may also be given as children (JSX)
children: routes,
+ // Client-side
+ history,
+
// Server-side
- location,
- branch: routes,
- params: object,
- components: arrayOf(components)
+ location
},
getDefaultProps() {
return {
- createElement
+ createElement,
+ parseQueryString,
+ stringifyQuery
};
},
@@ -107,78 +95,50 @@ var Router = React.createClass({
return {
isTransitioning: false,
location: null,
- branch: null,
+ routes: null,
params: null,
components: null
};
},
- _updateState(location) {
- invariant(
- isLocation(location),
- 'A needs a valid Location'
- );
-
- var hooks = this.transitionHooks;
- if (hooks)
- hooks = hooks.map(hook => createTransitionHook(hook, this));
+ updateLocation(location) {
+ if (!location.query)
+ location.query = this.props.parseQueryString(location.search.substring(1));
- this.setState({ isTransitioning: true });
+ this.setState({
+ isTransitioning: true
+ });
- runTransition(this.state, this.routes, location, hooks, (error, state, transition) => {
+ Router.run(this.routes, location, (error, state, redirectInfo) => {
if (error) {
this.handleError(error);
- } else if (transition.isCancelled) {
- if (transition.redirectInfo) {
- var { pathname, query, state } = transition.redirectInfo;
- this.replaceWith(pathname, query, state);
- } else {
- invariant(
- this.state.location,
- 'You may not abort the initial transition'
- );
-
- this.handleAbort(transition.abortReason);
- }
+ } else if (redirectInfo) {
+ var { pathname, query, state } = redirectInfo;
+ this.replaceWith(pathname, query, state);
} else if (state == null) {
- warning(false, 'Location "%s" did not match any routes', location.pathname);
+ warning(
+ false,
+ 'Location "%s" did not match any routes',
+ location.pathname + location.search
+ );
} else {
this.setState(state, this.props.onUpdate);
}
- this.setState({ isTransitioning: false });
- });
- },
-
- /**
- * Adds a transition hook that runs before all route hooks in a
- * transition. The signature is the same as route transition hooks.
- */
- addTransitionHook(hook) {
- if (!this.transitionHooks)
- this.transitionHooks = [];
-
- this.transitionHooks.push(hook);
- },
-
- /**
- * Removes the given transition hook.
- */
- removeTransitionHook(hook) {
- if (this.transitionHooks)
- this.transitionHooks = this.transitionHooks.filter(h => h !== hook);
+ this.setState({
+ isTransitioning: false
+ });
+ }, this.state);
},
- handleAbort(reason) {
- if (this.props.onAbort) {
- this.props.onAbort.call(this, reason);
- } else {
- // The best we can do here is goBack so the location state reverts
- // to what it was. However, we also set a flag so that we know not
- // to run through _updateState again since state did not change.
- this._ignoreNextHistoryChange = true;
- this.goBack();
+ updateHistory(history) {
+ if (this._unlisten) {
+ this._unlisten();
+ this._unlisten = null;
}
+
+ if (history)
+ this._unlisten = history.listen(this.updateLocation);
},
handleError(error) {
@@ -190,79 +150,49 @@ var Router = React.createClass({
}
},
- handleHistoryChange() {
- if (this._ignoreNextHistoryChange) {
- this._ignoreNextHistoryChange = false;
- } else {
- this._updateState(this.props.history.location);
- }
- },
-
componentWillMount() {
- var { history, routes, children, location, branch, params, components } = this.props;
-
- if (history) {
- invariant(
- routes || children,
- 'Client-side s need routes. Try using or ' +
- 'passing your routes as nested children'
- );
+ var { routes, children, history, location } = this.props;
- this.routes = createRoutes(routes || children);
-
- if (typeof history.setup === 'function')
- history.setup();
-
- // We need to listen first in case we redirect immediately.
- if (history.addChangeListener)
- history.addChangeListener(this.handleHistoryChange);
+ invariant(
+ routes || children,
+ 's need routes. Try using or ' +
+ 'passing your routes as nested children'
+ );
- this._updateState(history.location);
- } else {
- invariant(
- location && branch && params && components,
- 'Server-side s need location, branch, params, and components ' +
- 'props. Try using Router.run to get all the props you need'
- );
+ this.routes = createRoutes(routes || children);
- this.setState({ location, branch, params, components });
+ if (history) {
+ this.updateHistory(history);
+ } else if (location) {
+ this.updateLocation(location);
}
},
componentWillReceiveProps(nextProps) {
- invariant(
- this.props.history === nextProps.history,
- ' may not be changed'
- );
-
- if (nextProps.history) {
- var currentRoutes = this.props.routes || this.props.children;
- var nextRoutes = nextProps.routes || nextProps.children;
-
- if (currentRoutes !== nextRoutes) {
- this.routes = createRoutes(nextRoutes);
-
- // Call this here because _updateState
- // uses this.routes to determine state.
- if (nextProps.history.location)
- this._updateState(nextProps.history.location);
- }
- }
+ // TODO
},
componentWillUnmount() {
- var { history } = this.props;
+ if (this._unlisten)
+ this._unlisten();
+ },
+
+ createPath(pathname, query) {
+ var { stringifyQuery } = this.props;
+
+ var queryString;
+ if (query == null || (queryString = stringifyQuery(query)) === '')
+ return pathname;
- if (history && history.removeChangeListener)
- history.removeChangeListener(this.handleHistoryChange);
+ return pathname + (pathname.indexOf('?') === -1 ? '?' : '&') + queryString;
},
- _createElement(component, props) {
+ createElement(component, props) {
return typeof component === 'function' ? this.props.createElement(component, props) : null;
},
render() {
- var { branch, params, components } = this.state;
+ var { routes, params, components } = this.state;
var element = null;
if (components) {
@@ -270,7 +200,7 @@ var Router = React.createClass({
if (components == null)
return element; // Don't create new children; use the grandchildren.
- var route = branch[index];
+ var route = routes[index];
var routeParams = getRouteParams(route, params);
var props = Object.assign({}, this.state, { route, routeParams });
@@ -286,12 +216,12 @@ var Router = React.createClass({
for (var key in components)
if (components.hasOwnProperty(key))
- elements[key] = this._createElement(components[key], props);
+ elements[key] = this.createElement(components[key], props);
return elements;
}
- return this._createElement(components, props);
+ return this.createElement(components, props);
}, element);
}
diff --git a/modules/RouterContextMixin.js b/modules/RouterContextMixin.js
deleted file mode 100644
index e2e35d7e64..0000000000
--- a/modules/RouterContextMixin.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import React from 'react';
-import invariant from 'invariant';
-import { stripLeadingSlashes, stringifyQuery } from './URLUtils';
-
-var { func, object } = React.PropTypes;
-
-function pathnameIsActive(pathname, activePathname) {
- if (stripLeadingSlashes(activePathname).indexOf(stripLeadingSlashes(pathname)) === 0)
- return true; // This quick comparison satisfies most use cases.
-
- // TODO: Implement a more stringent comparison that checks
- // to see if the pathname matches any routes (and params)
- // in the currently active branch.
-
- return false;
-}
-
-function queryIsActive(query, activeQuery) {
- if (activeQuery == null)
- return query == null;
-
- if (query == null)
- return true;
-
- for (var p in query)
- if (query.hasOwnProperty(p) && String(query[p]) !== String(activeQuery[p]))
- return false;
-
- return true;
-}
-
-var RouterContextMixin = {
-
- propTypes: {
- stringifyQuery: func.isRequired
- },
-
- getDefaultProps() {
- return {
- stringifyQuery
- };
- },
-
- childContextTypes: {
- router: object.isRequired
- },
-
- getChildContext() {
- return {
- router: this
- };
- },
-
- /**
- * Returns a full URL path from the given pathname and query.
- */
- makePath(pathname, query) {
- if (query) {
- if (typeof query !== 'string')
- query = this.props.stringifyQuery(query);
-
- if (query !== '')
- return pathname + '?' + query;
- }
-
- return pathname;
- },
-
- /**
- * Returns a string that may safely be used to link to the given
- * pathname and query.
- */
- makeHref(pathname, query) {
- var path = this.makePath(pathname, query);
- var { history } = this.props;
-
- if (history && history.makeHref)
- return history.makeHref(path);
-
- return path;
- },
-
- /**
- * Pushes a new Location onto the history stack.
- */
- transitionTo(pathname, query, state=null) {
- var { history } = this.props;
-
- invariant(
- history,
- 'Router#transitionTo is client-side only (needs history)'
- );
-
- history.pushState(state, this.makePath(pathname, query));
- },
-
- /**
- * Replaces the current Location on the history stack.
- */
- replaceWith(pathname, query, state=null) {
- var { history } = this.props;
-
- invariant(
- history,
- 'Router#replaceWith is client-side only (needs history)'
- );
-
- history.replaceState(state, this.makePath(pathname, query));
- },
-
- /**
- * Navigates forward/backward n entries in the history stack.
- */
- go(n) {
- var { history } = this.props;
-
- invariant(
- history,
- 'Router#go is client-side only (needs history)'
- );
-
- history.go(n);
- },
-
- /**
- * Navigates back one entry in the history stack. This is identical to
- * the user clicking the browser's back button.
- */
- goBack() {
- this.go(-1);
- },
-
- /**
- * Navigates forward one entry in the history stack. This is identical to
- * the user clicking the browser's forward button.
- */
- goForward() {
- this.go(1);
- },
-
- /**
- * Returns true if a to the given pathname/query combination is
- * currently active.
- */
- isActive(pathname, query) {
- var { location } = this.state;
-
- if (location == null)
- return false;
-
- return pathnameIsActive(pathname, location.pathname) &&
- queryIsActive(query, location.query);
- }
-
-};
-
-export default RouterContextMixin;
diff --git a/modules/RoutingUtils.js b/modules/RoutingUtils.js
deleted file mode 100644
index c9ab9ea8ec..0000000000
--- a/modules/RoutingUtils.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import invariant from 'invariant';
-import { createRoutes } from './RouteUtils';
-import { getParamNames, matchPattern, stripLeadingSlashes } from './URLUtils';
-import { loopAsync, mapAsync } from './AsyncUtils';
-
-function getChildRoutes(route, locationState, callback) {
- if (route.childRoutes) {
- callback(null, route.childRoutes);
- } else if (route.getChildRoutes) {
- route.getChildRoutes(locationState, callback);
- } else {
- callback();
- }
-}
-
-function getIndexRoute(route, locationState, callback) {
- if (route.indexRoute) {
- callback(null, route.indexRoute);
- } else if (route.getIndexRoute) {
- route.getIndexRoute(callback, locationState);
- } else {
- callback();
- }
-}
-
-function assignParams(params, paramNames, paramValues) {
- return paramNames.reduceRight(function (params, paramName, index) {
- var paramValue = paramValues[index];
-
- if (Array.isArray(params[paramName])) {
- params[paramName].unshift(paramValue);
- } else if (paramName in params) {
- params[paramName] = [ paramValue, params[paramName] ];
- } else {
- params[paramName] = paramValue;
- }
-
- return params;
- }, params);
-}
-
-function createParams(paramNames, paramValues) {
- return assignParams({}, paramNames, paramValues);
-}
-
-function matchRouteDeep(route, pathname, locationState, callback) {
- var { remainingPathname, paramNames, paramValues } = matchPattern(route.path, pathname);
- var isExactMatch = remainingPathname === '';
-
- if (isExactMatch && route.path) {
- var params = createParams(paramNames, paramValues);
- var branch = [ route ];
-
- getIndexRoute(route, locationState, function (error, indexRoute) {
- if (error) {
- callback(error);
- } else {
- if (indexRoute)
- branch.push(indexRoute);
-
- callback(null, { params, branch });
- }
- });
- } else if (remainingPathname != null) {
- // This route matched at least some of the path.
- getChildRoutes(route, locationState, function (error, childRoutes) {
- if (error) {
- callback(error);
- } else if (childRoutes) {
- // Check the child routes to see if any of them match.
- matchRoutes(childRoutes, remainingPathname, locationState, function (error, match) {
- if (error) {
- callback(error);
- } else if (match) {
- // A child route matched! Augment the match and pass it up the stack.
- assignParams(match.params, paramNames, paramValues);
- match.branch.unshift(route);
- callback(null, match);
- } else {
- callback();
- }
- });
- } else {
- callback();
- }
- });
- } else {
- callback();
- }
-}
-
-function matchRoutes(routes, pathname, locationState, callback) {
- routes = createRoutes(routes);
-
- loopAsync(routes.length, function (index, next, done) {
- matchRouteDeep(routes[index], pathname, locationState, function (error, match) {
- if (error || match) {
- done(error, match);
- } else {
- next();
- }
- });
- }, callback);
-}
-
-/**
- * Asynchronously matches the given location to a set of routes and calls
- * callback(error, state) when finished. The state object may have the
- * following properties:
- *
- * - branch An array of routes that matched, in hierarchical order
- * - params An object of URL parameters
- *
- * Note: This operation may return synchronously if no routes have an
- * asynchronous getChildRoutes method.
- */
-export function getState(routes, location, callback) {
- matchRoutes(routes, stripLeadingSlashes(location.pathname), location.state, callback);
-}
-
-function routeParamsChanged(route, prevState, nextState) {
- if (!route.path)
- return false;
-
- var paramNames = getParamNames(route.path);
-
- return paramNames.some(function (paramName) {
- return prevState.params[paramName] !== nextState.params[paramName];
- });
-}
-
-/**
- * Runs a diff on the two router states and returns an array of two
- * arrays: 1) the routes that we are leaving, starting with the leaf
- * route and 2) the routes that we are entering, ending with the leaf
- * route.
- */
-function computeDiff(prevState, nextState) {
- var fromRoutes = prevState && prevState.branch;
- var toRoutes = nextState.branch;
-
- var leavingRoutes, enteringRoutes;
- if (fromRoutes) {
- leavingRoutes = fromRoutes.filter(function (route) {
- return toRoutes.indexOf(route) === -1 || routeParamsChanged(route, prevState, nextState);
- });
-
- // onLeave hooks start at the leaf route.
- leavingRoutes.reverse();
-
- enteringRoutes = toRoutes.filter(function (route) {
- return fromRoutes.indexOf(route) === -1 || leavingRoutes.indexOf(route) !== -1;
- });
- } else {
- leavingRoutes = [];
- enteringRoutes = toRoutes;
- }
-
- return [
- leavingRoutes,
- enteringRoutes
- ];
-}
-
-export function createTransitionHook(fn, context) {
- return function (nextState, transition, callback) {
- if (fn.length > 2) {
- fn.call(context, nextState, transition, callback);
- } else {
- // Assume fn executes synchronously and
- // automatically call the callback for them.
- fn.call(context, nextState, transition);
- callback();
- }
- };
-}
-
-function getTransitionHooksFromRoutes(routes, hookName) {
- return routes.reduce(function (hooks, route) {
- if (route[hookName])
- hooks.push(createTransitionHook(route[hookName], route));
-
- return hooks;
- }, []);
-}
-
-/**
- * Compiles and returns an array of transition hook functions that
- * should be called before we transition to a new state. Transition
- * hook signatures are:
- *
- * - route.onLeave(nextState, transition[, callback ])
- * - route.onEnter(nextState, transition[, callback ])
- *
- * Transition hooks run in order from the leaf route in the branch
- * we're leaving, up the tree to the common parent route, and back
- * down the branch we're entering to the leaf route.
- *
- * If a transition hook needs to execute asynchronously it may have
- * a 3rd argument that it should call when it is finished. Otherwise
- * the transition executes synchronously.
- */
-export function getTransitionHooks(prevState, nextState) {
- var [ leavingRoutes, enteringRoutes ] = computeDiff(prevState, nextState);
- var hooks = getTransitionHooksFromRoutes(leavingRoutes, 'onLeave');
-
- hooks.push.apply(
- hooks,
- getTransitionHooksFromRoutes(enteringRoutes, 'onEnter')
- );
-
- return hooks;
-}
-
-function getComponentsForRoute(route, callback) {
- if (route.component || route.components) {
- callback(null, route.component || route.components);
- } else if (route.getComponents) {
- route.getComponents(callback);
- } else {
- callback();
- }
-}
-
-/**
- * Asynchronously fetches all components needed for the given router
- * state and calls callback(error, components) when finished.
- *
- * Note: This operation may return synchronously if no routes have an
- * asynchronous getComponents method.
- */
-export function getComponents(routes, callback) {
- mapAsync(routes, function (route, index, callback) {
- getComponentsForRoute(route, callback);
- }, callback);
-}
-
-/**
- * Extracts an object of params the given route cares about from
- * the given params object.
- */
-export function getRouteParams(route, params) {
- var routeParams = {};
-
- if (!route.path)
- return routeParams;
-
- var paramNames = getParamNames(route.path);
-
- for (var p in params)
- if (params.hasOwnProperty(p) && paramNames.indexOf(p) !== -1)
- routeParams[p] = params[p];
-
- return routeParams;
-}
diff --git a/modules/ScrollManagementMixin.js b/modules/ScrollManagementMixin.js
index fc5ee5e8f3..bb2539ab85 100644
--- a/modules/ScrollManagementMixin.js
+++ b/modules/ScrollManagementMixin.js
@@ -1,16 +1,16 @@
import React from 'react';
+import { Actions } from 'history';
import { canUseDOM, setWindowScrollPosition } from './DOMUtils';
-import NavigationTypes from './NavigationTypes';
var { func } = React.PropTypes;
-function getCommonAncestors(branch, otherBranch) {
- return branch.filter(route => otherBranch.indexOf(route) !== -1);
+function getCommonAncestors(routes, otherRoutes) {
+ return routes.filter(route => otherRoutes.indexOf(route) !== -1);
}
function shouldUpdateScrollPosition(state, prevState) {
- var { location, branch } = state;
- var { location: prevLocation, branch: prevBranch } = prevState;
+ var { location, routes } = state;
+ var { location: prevLocation, routes: prevRoutes } = prevState;
// When an onEnter hook uses transition.to to redirect
// on the initial load prevLocation is null, so assume
@@ -24,16 +24,16 @@ function shouldUpdateScrollPosition(state, prevState) {
// Don't update scroll position if any of the ancestors
// has `ignoreScrollPosition` set to `true` on the route.
- var sharedAncestors = getCommonAncestors(branch, prevBranch);
+ var sharedAncestors = getCommonAncestors(routes, prevRoutes);
if (sharedAncestors.some(route => route.ignoreScrollBehavior))
return false;
return true;
}
-function updateWindowScrollPosition(navigationType, scrollX, scrollY) {
+function updateWindowScrollPosition(action, scrollX, scrollY) {
if (canUseDOM) {
- if (navigationType === NavigationTypes.POP) {
+ if (action === Actions.POP) {
setWindowScrollPosition(scrollX, scrollY);
} else {
setWindowScrollPosition(0, 0);
@@ -57,11 +57,10 @@ var ScrollManagementMixin = {
componentDidUpdate(prevProps, prevState) {
var { location } = this.state;
- var locationState = location && location.state;
- if (locationState && this.props.shouldUpdateScrollPosition(this.state, prevState)) {
- var { scrollX, scrollY } = locationState;
- this.props.updateScrollPosition(location.navigationType, scrollX || 0, scrollY || 0);
+ if (location && this.props.shouldUpdateScrollPosition(this.state, prevState)) {
+ var { action, scrollX, scrollY } = location;
+ this.props.updateScrollPosition(action, scrollX || 0, scrollY || 0);
}
}
diff --git a/modules/Transition.js b/modules/Transition.js
deleted file mode 100644
index 1b5fa89fd6..0000000000
--- a/modules/Transition.js
+++ /dev/null
@@ -1,21 +0,0 @@
-class Transition {
-
- constructor() {
- this.isCancelled = false;
- this.redirectInfo = null;
- this.abortReason = null;
- }
-
- to(pathname, query, state) {
- this.redirectInfo = { pathname, query, state };
- this.isCancelled = true;
- }
-
- abort(reason) {
- this.abortReason = reason;
- this.isCancelled = true;
- }
-
-}
-
-export default Transition;
diff --git a/modules/TransitionHook.js b/modules/TransitionHook.js
deleted file mode 100644
index a53f1113d4..0000000000
--- a/modules/TransitionHook.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import warning from 'warning';
-
-var { object } = React.PropTypes;
-
-var TransitionHook = {
-
- contextTypes: {
- router: object.isRequired
- },
-
- componentDidMount() {
- warning(
- typeof this.routerWillLeave === 'function',
- 'Components that mixin TransitionHook should have a routerWillLeave method, check %s',
- this.constructor.displayName || this.constructor.name
- );
-
- if (this.routerWillLeave)
- this.context.router.addTransitionHook(this.routerWillLeave);
- },
-
- componentWillUnmount() {
- if (this.routerWillLeave)
- this.context.router.removeTransitionHook(this.routerWillLeave);
- }
-
-};
-
-export default TransitionHook;
diff --git a/modules/__tests__/ActiveMixin-test.js b/modules/__tests__/ActiveMixin-test.js
new file mode 100644
index 0000000000..729823aad8
--- /dev/null
+++ b/modules/__tests__/ActiveMixin-test.js
@@ -0,0 +1,120 @@
+import expect from 'expect';
+import React from 'react';
+import createLocation from 'history/lib/createLocation';
+import createHistory from 'history/lib/createMemoryHistory';
+import Router from '../Router';
+import Route from '../Route';
+
+describe('ActiveMixin', function () {
+
+ var node;
+ beforeEach(function () {
+ node = document.createElement('div');
+ });
+
+ afterEach(function () {
+ React.unmountComponentAtNode(node);
+ });
+
+ describe('a pathname that matches the URL', function () {
+ describe('with no query', function () {
+ it('is active', function (done) {
+ React.render((
+
+
+
+ ), node, function () {
+ expect(this.isActive('/home')).toBe(true);
+ done();
+ });
+ });
+ });
+
+ describe('with a query that also matches', function () {
+ it('is active', function (done) {
+ React.render((
+
+
+
+ ), node, function () {
+ expect(this.isActive('/home', { the: 'query' })).toBe(true);
+ done();
+ });
+ });
+ });
+
+ describe('with a query that does not match', function () {
+ it('is not active', function (done) {
+ React.render((
+
+
+
+ ), node, function () {
+ expect(this.isActive('/home', { something: 'else' })).toBe(false);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('a pathname that matches a parent route, but not the URL directly', function () {
+ describe('with no query', function () {
+ it('is active', function (done) {
+ React.render((
+
+
+
+
+
+ ), node, function () {
+ expect(this.isActive('/home')).toBe(true);
+ done();
+ });
+ });
+ });
+
+ describe('with a query that also matches', function () {
+ it('is active', function (done) {
+ React.render((
+
+
+
+
+
+ ), node, function () {
+ expect(this.isActive('/home', { the: 'query' })).toBe(true);
+ done();
+ });
+ });
+ });
+
+ describe('with a query that does not match', function () {
+ it('is active', function (done) {
+ React.render((
+
+
+
+
+
+ ), node, function () {
+ expect(this.isActive('/home', { something: 'else' })).toBe(false);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('a pathname that matches only the beginning of the URL', function () {
+ it('is not active', function (done) {
+ React.render((
+
+
+
+ ), node, function () {
+ expect(this.isActive('/h')).toBe(false);
+ done();
+ });
+ });
+ });
+
+});
diff --git a/modules/__tests__/BrowserHistory-test.js b/modules/__tests__/BrowserHistory-test.js
deleted file mode 100644
index 292d563136..0000000000
--- a/modules/__tests__/BrowserHistory-test.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import describeHistory from './describeHistory';
-import BrowserHistory, { history } from '../BrowserHistory';
-
-describe('BrowserHistory', function () {
- describeHistory(new BrowserHistory);
- describeHistory(history);
-});
diff --git a/modules/__tests__/HashHistory-test.js b/modules/__tests__/HashHistory-test.js
deleted file mode 100644
index e243209bac..0000000000
--- a/modules/__tests__/HashHistory-test.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import describeHistory from './describeHistory';
-import HashHistory, { history } from '../HashHistory';
-
-describe('HashHistory', function () {
- describeHistory(new HashHistory);
- describeHistory(history);
-});
diff --git a/modules/__tests__/Link-test.js b/modules/__tests__/Link-test.js
index e35411a4dd..77b3c515ec 100644
--- a/modules/__tests__/Link-test.js
+++ b/modules/__tests__/Link-test.js
@@ -1,9 +1,9 @@
import assert from 'assert';
import expect from 'expect';
-import React, { render } from 'react/addons';
+import React from 'react/addons';
+import createHistory from 'history/lib/createMemoryHistory';
+import execSteps from './execSteps';
import Router from '../Router';
-import MemoryHistory from '../MemoryHistory';
-import HashHistory from '../HashHistory';
import Route from '../Route';
import Link from '../Link';
@@ -34,9 +34,9 @@ describe('A ', function () {
}
});
- var div;
+ var node;
beforeEach(function () {
- div = document.createElement('div');
+ node = document.createElement('div');
});
it('knows how to make its href', function () {
@@ -46,32 +46,34 @@ describe('A ', function () {
}
});
- render((
-
-
+ React.render((
+
+
- ), div, function () {
- var a = div.querySelector('a');
+ ), node, function () {
+ var a = node.querySelector('a');
expect(a.getAttribute('href')).toEqual('/hello/michael?the=query');
});
});
- it('knows how to make its href with HashHistory', function () {
- var LinkWrapper = React.createClass({
- render() {
- return Link;
- }
- });
-
- render((
-
-
-
- ), div, function () {
- var a = div.querySelector('a');
- expect(a.getAttribute('href')).toEqual('#/hello/michael?the=query');
- });
- });
+ // This test needs to be in its own file with beforeEach(resetHash).
+ //
+ //it('knows how to make its href with HashHistory', function () {
+ // var LinkWrapper = React.createClass({
+ // render() {
+ // return Link;
+ // }
+ // });
+
+ // render((
+ //
+ //
+ //
+ // ), node, function () {
+ // var a = node.querySelector('a');
+ // expect(a.getAttribute('href')).toEqual('#/hello/michael?the=query');
+ // });
+ //});
describe('with params', function () {
var App = React.createClass({
@@ -86,28 +88,28 @@ describe('A ', function () {
});
it('is active when its params match', function (done) {
- render((
-
+ React.render((
+
-
+
- ), div, function () {
- var a = div.querySelectorAll('a')[0];
+ ), node, function () {
+ var a = node.querySelectorAll('a')[0];
expect(a.className.trim()).toEqual('active');
done();
});
});
it('is not active when its params do not match', function (done) {
- render((
-
+ React.render((
+
-
+
- ), div, function () {
- var a = div.querySelectorAll('a')[1];
+ ), node, function () {
+ var a = node.querySelectorAll('a')[1];
expect(a.className.trim()).toEqual('');
done();
});
@@ -120,7 +122,7 @@ describe('A ', function () {
render() {
return (
- Link
+ Link
{this.props.children}
);
@@ -129,32 +131,25 @@ describe('A ', function () {
var a, steps = [
function () {
- a = div.querySelector('a');
+ a = node.querySelector('a');
expect(a.className).toEqual('dontKillMe');
- this.transitionTo('hello');
+ this.transitionTo('/hello');
},
function () {
expect(a.className).toEqual('dontKillMe');
- done();
}
];
- function execNextStep() {
- try {
- steps.shift().apply(this, arguments);
- } catch (error) {
- done(error);
- }
- }
+ var execNextStep = execSteps(steps, done);
- render((
-
+ React.render((
+
- ), div, execNextStep);
+ ), node, execNextStep);
});
});
@@ -164,7 +159,7 @@ describe('A ', function () {
render() {
return (
- Link
+ Link
{this.props.children}
);
@@ -173,32 +168,25 @@ describe('A ', function () {
var a, steps = [
function () {
- a = div.querySelector('a');
+ a = node.querySelector('a');
expect(a.className).toEqual('dontKillMe');
- this.transitionTo('hello');
+ this.transitionTo('/hello');
},
function () {
expect(a.className).toEqual('dontKillMe highlight');
- done();
}
];
- function execNextStep() {
- try {
- steps.shift().apply(this, arguments);
- } catch (error) {
- done(error);
- }
- }
+ var execNextStep = execSteps(steps, done);
- render((
-
+ React.render((
+
- ), div, execNextStep);
+ ), node, execNextStep);
});
it('has its activeStyle', function (done) {
@@ -206,7 +194,7 @@ describe('A ', function () {
render() {
return (
- Link
+ Link
{this.props.children}
);
@@ -215,32 +203,25 @@ describe('A ', function () {
var a, steps = [
function () {
- a = div.querySelector('a');
+ a = node.querySelector('a');
expect(a.style.color).toEqual('white');
- this.transitionTo('hello');
+ this.transitionTo('/hello');
},
function () {
expect(a.style.color).toEqual('red');
- done();
}
];
- function execNextStep() {
- try {
- steps.shift().apply(this, arguments);
- } catch (error) {
- done(error);
- }
- }
+ var execNextStep = execSteps(steps, done);
- render((
-
+ React.render((
+
- ), div, execNextStep);
+ ), node, execNextStep);
});
});
@@ -253,17 +234,17 @@ describe('A ', function () {
done();
},
render() {
- return Link;
+ return Link;
}
});
- render((
-
-
-
+ React.render((
+
+
+
- ), div, () => {
- click(div.querySelector('a'));
+ ), node, () => {
+ click(node.querySelector('a'));
});
});
@@ -273,34 +254,27 @@ describe('A ', function () {
// just here to make sure click handlers don't prevent it from happening
},
render() {
- return Link;
+ return Link;
}
});
var steps = [
function () {
- click(div.querySelector('a'), { button: 0 });
+ click(node.querySelector('a'), { button: 0 });
},
function () {
- expect(div.innerHTML).toMatch(/Hello/);
- done();
+ expect(node.innerHTML).toMatch(/Hello/);
}
];
- function execNextStep() {
- try {
- steps.shift().apply(this, arguments);
- } catch (error) {
- done(error);
- }
- }
+ var execNextStep = execSteps(steps, done);
- render((
-
-
-
+ React.render((
+
+
+
- ), div, execNextStep);
+ ), node, execNextStep);
});
});
diff --git a/modules/__tests__/Location-test.js b/modules/__tests__/Location-test.js
deleted file mode 100644
index 113a5b6c1a..0000000000
--- a/modules/__tests__/Location-test.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import expect from 'expect';
-import Location from '../Location';
-
-describe('Location.isLocation', function () {
- it('returns true for Location objects', function () {
- expect(Location.isLocation(new Location)).toBe(true);
- });
-
- it('returns false for other objects', function () {
- expect(Location.isLocation('path')).toBe(false);
- expect(Location.isLocation(1)).toBe(false);
- expect(Location.isLocation(true)).toBe(false);
- expect(Location.isLocation(undefined)).toBe(false);
- expect(Location.isLocation(null)).toBe(false);
- });
-});
diff --git a/modules/__tests__/MemoryHistory-test.js b/modules/__tests__/MemoryHistory-test.js
deleted file mode 100644
index c9ef0852c2..0000000000
--- a/modules/__tests__/MemoryHistory-test.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import expect from 'expect';
-import describeHistory from './describeHistory';
-import MemoryHistory from '../MemoryHistory';
-
-describe('MemoryHistory', function () {
- describeHistory(new MemoryHistory('/'));
-
- var history;
- beforeEach(function () {
- history = new MemoryHistory('/');
- });
-
- describe('when first created', function () {
- it('cannot go back', function () {
- expect(history.canGoBack()).toBe(false);
- });
-
- it('cannot go forward', function () {
- expect(history.canGoForward()).toBe(false);
- });
- });
-
- describe('when pushing a new path', function () {
- beforeEach(function () {
- history.pushState(null, '/push');
- });
-
- it('increments current index by one', function () {
- expect(history.current).toEqual(1);
- });
-
- it('has the correct path', function () {
- expect(history.location.pathname).toEqual('/push');
- });
-
- it('can go back', function () {
- expect(history.canGoBack()).toBe(true);
- });
-
- it('cannot go forward', function () {
- expect(history.canGoForward()).toBe(false);
- });
-
- describe('and then replacing that path', function () {
- beforeEach(function () {
- history.replaceState(null, '/replace');
- });
-
- it('maintains the current index', function () {
- expect(history.current).toEqual(1);
- });
-
- it('returns the correct path', function () {
- expect(history.location.pathname).toEqual('/replace');
- });
-
- it('can go back', function () {
- expect(history.canGoBack()).toBe(true);
- });
-
- it('cannot go forward', function () {
- expect(history.canGoForward()).toBe(false);
- });
- });
-
- describe('and then going back', function () {
- beforeEach(function () {
- history.back();
- });
-
- it('decrements current index by one', function () {
- expect(history.current).toEqual(0);
- });
-
- it('has the correct path', function () {
- expect(history.location.pathname).toEqual('/');
- });
-
- it('cannot go back', function () {
- expect(history.canGoBack()).toBe(false);
- });
-
- it('can go forward', function () {
- expect(history.canGoForward()).toBe(true);
- });
- });
- });
-});
diff --git a/modules/__tests__/Redirect-test.js b/modules/__tests__/Redirect-test.js
index 6f64df4221..d8f99f1be3 100644
--- a/modules/__tests__/Redirect-test.js
+++ b/modules/__tests__/Redirect-test.js
@@ -1,9 +1,9 @@
import expect from 'expect';
-import React, { render } from 'react';
-import MemoryHistory from '../MemoryHistory';
+import React from 'react';
+import createHistory from 'history/lib/createMemoryHistory';
+import Redirect from '../Redirect';
import Router from '../Router';
import Route from '../Route';
-import Redirect from '../Redirect';
describe('A ', function () {
var node;
@@ -16,10 +16,10 @@ describe('A ', function () {
});
it('works', function (done) {
- render((
-
-
-
+ React.render((
+
+
+
), node, function () {
expect(this.state.location.pathname).toEqual('/messages/5');
diff --git a/modules/__tests__/Router-test.js b/modules/__tests__/Router-test.js
index 51ee502692..8847ac57ac 100644
--- a/modules/__tests__/Router-test.js
+++ b/modules/__tests__/Router-test.js
@@ -1,17 +1,18 @@
import expect from 'expect';
-import React, { render } from 'react';
-import MemoryHistory from '../MemoryHistory';
+import React from 'react';
+import { createLocation } from 'history';
import Router from '../Router';
import Route from '../Route';
describe('Router', function () {
- var div;
+
+ var node;
beforeEach(function () {
- div = document.createElement('div');
+ node = document.createElement('div');
});
afterEach(function () {
- React.unmountComponentAtNode(div);
+ React.unmountComponentAtNode(node);
});
var Parent = React.createClass({
@@ -27,54 +28,54 @@ describe('Router', function () {
});
it('renders routes', function (done) {
- render((
-
-
+ React.render((
+
+
- ), div, function () {
- expect(div.textContent.trim()).toEqual('parent');
+ ), node, function () {
+ expect(node.textContent.trim()).toEqual('parent');
done();
});
});
it('renders child routes when the parent does not have a path', function (done) {
- render((
-
+ React.render((
+
-
+
- ), div, function () {
- expect(div.textContent.trim()).toEqual('parentparentchild');
+ ), node, function () {
+ expect(node.textContent.trim()).toEqual('parentparentchild');
done();
});
});
it('renders nested children correctly', function (done) {
- render((
-
+ React.render((
+
-
+
- ), div, function () {
- expect(div.textContent.trim()).toMatch(/parent/);
- expect(div.textContent.trim()).toMatch(/child/);
+ ), node, function () {
+ expect(node.textContent.trim()).toMatch(/parent/);
+ expect(node.textContent.trim()).toMatch(/child/);
done();
});
});
it('renders the child\'s component when it has no component', function (done) {
- render((
-
+ React.render((
+
-
+
- ), div, function () {
- expect(div.textContent.trim()).toMatch(/child/);
+ ), node, function () {
+ expect(node.textContent.trim()).toMatch(/child/);
done();
});
});
@@ -83,7 +84,7 @@ describe('Router', function () {
var Wrapper = React.createClass({
render() {
var { Component } = this.props;
- return
+ return
}
});
@@ -93,12 +94,12 @@ describe('Router', function () {
}
});
- render((
- }>
+ React.render((
+ }>
- ), div, function () {
- expect(div.textContent.trim()).toEqual('wrapped');
+ ), node, function () {
+ expect(node.textContent.trim()).toEqual('wrapped');
done();
});
});
diff --git a/modules/__tests__/RoutingUtils-test.js b/modules/__tests__/RoutingUtils-test.js
deleted file mode 100644
index c91b03eb77..0000000000
--- a/modules/__tests__/RoutingUtils-test.js
+++ /dev/null
@@ -1,370 +0,0 @@
-import expect, { spyOn } from 'expect';
-import { getState as _getState } from '../RoutingUtils';
-import Location from '../Location';
-
-function getState(routes, pathname, callback) {
- return _getState(routes, new Location(pathname), callback);
-}
-
-describe('getState', function () {
- var RootRoute, AboutRoute, CoursesRoute, GradesRoute, CourseRoute, CourseGradesRoute, AssignmentRoute, AssignmentsRoute, CatchAllRoute, AccountRoute, AccountIndexRoute, ProfileRoute, ProfileIndexRoute;
- beforeEach(function () {
- AboutRoute = {
- path: 'about'
- };
-
- GradesRoute = {
- path: 'grades'
- };
-
- CoursesRoute = {
- path: 'courses',
- getChildRoutes(state, callback) {
- callback(null, [ GradesRoute ]);
- }
- };
-
- CourseGradesRoute = {
- path: 'grades'
- };
-
- AssignmentRoute = {
- path: 'assignments/:assignmentID'
- };
-
- AssignmentsRoute = {
- path: 'assignments'
- };
-
- CourseRoute = {
- getChildRoutes(state, callback) {
- setTimeout(function () {
- callback(null, [ CourseGradesRoute, AssignmentRoute, AssignmentsRoute ]);
- }, 0);
- }
- };
-
- AccountIndexRoute = {};
-
- AccountRoute = {
- path: 'account',
- indexRoute: AccountIndexRoute
- };
-
- ProfileIndexRoute = {};
-
- ProfileRoute = {
- path: 'profile',
- getIndexRoute (cb) {
- cb(null, ProfileIndexRoute);
- }
- };
-
- CatchAllRoute = {
- path: '*'
- };
-
- RootRoute = {
- path: '/',
- childRoutes: [ AboutRoute, CoursesRoute, CourseRoute, AccountRoute, ProfileRoute, CatchAllRoute ]
- };
- });
-
- describe('when the path does not match any route', function () {
- it('matches the "catch all" route', function (done) {
- getState(RootRoute, '/not-found', function (error, state) {
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.branch).toEqual([ RootRoute, CatchAllRoute ]);
- done();
- });
- });
- });
-
- describe('when the path matches a route exactly', function () {
- it('matches', function (done) {
- getState(RootRoute, '/', function (error, state) {
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.branch).toEqual([ RootRoute ]);
- done();
- });
- });
- });
-
- describe('when the path matches a nested route exactly', function () {
- it('matches', function (done) {
- getState(RootRoute, '/courses', function (error, state) {
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.branch).toEqual([ RootRoute, CoursesRoute ]);
- done();
- });
- });
-
- it('does not attempt to fetch the nested route\'s child routes', function (done) {
- var spy = spyOn(CoursesRoute, 'getChildRoutes');
-
- getState(RootRoute, '/courses', function () {
- expect(spy.calls.length).toEqual(0);
- done();
- });
- });
-
- describe('with an index route', function () {
- it('matches synchronously', function (done) {
- getState(RootRoute, '/account', function (error, state) {
- expect(state.branch).toEqual([ RootRoute, AccountRoute, AccountIndexRoute]);
- done();
- });
- });
-
- it('matches asynchronously', function (done) {
- getState(RootRoute, '/profile', function (error, state) {
- expect(state.branch).toEqual([ RootRoute, ProfileRoute, ProfileIndexRoute]);
- done();
- });
- });
- });
- });
-
- describe('when the path matches a nested route with a trailing slash', function () {
- it('matches', function (done) {
- getState(RootRoute, '/courses/', function (error, state) {
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.branch).toEqual([ RootRoute, CoursesRoute ]);
- done();
- });
- });
-
- it('does not attempt to fetch the nested route\'s child routes', function (done) {
- var spy = spyOn(CoursesRoute, 'getChildRoutes');
-
- getState(RootRoute, '/courses/', function () {
- expect(spy.calls.length).toEqual(0);
- done();
- });
- });
- });
-
- describe('when the path matches a deeply nested route', function () {
- it('matches', function (done) {
- getState(RootRoute, '/courses/grades', function (error, state) {
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.branch).toEqual([ RootRoute, CoursesRoute, GradesRoute ]);
- done();
- });
- });
-
- it('fetches the nested route\'s child routes', function (done) {
- var spy = spyOn(CoursesRoute, 'getChildRoutes').andCallThrough();
-
- getState(RootRoute, '/courses/grades', function () {
- expect(spy).toHaveBeenCalled();
- done();
- });
- });
- });
-
- describe('when the path matches a route with a dynamic segment', function () {
- it('stores the value of that segment in params', function (done) {
- getState(RootRoute, '/assignments/abc', function (error, state) {
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.branch).toEqual([ RootRoute, CourseRoute, AssignmentRoute ]);
- expect(state.params).toEqual({ assignmentID: 'abc' });
- done();
- });
- });
- });
-
- describe('when nested routes are able to be fetched synchronously', function () {
- it('matches synchronously', function () {
- var error, state;
-
- getState(RootRoute, '/courses/grades', function (innerError, innerState) {
- error = innerError;
- state = innerState;
- });
-
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.branch).toEqual([ RootRoute, CoursesRoute, GradesRoute ]);
- });
- });
-
- describe('when nested routes must be fetched asynchronously', function () {
- it('matches asynchronously', function (done) {
- var outerError, outerState;
-
- getState(RootRoute, '/assignments', function (error, state) {
- outerError = error;
- outerState = state;
-
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.branch).toEqual([ RootRoute, CourseRoute, AssignmentsRoute ]);
- done();
- });
-
- expect(outerError).toNotExist();
- expect(outerState).toNotExist();
- });
- });
-});
-
-describe('Matching params', function () {
- function assertParams(routes, pathname, params, callback) {
- if (typeof routes === 'string')
- routes = [ { path: routes } ];
-
- getState(routes, pathname, function (error, state) {
- try {
- expect(error).toNotExist();
- expect(state).toBeAn('object');
- expect(state.params).toEqual(params);
- callback();
- } catch (error) {
- callback(error);
- }
- });
- }
-
- describe('when a pattern does not have dynamic segments', function () {
- var pattern = 'about/us';
-
- describe('and the path matches', function () {
- it('returns an empty object', function (done) {
- assertParams(pattern, '/about/us', {}, done);
- });
- });
- });
-
- describe('when a pattern has dynamic segments', function () {
- var pattern = 'comments/:id.:ext/edit';
-
- describe('and the path matches', function () {
- it('returns an object with the params', function (done) {
- assertParams(pattern, 'comments/abc.js/edit', { id: 'abc', ext: 'js' }, done);
- });
- });
-
- describe('and the pattern is optional', function () {
- var pattern = 'comments/(:id)/edit';
-
- describe('and the path matches with supplied param', function () {
- it('returns an object with the params', function (done) {
- assertParams(pattern, 'comments/123/edit', { id: '123' }, done);
- });
- });
-
- describe('and the path matches without supplied param', function () {
- it('returns an object with an undefined param', function (done) {
- assertParams(pattern, 'comments//edit', { id: undefined }, done);
- });
- });
- });
-
- describe('and the pattern and forward slash are optional', function () {
- var pattern = 'comments(/:id)/edit';
-
- describe('and the path matches with supplied param', function () {
- it('returns an object with the params', function (done) {
- assertParams(pattern, 'comments/123/edit', { id: '123' }, done);
- });
- });
-
- describe('and the path matches without supplied param', function () {
- it('returns an object with an undefined param', function (done) {
- assertParams(pattern, 'comments/edit', { id: undefined }, done);
- });
- });
- });
-
- describe('and the path matches with a segment containing a .', function () {
- it('returns an object with the params', function (done) {
- assertParams(pattern, 'comments/foo.bar/edit', { id: 'foo', ext: 'bar' }, done);
- });
- });
- });
-
- describe('when a pattern has characters that have special URL encoding', function () {
- var pattern = 'one, two';
-
- describe('and the path matches', function () {
- it('returns an empty object', function (done) {
- assertParams(pattern, 'one, two', {}, done);
- });
- });
- });
-
- describe('when a pattern has dynamic segments and characters that have special URL encoding', function () {
- var pattern = '/comments/:id/edit now';
-
- describe('and the path matches', function () {
- it('returns an object with the params', function (done) {
- assertParams(pattern, '/comments/abc/edit now', { id: 'abc' }, done);
- });
- });
- });
-
- describe('when a pattern has a *', function () {
- describe('and the path has a single extension', function () {
- it('matches', function (done) {
- assertParams('/files/*', '/files/my/photo.jpg', { splat: 'my/photo.jpg' }, done);
- });
- });
-
- describe('and the path has multiple extensions', function () {
- it('matches', function (done) {
- assertParams('/files/*', '/files/my/photo.jpg.zip', { splat: 'my/photo.jpg.zip' }, done);
- });
- });
-
- describe('and an extension', function () {
- it('matches', function (done) {
- assertParams('/files/*.jpg', '/files/my/photo.jpg', { splat: 'my/photo' }, done);
- });
- });
- });
-
- describe('when a pattern has an optional group', function () {
- var pattern = '/archive(/:name)';
-
- describe('and the path contains a param for that group', function () {
- it('matches', function (done) {
- assertParams(pattern, '/archive/foo', { name: 'foo' }, done);
- });
- });
-
- describe('and the path does not contain a param for that group', function () {
- it('matches', function (done) {
- assertParams(pattern, '/archive', { name: undefined }, done);
- });
- });
- });
-
- describe('when a pattern contains dynamic segments', function () {
- var pattern = '/:query/with/:domain';
-
- describe('and the first param contains a dot', function () {
- it('matches', function (done) {
- assertParams(pattern, '/foo.ap/with/foo', { query: 'foo.ap', domain: 'foo' }, done);
- });
- });
-
- describe('and the second param contains a dot', function () {
- it('matches', function (done) {
- assertParams(pattern, '/foo/with/foo.app', { query: 'foo', domain: 'foo.app' }, done);
- });
- });
-
- describe('and both params contain a dot', function () {
- it('matches', function (done) {
- assertParams(pattern, '/foo.ap/with/foo.app', { query: 'foo.ap', domain: 'foo.app' }, done);
- });
- });
- });
-});
diff --git a/modules/__tests__/ScrollManagementMixin-test.js b/modules/__tests__/ScrollManagementMixin-test.js
new file mode 100644
index 0000000000..fcd0fb6c82
--- /dev/null
+++ b/modules/__tests__/ScrollManagementMixin-test.js
@@ -0,0 +1,73 @@
+import expect from 'expect';
+import React from 'react';
+import createHistory from 'history/lib/createHashHistory';
+import resetHash from './resetHash';
+import execStepsWithDelay from './execStepsWithDelay';
+import { getWindowScrollPosition } from '../DOMUtils';
+import Router from '../Router';
+import Route from '../Route';
+
+describe.skip('ScrollManagementMixin', function () {
+ var Home = React.createClass({
+ render() {
+ return Yo, this page is huge.
;
+ }
+ });
+
+ var Inbox = React.createClass({
+ render() {
+ return Yo, this page is huge.
;
+ }
+ });
+
+ beforeEach(resetHash);
+
+ var node;
+ beforeEach(function (done) {
+ node = document.createElement('div');
+ document.body.appendChild(node);
+ });
+
+ afterEach(function () {
+ React.unmountComponentAtNode(node);
+ document.body.removeChild(node);
+ });
+
+ it('correctly updates the window scroll position', function (done) {
+ var steps = [
+ function () {
+ expect(this.state.location.pathname).toEqual('/');
+
+ window.scrollTo(200, 200);
+ expect(getWindowScrollPosition()).toEqual({ x: 200, y: 200 });
+
+ this.transitionTo('/inbox');
+ },
+ function () {
+ expect(this.state.location.pathname).toEqual('/inbox');
+ expect(getWindowScrollPosition()).toEqual({ x: 0, y: 0 });
+
+ this.goBack();
+ },
+ function () {
+ expect(this.state.location.pathname).toEqual('/');
+ expect(getWindowScrollPosition()).toEqual({ x: 200, y: 200 });
+ }
+ ];
+
+ var execNextStep = execStepsWithDelay(steps, 10, done);
+
+ // Needs scroll support in the history module
+ // See https://github.com/rackt/history/issues/17
+ var history = createHistory({
+ getScrollPosition: getWindowScrollPosition
+ });
+
+ React.render((
+
+
+
+
+ ), node, execNextStep);
+ });
+});
diff --git a/modules/__tests__/Transition-test.js b/modules/__tests__/Transition-test.js
deleted file mode 100644
index 04bd3b9a74..0000000000
--- a/modules/__tests__/Transition-test.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import assert from 'assert';
-import Transition from '../Transition';
-
-describe('A new Transition', function () {
-
- var transition;
- beforeEach(function () {
- transition = new Transition;
- });
-
- it('is not cancelled', function () {
- assert(!transition.isCancelled);
- });
-
- describe('that is redirected', function () {
- beforeEach(function () {
- transition.to('/something/else');
- });
-
- it('is cancelled', function () {
- assert(transition.isCancelled);
- });
- });
-
- describe('that is aborted', function () {
- beforeEach(function () {
- transition.abort();
- });
-
- it('is cancelled', function () {
- assert(transition.isCancelled);
- });
- });
-
-});
diff --git a/modules/__tests__/TransitionHook-test.js b/modules/__tests__/TransitionHook-test.js
deleted file mode 100644
index d2e2bd6895..0000000000
--- a/modules/__tests__/TransitionHook-test.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import expect from 'expect';
-import React, { render } from 'react';
-import MemoryHistory from '../MemoryHistory';
-import TransitionHook from '../TransitionHook';
-import Router from '../Router';
-import Route from '../Route';
-
-describe('TransitionHook', function () {
- it('calls routerWillLeave when the router leaves the current location', function (done) {
- var div = document.createElement('div');
- var hookCalled = false;
-
- var One = React.createClass({
- mixins: [ TransitionHook ],
- routerWillLeave() {
- hookCalled = true;
- },
- render() {
- return one
;
- }
- });
-
- var Two = React.createClass({
- render() {
- return two
;
- }
- });
-
- var steps = [
- function () {
- expect(this.state.location.pathname).toEqual('/one');
- expect(div.textContent.trim()).toEqual('one');
- this.transitionTo('/two')
- },
- function () {
- expect(hookCalled).toBe(true);
- expect(this.state.location.pathname).toEqual('/two');
- done();
- }
- ];
-
- function execNextStep() {
- steps.shift().apply(this, arguments);
- }
-
- render((
-
-
-
-
- ), div, execNextStep);
- });
-});
diff --git a/modules/__tests__/createRoutesFromReactChildren-test.js b/modules/__tests__/createRoutesFromReactChildren-test.js
index 5aa7086934..21d1d33ddd 100644
--- a/modules/__tests__/createRoutesFromReactChildren-test.js
+++ b/modules/__tests__/createRoutesFromReactChildren-test.js
@@ -1,7 +1,7 @@
import expect from 'expect';
import React from 'react';
-import Route from '../Route';
import { createRoutesFromReactChildren } from '../RouteUtils';
+import Route from '../Route';
describe('createRoutesFromReactChildren', function () {
diff --git a/modules/__tests__/describeHistory.js b/modules/__tests__/describeHistory.js
deleted file mode 100644
index acbe2b8d0f..0000000000
--- a/modules/__tests__/describeHistory.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import expect, { createSpy, spyOn } from 'expect';
-import History from '../History';
-
-export default function describeHistory(history) {
- it('is an instanceof History', function () {
- expect(history).toBeA(History);
- });
-
- var RequiredMethods = [ 'pushState', 'replaceState', 'go' ];
-
- RequiredMethods.forEach(function (method) {
- it('has a ' + method + ' method', function () {
- expect(history[method]).toBeA('function');
- });
- });
-
- describe('adding/removing a listener', function () {
- var pushState, go, pushStateSpy, goSpy;
- beforeEach(function () {
- // It's a bit tricky to test change listeners properly because
- // they are triggered when the URL changes. So we need to stub
- // out push/go to only notify listeners ... but we can't make
- // assertions on the location because it will be wrong.
- pushState = history.pushState;
- pushStateSpy = spyOn(history, 'pushState').andCall(history._notifyChange);
-
- go = history.go;
- goSpy = spyOn(history, 'go').andCall(history._notifyChange);
- });
-
- afterEach(function () {
- history.push = pushState;
- history.go = go;
- });
-
- it('works', function () {
- var spy = expect.createSpy(function () {});
-
- history.addChangeListener(spy);
- history.pushState(null, '/home'); // call #1
- expect(pushStateSpy).toHaveBeenCalled();
-
- expect(spy.calls.length).toEqual(1);
-
- history.removeChangeListener(spy)
- history.back(); // call #2
- expect(goSpy).toHaveBeenCalled();
-
- expect(spy.calls.length).toEqual(1);
- });
- });
-}
diff --git a/modules/__tests__/execSteps.js b/modules/__tests__/execSteps.js
new file mode 100644
index 0000000000..2886aa0075
--- /dev/null
+++ b/modules/__tests__/execSteps.js
@@ -0,0 +1,20 @@
+function execSteps(steps, done) {
+ var index = 0;
+
+ return function () {
+ if (steps.length === 0) {
+ done();
+ } else {
+ try {
+ steps[index++].apply(this, arguments);
+
+ if (index === steps.length)
+ done();
+ } catch (error) {
+ done(error);
+ }
+ }
+ };
+}
+
+export default execSteps;
diff --git a/modules/__tests__/execStepsWithDelay.js b/modules/__tests__/execStepsWithDelay.js
new file mode 100644
index 0000000000..9900f111f0
--- /dev/null
+++ b/modules/__tests__/execStepsWithDelay.js
@@ -0,0 +1,15 @@
+import execSteps from './execSteps';
+
+function execStepsWithDelay(steps, delay, done) {
+ var execNextStep = execSteps(steps, done);
+
+ return function () {
+ var context = this, args = Array.prototype.slice.call(arguments, 0);
+
+ return setTimeout(function () {
+ execNextStep.apply(context, args);
+ }, delay);
+ };
+}
+
+export default execStepsWithDelay;
diff --git a/modules/__tests__/matchRoutes-test.js b/modules/__tests__/matchRoutes-test.js
index 3f1901f751..2b66af8b27 100644
--- a/modules/__tests__/matchRoutes-test.js
+++ b/modules/__tests__/matchRoutes-test.js
@@ -3,8 +3,8 @@ import expect from 'expect';
import { createLocation } from 'history';
import matchRoutes from '../matchRoutes';
-describe.skip('matchRoutes', function () {
- var routes, RootRoute, UsersRoute, UsersIndexRoute, UserRoute, AboutRoute, TeamRoute, ProfileRoute;
+describe('matchRoutes', function () {
+ var routes, RootRoute, UsersRoute, UsersIndexRoute, UserRoute, PostRoute, FilesRoute, AboutRoute, TeamRoute, ProfileRoute, CatchAllRoute;
beforeEach(function () {
/*
@@ -17,6 +17,7 @@ describe.skip('matchRoutes', function () {
+
*/
routes = [
RootRoute = {
@@ -30,6 +31,9 @@ describe.skip('matchRoutes', function () {
childRoutes: [
ProfileRoute = {
path: '/profile'
+ },
+ PostRoute = {
+ path: ':postID'
}
]
},
@@ -40,8 +44,14 @@ describe.skip('matchRoutes', function () {
}
]
},
+ FilesRoute = {
+ path: '/files/*/*.jpg'
+ },
AboutRoute = {
path: '/about'
+ },
+ CatchAllRoute = {
+ path: '*'
}
];
});
@@ -68,6 +78,28 @@ describe.skip('matchRoutes', function () {
});
});
+ describe('when the location matches a deeply nested route with params', function () {
+ it('matches the correct routes and params', function (done) {
+ matchRoutes(routes, createLocation('/users/5/abc'), function (error, match) {
+ assert(match);
+ expect(match.routes).toEqual([ RootRoute, UsersRoute, UserRoute, PostRoute ]);
+ expect(match.params).toEqual({ userID: '5', postID: 'abc' });
+ done();
+ });
+ });
+ });
+
+ describe('when the location matches a nested route with multiple splat params', function () {
+ it('matches the correct routes and params', function (done) {
+ matchRoutes(routes, createLocation('/files/a/b/c.jpg'), function (error, match) {
+ assert(match);
+ expect(match.routes).toEqual([ FilesRoute ]);
+ expect(match.params).toEqual({ splat: [ 'a', 'b/c' ] });
+ done();
+ });
+ });
+ });
+
describe('when the location matches an absolute route', function () {
it('matches the correct routes', function (done) {
matchRoutes(routes, createLocation('/about'), function (error, match) {
@@ -77,6 +109,16 @@ describe.skip('matchRoutes', function () {
});
});
});
+
+ describe('when the location does not match any routes', function () {
+ it('matches the "catch-all" route', function (done) {
+ matchRoutes(routes, createLocation('/not-found'), function (error, match) {
+ assert(match);
+ expect(match.routes).toEqual([ CatchAllRoute ]);
+ done();
+ });
+ });
+ });
}
describe('a synchronous route config', function () {
@@ -97,7 +139,7 @@ describe.skip('matchRoutes', function () {
matchRoutes(routes, createLocation('/profile'), function (error, match) {
assert(match);
expect(match.routes).toEqual([ RootRoute, UsersRoute, UserRoute, ProfileRoute ]);
- expect(match.params).toEqual({ userID: null });
+ expect(match.params).toEqual({}); // no userID param
done();
});
});
diff --git a/modules/__tests__/resetHash.js b/modules/__tests__/resetHash.js
new file mode 100644
index 0000000000..1a751c500f
--- /dev/null
+++ b/modules/__tests__/resetHash.js
@@ -0,0 +1,10 @@
+function resetHash(done) {
+ if (window.location.hash !== '') {
+ window.location.hash = '';
+ setTimeout(done, 10);
+ } else {
+ done();
+ }
+}
+
+export default resetHash;
diff --git a/modules/__tests__/routeMatching-test.js b/modules/__tests__/routeMatching-test.js
deleted file mode 100644
index c2abdd1716..0000000000
--- a/modules/__tests__/routeMatching-test.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import expect from 'expect';
-import React, { render } from 'react';
-import MemoryHistory from '../MemoryHistory';
-import Router from '../Router';
-import Route from '../Route';
-
-describe('A Router', function () {
- var div;
- beforeEach(function () {
- div = document.createElement('div');
- });
-
- afterEach(function () {
- React.unmountComponentAtNode(div);
- });
-
- var Component1 = React.createClass({ render() { return null; } });
- var Component2 = React.createClass({ render() { return null; } });
- var Component3 = React.createClass({ render() { return null; } });
- var Component4 = React.createClass({ render() { return null; } });
-
- describe('with a synchronous route config', function () {
- var childRoutes = [
- { path: 'two/:name', component: Component2 },
- { path: 'three',
- components: {
- main: Component3,
- sidebar: Component4
- }
- }
- ];
-
- var parentRoute = {
- path: '/',
- component: Component1,
- childRoutes: childRoutes,
- };
-
- var routes = [ parentRoute ];
-
- it('matches the correct components', function (done) {
- render(, div, function () {
- expect(this.state.components).toEqual([
- parentRoute.component,
- childRoutes[0].component
- ]);
-
- done();
- });
- });
-
- it('matches named components', function (done) {
- render(, div, function () {
- expect(this.state.components).toEqual([
- Component1,
- { main: Component3, sidebar: Component4 }
- ]);
-
- done();
- });
- });
-
- it('matches the correct route branch', function (done) {
- render(, div, function () {
- expect(this.state.branch).toEqual([
- parentRoute,
- childRoutes[1]
- ]);
-
- done();
- });
- });
-
- it('matches the correct params', function (done) {
- render(, div, function () {
- expect(this.state.params).toEqual({ name: 'sally' });
- done();
- });
- });
- });
-
- describe('with an asynchronous route config', function () {
- var childRoutes = [
- { path: 'two/:name', getComponents (cb){ cb(null, Component2); } },
- {
- path: 'three',
- getComponents (cb) {
- cb(null, {
- main: Component3,
- sidebar: Component4
- });
- }
- }
- ];
-
- var parentRoute = {
- path: '/',
- getChildRoutes (state, cb) {
- cb(null, childRoutes);
- },
- getComponents (cb) {
- cb(null, Component1);
- }
- };
-
- var routes = [ parentRoute ];
-
- it('matches the correct components', function (done) {
- render(, div, function () {
- expect(this.state.components).toEqual([ Component1, Component2 ]);
- done();
- });
- });
-
- it('matches named components', function (done) {
- render(, div, function () {
- expect(this.state.components).toEqual([
- Component1,
- { main: Component3, sidebar: Component4 }
- ]);
- done();
- });
- });
-
- it('matches the correct route branch', function (done) {
- render(, div, function () {
- expect(this.state.branch).toEqual([
- parentRoute,
- childRoutes[1]
- ]);
-
- done();
- });
- });
-
- it('matches the correct params', function (done) {
- render(, div, function () {
- expect(this.state.params).toEqual({ name: 'sally' });
- done();
- });
- });
- });
-
-});
diff --git a/modules/__tests__/scrollManagement-test.js b/modules/__tests__/scrollManagement-test.js
deleted file mode 100644
index 410b6d91b4..0000000000
--- a/modules/__tests__/scrollManagement-test.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import expect from 'expect';
-import React, { render } from 'react';
-import HashHistory from '../HashHistory';
-import { getWindowScrollPosition } from '../DOMUtils';
-import Router from '../Router';
-import Route from '../Route';
-
-describe('Scroll management', function () {
- var node, Home, Inbox;
- beforeEach(function (done) {
- node = document.createElement('div');
- document.body.appendChild(node);
-
- Home = React.createClass({
- render() {
- return (
-
- {/* make it scroll baby */}
-
Yo, this is the home page.
-
- );
- }
- });
-
- Inbox = React.createClass({
- render() {
- return This is the inbox.
;
- }
- });
-
- window.location.hash = '/';
-
- // Give the DOM a little time to reflect the hashchange.
- setTimeout(done, 10);
- });
-
- afterEach(function () {
- React.unmountComponentAtNode(node);
- document.body.removeChild(node);
- });
-
- it('correctly updates the window scroll position', function (done) {
- var steps = [
- function () {
- expect(this.state.location.pathname).toEqual('/');
- window.scrollTo(100, 100);
- expect(getWindowScrollPosition()).toEqual({ scrollX: 100, scrollY: 100 });
- this.transitionTo('/inbox');
- },
- function () {
- expect(this.state.location.pathname).toEqual('/inbox');
- expect(getWindowScrollPosition()).toEqual({ scrollX: 0, scrollY: 0 });
- this.goBack();
- },
- function () {
- expect(this.state.location.pathname).toEqual('/');
- expect(getWindowScrollPosition()).toEqual({ scrollX: 100, scrollY: 100 });
- done();
- }
- ];
-
- function execNextStep() {
- if (steps.length < 1){
- done();
- return;
- }
-
- // Give the DOM a little time to reflect the hashchange.
- setTimeout(() => {
- steps.shift().call(this);
- }, 10);
- }
-
- var history = new HashHistory({ queryKey: true });
-
- render((
-
-
-
-
- ), node, execNextStep);
- });
-});
diff --git a/modules/__tests__/serverRendering-test.js b/modules/__tests__/serverRendering-test.js
index f01c68611b..4ff9cc1fff 100644
--- a/modules/__tests__/serverRendering-test.js
+++ b/modules/__tests__/serverRendering-test.js
@@ -1,64 +1,39 @@
import expect from 'expect';
-import React, { createClass, renderToString } from 'react';
-import Location from '../Location';
+import React from 'react';
+import { createLocation } from 'history';
import Router from '../Router';
import Link from '../Link';
-describe('Server rendering', function () {
- var Dashboard, Inbox, DashboardRoute, InboxRoute, RedirectToInboxRoute, routes;
+describe('server rendering', function () {
+ var Dashboard, DashboardRoute, routes;
beforeEach(function () {
- Dashboard = createClass({
+ Dashboard = React.createClass({
render() {
return (
The Dashboard
- {this.props.children}
);
}
});
-
- Inbox = createClass({
- render() {
- return Inbox Go to the dashboard
;
- }
- });
-
- DashboardRoute = {
- component: Dashboard,
- getChildRoutes(locationState, callback) {
- setTimeout(function () {
- callback(null, [ InboxRoute, RedirectToInboxRoute ]);
- }, 0);
- }
- };
- InboxRoute = {
- path: 'inbox',
- component: Inbox
+ DashboardRoute = {
+ path: '/',
+ component: Dashboard
};
- RedirectToInboxRoute = {
- path: 'redirect-to-inbox',
- onEnter(nextState, transition) {
- transition.to('/inbox');
- }
- };
-
routes = [
DashboardRoute
];
});
-
+
it('works', function (done) {
- var location = new Location('/inbox');
+ var location = createLocation('/');
Router.run(routes, location, function (error, state, transition) {
- var string = renderToString();
- expect(string).toMatch(/Dashboard/);
- expect(string).toMatch(/Inbox/);
+ var string = React.renderToString();
+ expect(string).toMatch(/The Dashboard/);
done();
});
});
-
});
diff --git a/modules/__tests__/transitionHooks-test.js b/modules/__tests__/transitionHooks-test.js
index 8bcf78d531..d60b4d44ae 100644
--- a/modules/__tests__/transitionHooks-test.js
+++ b/modules/__tests__/transitionHooks-test.js
@@ -1,15 +1,16 @@
import expect, { spyOn } from 'expect';
-import React, { render, createClass } from 'react';
-import MemoryHistory from '../MemoryHistory';
+import React from 'react';
+import createHistory from 'history/lib/createMemoryHistory';
+import execSteps from './execSteps';
import Router from '../Router';
import Route from '../Route';
describe('When a router enters a branch', function () {
- var div, Dashboard, NewsFeed, Inbox, DashboardRoute, NewsFeedRoute, InboxRoute, RedirectToInboxRoute, MessageRoute, routes;
+ var node, Dashboard, NewsFeed, Inbox, DashboardRoute, NewsFeedRoute, InboxRoute, RedirectToInboxRoute, MessageRoute, routes;
beforeEach(function () {
- div = document.createElement('div');
+ node = document.createElement('div');
- Dashboard = createClass({
+ Dashboard = React.createClass({
render() {
return (
@@ -20,13 +21,13 @@ describe('When a router enters a branch', function () {
}
});
- NewsFeed = createClass({
+ NewsFeed = React.createClass({
render() {
return
News
;
}
});
- Inbox = createClass({
+ Inbox = React.createClass({
render() {
return
Inbox
;
}
@@ -35,65 +36,65 @@ describe('When a router enters a branch', function () {
NewsFeedRoute = {
path: 'news',
component: NewsFeed,
- onEnter(nextState, transition) {
- expect(nextState.branch).toContain(NewsFeedRoute);
- expect(transition).toBeAn('object');
+ onEnter(nextState, redirectTo) {
+ expect(nextState.routes).toContain(NewsFeedRoute);
+ expect(redirectTo).toBeA('function');
},
- onLeave(nextState, transition) {
- expect(nextState.branch).toNotContain(NewsFeedRoute);
- expect(transition).toBeAn('object');
+ onLeave(nextState, redirectTo) {
+ expect(nextState.routes).toNotContain(NewsFeedRoute);
+ expect(redirectTo).toBeA('function');
}
};
InboxRoute = {
path: 'inbox',
component: Inbox,
- onEnter(nextState, transition) {
- expect(nextState.branch).toContain(InboxRoute);
- expect(transition).toBeAn('object');
+ onEnter(nextState, redirectTo) {
+ expect(nextState.routes).toContain(InboxRoute);
+ expect(redirectTo).toBeA('function');
},
- onLeave(nextState, transition) {
- expect(nextState.branch).toNotContain(InboxRoute);
- expect(transition).toBeAn('object');
+ onLeave(nextState, redirectTo) {
+ expect(nextState.routes).toNotContain(InboxRoute);
+ expect(redirectTo).toBeA('function');
}
};
RedirectToInboxRoute = {
path: 'redirect-to-inbox',
- onEnter(nextState, transition) {
- expect(nextState.branch).toContain(RedirectToInboxRoute);
- expect(transition).toBeAn('object');
+ onEnter(nextState, redirectTo) {
+ expect(nextState.routes).toContain(RedirectToInboxRoute);
+ expect(redirectTo).toBeA('function');
- transition.to('/inbox');
+ redirectTo('/inbox');
},
- onLeave(nextState, transition) {
- expect(nextState.branch).toNotContain(RedirectToInboxRoute);
- expect(transition).toBeAn('object');
+ onLeave(nextState, redirectTo) {
+ expect(nextState.routes).toNotContain(RedirectToInboxRoute);
+ expect(redirectTo).toBeA('function');
}
};
MessageRoute = {
path: 'messages/:messageID',
- onEnter(nextState, transition) {
- expect(nextState.branch).toContain(MessageRoute);
- expect(transition).toBeAn('object');
+ onEnter(nextState, redirectTo) {
+ expect(nextState.routes).toContain(MessageRoute);
+ expect(redirectTo).toBeA('function');
},
- onLeave(nextState, transition) {
+ onLeave(nextState, redirectTo) {
// We can't make this assertion when switching from /messages/123 => /messages/456
- //expect(nextState.branch).toNotContain(MessageRoute);
- expect(transition).toBeAn('object');
+ //expect(nextState.routes).toNotContain(MessageRoute);
+ expect(redirectTo).toBeA('function');
}
};
DashboardRoute = {
component: Dashboard,
- onEnter(nextState, transition) {
- expect(nextState.branch).toContain(DashboardRoute);
- expect(transition).toBeAn('object');
+ onEnter(nextState, redirectTo) {
+ expect(nextState.routes).toContain(DashboardRoute);
+ expect(redirectTo).toBeA('function');
},
- onLeave(nextState, transition) {
- expect(nextState.branch).toNotContain(DashboardRoute);
- expect(transition).toBeAn('object');
+ onLeave(nextState, redirectTo) {
+ expect(nextState.routes).toNotContain(DashboardRoute);
+ expect(redirectTo).toBeA('function');
},
childRoutes: [ NewsFeedRoute, InboxRoute, RedirectToInboxRoute, MessageRoute ]
};
@@ -102,12 +103,16 @@ describe('When a router enters a branch', function () {
DashboardRoute
];
});
+
+ afterEach(function () {
+ React.unmountComponentAtNode(node);
+ });
it('calls the onEnter hooks of all routes in that branch', function (done) {
var dashboardRouteEnterSpy = spyOn(DashboardRoute, 'onEnter').andCallThrough();
var newsFeedRouteEnterSpy = spyOn(NewsFeedRoute, 'onEnter').andCallThrough();
- render(
, div, function () {
+ React.render(, node, function () {
expect(dashboardRouteEnterSpy).toHaveBeenCalled();
expect(newsFeedRouteEnterSpy).toHaveBeenCalled();
done();
@@ -120,7 +125,7 @@ describe('When a router enters a branch', function () {
var redirectRouteLeaveSpy = spyOn(RedirectToInboxRoute, 'onLeave').andCallThrough();
var inboxEnterSpy = spyOn(InboxRoute, 'onEnter').andCallThrough();
- render(, div, function () {
+ React.render(, node, function () {
expect(this.state.location.pathname).toEqual('/inbox');
expect(redirectRouteEnterSpy).toHaveBeenCalled();
expect(redirectRouteLeaveSpy.calls.length).toEqual(0);
@@ -144,19 +149,16 @@ describe('When a router enters a branch', function () {
function () {
expect(inboxRouteLeaveSpy).toHaveBeenCalled('InboxRoute.onLeave was not called');
expect(dashboardRouteLeaveSpy.calls.length).toEqual(0, 'DashboardRoute.onLeave was called');
- done();
}
];
- function execNextStep() {
- try {
- steps.shift().apply(this, arguments);
- } catch (error) {
- done(error);
- }
- }
+ var execNextStep = execSteps(steps, done);
- render(, div, execNextStep);
+ React.render(
+ , node, execNextStep);
});
});
@@ -177,20 +179,16 @@ describe('When a router enters a branch', function () {
expect(messageRouteLeaveSpy).toHaveBeenCalled('MessageRoute.onLeave was not called');
expect(messageRouteEnterSpy).toHaveBeenCalled('MessageRoute.onEnter was not called');
expect(dashboardRouteLeaveSpy.calls.length).toEqual(0, 'DashboardRoute.onLeave was called');
- done();
}
];
- function execNextStep() {
- try {
- steps.shift().apply(this, arguments);
- } catch (error) {
- done(error);
- }
- }
+ var execNextStep = execSteps(steps, done);
- render(, div, execNextStep);
+ React.render(
+ , node, execNextStep);
});
});
});
-
diff --git a/modules/__tests__/transitions-test.js b/modules/__tests__/transitionTo-test.js
similarity index 75%
rename from modules/__tests__/transitions-test.js
rename to modules/__tests__/transitionTo-test.js
index b18903c15c..2fdc1f6f29 100644
--- a/modules/__tests__/transitions-test.js
+++ b/modules/__tests__/transitionTo-test.js
@@ -1,10 +1,14 @@
import expect from 'expect';
-import React, { render } from 'react';
-import MemoryHistory from '../MemoryHistory';
+import React from 'react';
+import createHistory from 'history/lib/createHashHistory';
+import resetHash from './resetHash';
+import execSteps from './execSteps';
import Router from '../Router';
import Route from '../Route';
describe('transitionTo', function () {
+ beforeEach(resetHash);
+
var node;
beforeEach(function () {
node = document.createElement('div');
@@ -35,20 +39,14 @@ describe('transitionTo', function () {
},
function () {
expect(this.state.location.pathname).toEqual('/home/hi:there');
- done();
}
];
- function execNextStep() {
- try {
- steps.shift().apply(this, arguments);
- } catch (error) {
- done(error);
- }
- }
+ var execNextStep = execSteps(steps, done);
+ var history = createHistory();
- render((
-
+ React.render((
+
diff --git a/modules/getComponents.js b/modules/getComponents.js
new file mode 100644
index 0000000000..38d23d1f52
--- /dev/null
+++ b/modules/getComponents.js
@@ -0,0 +1,26 @@
+import { mapAsync } from './AsyncUtils';
+
+function getComponentsForRoute(route, callback) {
+ if (route.component || route.components) {
+ callback(null, route.component || route.components);
+ } else if (route.getComponents) {
+ route.getComponents(callback);
+ } else {
+ callback();
+ }
+}
+
+/**
+ * Asynchronously fetches all components needed for the given router
+ * state and calls callback(error, components) when finished.
+ *
+ * Note: This operation may finish synchronously if no routes have an
+ * asynchronous getComponents method.
+ */
+function getComponents(nextState, callback) {
+ mapAsync(nextState.routes, function (route, index, callback) {
+ getComponentsForRoute(route, callback);
+ }, callback);
+}
+
+export default getComponents;
diff --git a/modules/getRouteParams.js b/modules/getRouteParams.js
new file mode 100644
index 0000000000..d363971a98
--- /dev/null
+++ b/modules/getRouteParams.js
@@ -0,0 +1,22 @@
+import { getParamNames } from './PatternUtils';
+
+/**
+ * Extracts an object of params the given route cares about from
+ * the given params object.
+ */
+function getRouteParams(route, params) {
+ var routeParams = {};
+
+ if (!route.path)
+ return routeParams;
+
+ var paramNames = getParamNames(route.path);
+
+ for (var p in params)
+ if (params.hasOwnProperty(p) && paramNames.indexOf(p) !== -1)
+ routeParams[p] = params[p];
+
+ return routeParams;
+}
+
+export default getRouteParams;
diff --git a/modules/index.js b/modules/index.js
index 693f7a75b2..34a33fd620 100644
--- a/modules/index.js
+++ b/modules/index.js
@@ -8,7 +8,6 @@ export Route from './Route';
/* mixins */
export Navigation from './Navigation';
-export TransitionHook from './TransitionHook';
export State from './State';
/* utils */
diff --git a/modules/matchRoutes.js b/modules/matchRoutes.js
index 267412869f..059350d14e 100644
--- a/modules/matchRoutes.js
+++ b/modules/matchRoutes.js
@@ -51,17 +51,19 @@ function matchRouteDeep(basename, route, location, callback) {
var isExactMatch = remainingPathname === '';
if (isExactMatch && route.path) {
- var routes = [ route ];
- var params = createParams(paramNames, paramValues);
+ var match = {
+ routes: [ route ],
+ params: createParams(paramNames, paramValues)
+ };
getIndexRoute(route, location, function (error, indexRoute) {
if (error) {
callback(error);
} else {
if (indexRoute)
- routes.push(indexRoute);
+ match.routes.push(indexRoute);
- callback(null, { routes, params });
+ callback(null, match);
}
});
} else if (remainingPathname != null || route.childRoutes) {
@@ -79,8 +81,6 @@ function matchRouteDeep(basename, route, location, callback) {
} else if (match) {
// A child route matched! Augment the match and pass it up the stack.
match.routes.unshift(route);
- assignParams(match.params, paramNames, paramValues);
-
callback(null, match);
} else {
callback();
@@ -97,13 +97,13 @@ function matchRouteDeep(basename, route, location, callback) {
/**
* Asynchronously matches the given location to a set of routes and calls
- * callback(error, state) when finished. The state object may have the
+ * callback(error, state) when finished. The state object will have the
* following properties:
*
* - routes An array of routes that matched, in hierarchical order
* - params An object of URL parameters
*
- * Note: This operation may return synchronously if no routes have an
+ * Note: This operation may finish synchronously if no routes have an
* asynchronous getChildRoutes method.
*/
function matchRoutes(routes, location, callback, basename='') {
diff --git a/modules/runTransitionHooks.js b/modules/runTransitionHooks.js
new file mode 100644
index 0000000000..04399d4510
--- /dev/null
+++ b/modules/runTransitionHooks.js
@@ -0,0 +1,102 @@
+import { loopAsync } from './AsyncUtils';
+import { getParamNames } from './PatternUtils';
+
+function routeParamsChanged(route, prevState, nextState) {
+ if (!route.path)
+ return false;
+
+ var paramNames = getParamNames(route.path);
+
+ return paramNames.some(function (paramName) {
+ return prevState.params[paramName] !== nextState.params[paramName];
+ });
+}
+
+function createTransitionHook(fn, context) {
+ return function (a, b, callback) {
+ fn.apply(context, arguments);
+
+ if (fn.length < 3) {
+ // Assume fn executes synchronously and
+ // automatically call the callback.
+ callback();
+ }
+ };
+}
+
+function getTransitionHooksFromRoutes(routes, hookName) {
+ return routes.reduce(function (hooks, route) {
+ if (route[hookName])
+ hooks.push(createTransitionHook(route[hookName], route));
+
+ return hooks;
+ }, []);
+}
+
+function getTransitionHooks(prevState, nextState) {
+ var prevRoutes = prevState && prevState.routes;
+ var nextRoutes = nextState.routes;
+
+ var leaveRoutes, enterRoutes;
+ if (prevRoutes) {
+ leaveRoutes = prevRoutes.filter(function (route) {
+ return nextRoutes.indexOf(route) === -1 || routeParamsChanged(route, prevState, nextState);
+ });
+
+ // onLeave hooks start at the leaf route.
+ leaveRoutes.reverse();
+
+ enterRoutes = nextRoutes.filter(function (route) {
+ return prevRoutes.indexOf(route) === -1 || leaveRoutes.indexOf(route) !== -1;
+ });
+ } else {
+ leaveRoutes = [];
+ enterRoutes = nextRoutes;
+ }
+
+ var hooks = getTransitionHooksFromRoutes(leaveRoutes, 'onLeave');
+
+ hooks.push.apply(
+ hooks,
+ getTransitionHooksFromRoutes(enterRoutes, 'onEnter')
+ );
+
+ return hooks;
+}
+
+/**
+ * Compiles and runs an array of transition hook functions that
+ * should be called before we transition to a new state. Transition
+ * hook signatures are:
+ *
+ * - route.onEnter(nextState, redirectTo[, callback ])
+ * - route.onLeave(nextState, redirectTo[, callback ])
+ *
+ * Transition hooks run in order from the leaf route in the branch
+ * we're leaving, up the tree to the common parent route, and back
+ * down the branch we're entering to the leaf route.
+ *
+ * If a transition hook needs to execute asynchronously it may have
+ * a 3rd argument that it should call when it is finished. Otherwise
+ * the transition executes synchronously.
+ */
+function runTransitionHooks(prevState, nextState, callback) {
+ var hooks = getTransitionHooks(prevState, nextState);
+
+ var redirectInfo;
+ function redirectTo(pathname, query, state) {
+ redirectInfo = { pathname, query, state };
+ }
+
+ loopAsync(hooks.length, (index, next, done) => {
+ hooks[index](nextState, redirectTo, (error) => {
+ if (error || redirectInfo) {
+ done(error, redirectInfo); // No need to continue.
+ } else {
+ next();
+ }
+ });
+ }, callback);
+}
+
+export default runTransitionHooks;
diff --git a/package.json b/package.json
index c08135b92f..00095f5713 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
],
"license": "MIT",
"dependencies": {
- "history": "^1.0.0",
+ "history": "^1.1.0",
"invariant": "^2.0.0",
"keymirror": "^0.1.1",
"qs": "2.4.1",