diff --git a/examples/data-flow/app.js b/examples/data-flow/app.js index 8434f1b8d7..272dfa6c4c 100644 --- a/examples/data-flow/app.js +++ b/examples/data-flow/app.js @@ -6,6 +6,9 @@ var Routes = Router.Routes; var Link = Router.Link; var App = React.createClass({ + + mixins: [ Router.Transitions ], + getInitialState: function() { return { tacos: [ @@ -28,7 +31,7 @@ var App = React.createClass({ return taco.name != removedTaco; }); this.setState({tacos: tacos}); - Router.transitionTo('/'); + this.transitionTo('/'); }, render: function() { diff --git a/examples/master-detail/app.js b/examples/master-detail/app.js index 2b32092eca..653f76f0e7 100644 --- a/examples/master-detail/app.js +++ b/examples/master-detail/app.js @@ -132,6 +132,9 @@ var Index = React.createClass({ }); var Contact = React.createClass({ + + mixins: [ Router.Transitions ], + getStateFromStore: function(props) { props = props || this.props; return { @@ -164,7 +167,7 @@ var Contact = React.createClass({ destroy: function() { ContactStore.removeContact(this.props.params.id); - Router.transitionTo('/'); + this.transitionTo('/'); }, render: function() { @@ -182,14 +185,17 @@ var Contact = React.createClass({ }); var NewContact = React.createClass({ + + mixins: [ Router.Transitions ], + createContact: function(event) { event.preventDefault(); ContactStore.addContact({ first: this.refs.first.getDOMNode().value, last: this.refs.last.getDOMNode().value }, function(contact) { - Router.transitionTo('contact', { id: contact.id }); - }); + this.transitionTo('contact', { id: contact.id }); + }.bind(this)); }, render: function() { diff --git a/examples/transitions/app.js b/examples/transitions/app.js index d40e87e716..145136f61a 100644 --- a/examples/transitions/app.js +++ b/examples/transitions/app.js @@ -26,6 +26,9 @@ var Dashboard = React.createClass({ }); var Form = React.createClass({ + + mixins: [ Router.Transitions ], + statics: { willTransitionFrom: function(transition, component) { if (component.refs.userInput.getDOMNode().value !== '') { @@ -39,7 +42,7 @@ var Form = React.createClass({ handleSubmit: function(event) { event.preventDefault(); this.refs.userInput.getDOMNode().value = ''; - Router.transitionTo('/'); + this.transitionTo('/'); }, render: function() { diff --git a/modules/locations/HashLocation.js b/modules/locations/HashLocation.js index f6dc5a3d10..6e3876fa91 100644 --- a/modules/locations/HashLocation.js +++ b/modules/locations/HashLocation.js @@ -5,31 +5,35 @@ var LocationDispatcher = require('../dispatchers/LocationDispatcher'); var getWindowPath = require('../utils/getWindowPath'); function getHashPath() { - return window.location.hash.substr(1) || '/'; + return window.location.hash.substr(1); } +var _actionType; + function ensureSlash() { var path = getHashPath(); if (path.charAt(0) === '/') return true; - HashLocation.replace('/' + path, _actionSender); + HashLocation.replace('/' + path); return false; } -var _actionType, _actionSender; - function onHashChange() { if (ensureSlash()) { + var path = getHashPath(); + LocationDispatcher.handleViewAction({ - type: _actionType, - path: getHashPath(), - sender: _actionSender || window + // If we don't have an _actionType then all we know is the hash + // changed. It was probably caused by the user clicking the Back + // button, but may have also been the Forward button. + type: _actionType || LocationActions.POP, + path: getHashPath() }); - _actionSender = null; + _actionType = null; } } @@ -49,18 +53,19 @@ var HashLocation = { 'You cannot use HashLocation in an environment with no DOM' ); + ensureSlash(); + + LocationDispatcher.handleViewAction({ + type: LocationActions.SETUP, + path: getHashPath() + }); + if (window.addEventListener) { window.addEventListener('hashchange', onHashChange, false); } else { window.attachEvent('onhashchange', onHashChange); } - LocationDispatcher.handleViewAction({ - type: LocationActions.SETUP, - path: getHashPath(), - sender: window - }); - _isSetup = true; }, @@ -74,21 +79,18 @@ var HashLocation = { _isSetup = false; }, - push: function (path, sender) { + push: function (path) { _actionType = LocationActions.PUSH; - _actionSender = sender; window.location.hash = path; }, - replace: function (path, sender) { + replace: function (path) { _actionType = LocationActions.REPLACE; - _actionSender = sender; window.location.replace(getWindowPath() + '#' + path); }, - pop: function (sender) { + pop: function () { _actionType = LocationActions.POP; - _actionSender = sender; window.history.back(); }, diff --git a/modules/locations/HistoryLocation.js b/modules/locations/HistoryLocation.js index 90cdd9b0e1..7e4b5fbe0a 100644 --- a/modules/locations/HistoryLocation.js +++ b/modules/locations/HistoryLocation.js @@ -4,16 +4,11 @@ var LocationActions = require('../actions/LocationActions'); var LocationDispatcher = require('../dispatchers/LocationDispatcher'); var getWindowPath = require('../utils/getWindowPath'); -var _actionSender; - function onPopState() { LocationDispatcher.handleViewAction({ type: LocationActions.POP, - path: getWindowPath(), - sender: _actionSender || window + path: getWindowPath() }); - - _actionSender = null; } var _isSetup = false; @@ -32,18 +27,17 @@ var HistoryLocation = { 'You cannot use HistoryLocation in an environment with no DOM' ); + LocationDispatcher.handleViewAction({ + type: LocationActions.SETUP, + path: getWindowPath() + }); + if (window.addEventListener) { window.addEventListener('popstate', onPopState, false); } else { window.attachEvent('popstate', onPopState); } - LocationDispatcher.handleViewAction({ - type: LocationActions.SETUP, - path: getWindowPath(), - sender: window - }); - _isSetup = true; }, @@ -57,28 +51,25 @@ var HistoryLocation = { _isSetup = false; }, - push: function (path, sender) { + push: function (path) { window.history.pushState({ path: path }, '', path); LocationDispatcher.handleViewAction({ type: LocationActions.PUSH, - path: getWindowPath(), - sender: sender + path: getWindowPath() }); }, - replace: function (path, sender) { + replace: function (path) { window.history.replaceState({ path: path }, '', path); LocationDispatcher.handleViewAction({ type: LocationActions.REPLACE, - path: getWindowPath(), - sender: sender + path: getWindowPath() }); }, - pop: function (sender) { - _actionSender = sender; + pop: function () { window.history.back(); }, diff --git a/modules/locations/MemoryLocation.js b/modules/locations/MemoryLocation.js deleted file mode 100644 index 9908adc600..0000000000 --- a/modules/locations/MemoryLocation.js +++ /dev/null @@ -1,67 +0,0 @@ -var warning = require('react/lib/warning'); -var LocationActions = require('../actions/LocationActions'); -var LocationDispatcher = require('../dispatchers/LocationDispatcher'); - -var _lastPath = null; -var _currentPath = null; - -function getCurrentPath() { - return _currentPath || '/'; -} - -/** - * A Location that does not require a DOM. - */ -var MemoryLocation = { - - setup: function () { - LocationDispatcher.handleViewAction({ - type: LocationActions.SETUP, - path: getCurrentPath() - }); - }, - - push: function (path, sender) { - _lastPath = _currentPath; - _currentPath = path; - - LocationDispatcher.handleViewAction({ - type: LocationActions.PUSH, - path: getCurrentPath(), - sender: sender - }); - }, - - replace: function (path, sender) { - _currentPath = path; - - LocationDispatcher.handleViewAction({ - type: LocationActions.REPLACE, - path: getCurrentPath(), - sender: sender - }); - }, - - pop: function (sender) { - warning( - _lastPath != null, - 'You cannot use MemoryLocation to go back more than once' - ); - - _currentPath = _lastPath; - _lastPath = null; - - LocationDispatcher.handleViewAction({ - type: LocationActions.POP, - path: getCurrentPath(), - sender: sender - }); - }, - - toString: function () { - return ''; - } - -}; - -module.exports = MemoryLocation; diff --git a/modules/locations/RefreshLocation.js b/modules/locations/RefreshLocation.js index c930c6c08d..6e806639c0 100644 --- a/modules/locations/RefreshLocation.js +++ b/modules/locations/RefreshLocation.js @@ -19,8 +19,7 @@ var RefreshLocation = { LocationDispatcher.handleViewAction({ type: LocationActions.SETUP, - path: getWindowPath(), - sender: window + path: getWindowPath() }); }, diff --git a/modules/mixins/PathDelegate.js b/modules/mixins/PathDelegate.js index 0e31287563..e9c99b7795 100644 --- a/modules/mixins/PathDelegate.js +++ b/modules/mixins/PathDelegate.js @@ -2,6 +2,7 @@ var React = require('react'); var invariant = require('react/lib/invariant'); var PathState = require('./PathState'); var RouteContainer = require('./RouteContainer'); +var LocationActions = require('../actions/LocationActions'); var HashLocation = require('../locations/HashLocation'); var Path = require('../utils/Path'); @@ -62,17 +63,16 @@ var PathDelegate = { * Transitions to the URL specified in the arguments by pushing * a new URL onto the history stack. */ - transitionTo: function (to, params, query, sender) { - sender = sender || this; - + transitionTo: function (to, params, query) { var path = this.makePath(to, params, query); var location = this.getLocation(); - // If we have a location, route the transition through it. + // If we have a location, route the transition + // through it so the URL is updated as well. if (location) { - location.push(path, this); + location.push(path); } else if (this.updatePath) { - this.updatePath(path, this); + this.updatePath(path, LocationActions.PUSH); } }, @@ -80,26 +80,23 @@ var PathDelegate = { * Transitions to the URL specified in the arguments by replacing * the current URL in the history stack. */ - replaceWith: function (to, params, query, sender) { - sender = sender || this; - + replaceWith: function (to, params, query) { var path = this.makePath(to, params, query); var location = this.getLocation(); - // If we have a location, route the transition through it. + // If we have a location, route the transition + // through it so the URL is updated as well. if (location) { - location.replace(path, sender); + location.replace(path); } else if (this.updatePath) { - this.updatePath(path, sender); + this.updatePath(path, LocationActions.REPLACE); } }, /** * Transitions to the previous URL. */ - goBack: function (sender) { - sender = sender || this; - + goBack: function () { var location = this.getLocation(); invariant( @@ -107,7 +104,7 @@ var PathDelegate = { 'You cannot goBack without a location' ); - location.pop(sender); + location.pop(); } }; diff --git a/modules/mixins/PathState.js b/modules/mixins/PathState.js index a06d735a2a..78f7a88318 100644 --- a/modules/mixins/PathState.js +++ b/modules/mixins/PathState.js @@ -1,9 +1,11 @@ +var React = require('react'); +var invariant = require('react/lib/invariant'); var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; var HashLocation = require('../locations/HashLocation'); var HistoryLocation = require('../locations/HistoryLocation'); var RefreshLocation = require('../locations/RefreshLocation'); -var PathStore = require('../stores/PathStore'); var supportsHistory = require('../utils/supportsHistory'); +var PathStore = require('../stores/PathStore'); /** * A hash of { name: location } pairs. @@ -16,13 +18,12 @@ var NAMED_LOCATIONS = { /** * A mixin for components that need to know the current URL path. Components - * that use it may specify a `location` prop that they use to track changes - * to the URL. They also get: + * that use it get two things: * - * 1. An `updatePath` method that is called when the + * 1. An optional `location` prop that they use to track + * changes to the URL + * 2. An `updatePath` method that is called when the * current URL path changes - * 2. A `getCurrentPath` method they can use to get - * the current URL path * * Example: * @@ -30,15 +31,9 @@ var NAMED_LOCATIONS = { * * mixins: [ Router.PathState ], * - * getInitialState: function () { - * return { - * currentPath: this.getCurrentPath() - * }; - * }, - * - * updatePath: function () { + * updatePath: function (path, actionType) { * this.setState({ - * currentPath: this.getCurrentPath() + * currentPath: path * }); * } * @@ -48,6 +43,8 @@ var PathState = { propTypes: { + fixedPath: React.PropTypes.string, + location: function (props, propName, componentName) { var location = props[propName]; @@ -59,8 +56,8 @@ var PathState = { getDefaultProps: function () { return { - location: canUseDOM ? HashLocation : null, - path: null + fixedPath: null, + location: canUseDOM ? HashLocation : null }; }, @@ -84,11 +81,16 @@ var PathState = { componentWillMount: function () { var location = this.getLocation(); + invariant( + this.props.fixedPath == null || this.getLocation() == null, + 'You cannot use a fixed path with a location. Choose one or the other' + ); + if (location && location.setup) location.setup(); if (this.updatePath) - this.updatePath(this.getCurrentPath(), this); + this.updatePath(this.getCurrentPath(), this.getCurrentActionType()); }, componentDidMount: function () { @@ -99,16 +101,17 @@ var PathState = { PathStore.removeChangeListener(this.handlePathChange); }, - handlePathChange: function (sender) { + handlePathChange: function () { if (this.isMounted() && this.updatePath) - this.updatePath(this.getCurrentPath(), sender); + this.updatePath(this.getCurrentPath(), this.getCurrentActionType()); }, - /** - * Returns the current URL path. - */ getCurrentPath: function () { - return PathStore.getCurrentPath(); + return this.props.fixedPath || PathStore.getCurrentPath(); + }, + + getCurrentActionType: function () { + return PathStore.getCurrentActionType(); } }; diff --git a/modules/mixins/RouteLookup.js b/modules/mixins/RouteLookup.js index 00e29d5227..da919d92c4 100644 --- a/modules/mixins/RouteLookup.js +++ b/modules/mixins/RouteLookup.js @@ -18,6 +18,13 @@ var RouteLookup = { return this.context.routeContainer.getRoutes(); }, + /** + * See RouteContainer#getNamedRoutes. + */ + getNamedRoutes: function () { + return this.context.routeContainer.getNamedRoutes(); + }, + /** * See RouteContainer#getRouteByName. */ diff --git a/modules/mixins/ScrollDelegate.js b/modules/mixins/ScrollDelegate.js index d853f88eb8..53444b8975 100644 --- a/modules/mixins/ScrollDelegate.js +++ b/modules/mixins/ScrollDelegate.js @@ -24,13 +24,13 @@ var ScrollDelegate = { * Updates the current scroll position according to the last * one that was recorded for the given path. */ - updateScroll: function (path, sender) { + updateScroll: function (path, actionType) { if (this._scrollPositions) { var behavior = this.getScrollBehavior(); var position = this._scrollPositions[path]; if (behavior && position) - behavior.updateScrollPosition(position, sender); + behavior.updateScrollPosition(position, actionType); } } diff --git a/modules/mixins/ScrollState.js b/modules/mixins/ScrollState.js index 623c0154dc..4187e93def 100644 --- a/modules/mixins/ScrollState.js +++ b/modules/mixins/ScrollState.js @@ -1,5 +1,6 @@ -var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; var invariant = require('react/lib/invariant'); +var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; +var LocationActions = require('../actions/LocationActions'); /** * A scroll behavior that attempts to imitate the default behavior @@ -7,12 +8,15 @@ var invariant = require('react/lib/invariant'); */ var ImitateBrowserBehavior = { - updateScrollPosition: function (position, sender) { - if (sender === window) { - window.scrollTo(position.x, position.y); - } else { - // Clicking on links always scrolls the window to the top. - window.scrollTo(0, 0); + updateScrollPosition: function (position, actionType) { + switch (actionType) { + case LocationActions.PUSH: + case LocationActions.REPLACE: + window.scrollTo(0, 0); + break; + case LocationActions.POP: + window.scrollTo(position.x, position.y); + break; } } @@ -69,7 +73,7 @@ var ScrollState = { var behavior = this.props.scrollBehavior; if (typeof behavior === 'string') - behavior = NAMED_SCROLL_STRATEGIES[behavior]; + behavior = NAMED_SCROLL_BEHAVIORS[behavior]; return behavior; }, diff --git a/modules/mixins/TransitionHandler.js b/modules/mixins/TransitionHandler.js index 1c9d134f64..b8a6538a7d 100644 --- a/modules/mixins/TransitionHandler.js +++ b/modules/mixins/TransitionHandler.js @@ -335,7 +335,7 @@ var TransitionHandler = { /** * See PathState. */ - updatePath: function (path, sender) { + updatePath: function (path, actionType) { if (this.state.path === path) return; // Nothing to do! @@ -351,7 +351,7 @@ var TransitionHandler = { self.props.onAbortedTransition.call(self, transition); } else { self.emitChange(); - self.updateScroll(path, sender); + self.updateScroll(path, actionType); } }); }, diff --git a/modules/stores/PathStore.js b/modules/stores/PathStore.js index d4e93e95fb..39dae44b47 100644 --- a/modules/stores/PathStore.js +++ b/modules/stores/PathStore.js @@ -5,11 +5,11 @@ var LocationDispatcher = require('../dispatchers/LocationDispatcher'); var CHANGE_EVENT = 'change'; var _events = new EventEmitter; -function notifyChange(sender) { - _events.emit(CHANGE_EVENT, sender); +function notifyChange() { + _events.emit(CHANGE_EVENT); } -var _currentPath; +var _currentPath, _currentActionType; /** * The PathStore keeps track of the current URL path. @@ -31,6 +31,13 @@ var PathStore = { return _currentPath; }, + /** + * Returns the type of the action that changed the URL. + */ + getCurrentActionType: function () { + return _currentActionType; + }, + dispatchToken: LocationDispatcher.register(function (payload) { var action = payload.action; @@ -41,7 +48,8 @@ var PathStore = { case LocationActions.POP: if (_currentPath !== action.path) { _currentPath = action.path; - notifyChange(action.sender); + _currentActionType = action.type; + notifyChange(); } break; }