Skip to content

[added] <DefaultRoute> component #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 14, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DefaultRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./modules/components/DefaultRoute');
17 changes: 9 additions & 8 deletions examples/dynamic-segments/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ var Task = React.createClass({
});

var routes = (
<Routes>
<Route handler={App}>
<Route name="user" path="/user/:userId" handler={User}>
<Route name="task" path="/user/:userId/tasks/:taskId" handler={Task}/>
<Redirect from="/user/:userId/todos/:taskId" to="task"/>
</Route>
<Route handler={App}>
<Route name="user" path="/user/:userId" handler={User}>
<Route name="task" path="/user/:userId/tasks/:taskId" handler={Task}/>
<Redirect from="/user/:userId/todos/:taskId" to="task"/>
</Route>
</Routes>
</Route>
);

React.renderComponent(routes, document.getElementById('example'));
React.renderComponent(
<Routes children={routes}/>,
document.getElementById('example')
);
33 changes: 18 additions & 15 deletions examples/master-detail/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
var React = require('react');
var Router = require('../../index');
var Route = Router.Route;
var DefaultRoute = Router.DefaultRoute;
var Routes = Router.Routes;
var Link = Router.Link;

Expand Down Expand Up @@ -85,12 +86,10 @@ var App = React.createClass({
},

componentDidMount: function() {
console.log('componentDidMount')
ContactStore.addChangeListener(this.updateContacts);
},

componentWillUnmount: function () {
console.log('componentWillUnmount')
ContactStore.removeChangeListener(this.updateContacts);
},

Expand All @@ -104,10 +103,6 @@ var App = React.createClass({
});
},

indexTemplate: function() {
return <h1>Address Book</h1>;
},

render: function() {
var contacts = this.state.contacts.map(function(contact) {
return <li key={contact.id}><Link to="contact" id={contact.id}>{contact.first}</Link></li>
Expand All @@ -121,13 +116,19 @@ var App = React.createClass({
</ul>
</div>
<div className="Content">
{this.props.activeRouteHandler() || this.indexTemplate()}
{this.props.activeRouteHandler()}
</div>
</div>
);
}
});

var Index = React.createClass({
render: function() {
return <h1>Address Book</h1>;
}
});

var Contact = React.createClass({
getInitialState: function() {
return {
Expand Down Expand Up @@ -204,16 +205,18 @@ var NotFound = React.createClass({
});

var routes = (
<Routes>
<Route handler={App}>
<Route name="new" path="contact/new" handler={NewContact}/>
<Route name="not-found" path="contact/not-found" handler={NotFound}/>
<Route name="contact" path="contact/:id" handler={Contact}/>
</Route>
</Routes>
<Route handler={App}>
<DefaultRoute handler={Index}/>
<Route name="new" path="contact/new" handler={NewContact}/>
<Route name="not-found" path="contact/not-found" handler={NotFound}/>
<Route name="contact" path="contact/:id" handler={Contact}/>
</Route>
);

React.renderComponent(routes, document.getElementById('example'));
React.renderComponent(
<Routes children={routes}/>,
document.getElementById('example')
);

// Request utils.

Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
exports.ActiveState = require('./ActiveState');
exports.AsyncState = require('./AsyncState');
exports.DefaultRoute = require('./DefaultRoute');
exports.Link = require('./Link');
exports.Redirect = require('./Redirect');
exports.Route = require('./Route');
Expand Down
19 changes: 19 additions & 0 deletions modules/components/DefaultRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
var copyProperties = require('react/lib/copyProperties');
var Route = require('./Route');

/**
* A <DefaultRoute> component is a special kind of <Route> that
* renders when its parent matches but none of its siblings do.
* Only one such route may be used at any given level in the
* route hierarchy.
*/
function DefaultRoute(props) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need @spicyj to show us how to do this properly before the next release of react, apparently we are cheating here and need to make components, not just functions that can trick JSX, but for now :shipit:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you by chance mean that something like <Route to='somewhere' handler={ShellHandler} defaultHandler={DefaultHandler} /> would be more correct instead? Definitely easer then doing checks to make sure there is only one DefaultRoute child.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean how do we make a real component out of <DefaultRoute/> instead of just a function that jsx thinks is a component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, thanks for the clarification.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can imagine that <DefaultRoute handler={Foo} /> will be compiled into {type: DefaultRoute, props: {handler: Foo}}. I am pretty sure that RegisterRoute will just have to understand what DefaultRoute is.

return Route(
copyProperties(props, {
name: null,
path: null
})
);
}

module.exports = DefaultRoute;
2 changes: 2 additions & 0 deletions modules/components/Route.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var withoutProperties = require('../helpers/withoutProperties');
var RESERVED_PROPS = {
handler: true,
path: true,
defaultRoute: true,
paramNames: true,
children: true // ReactChildren
};

Expand Down
89 changes: 39 additions & 50 deletions modules/components/Routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,20 @@ var Routes = React.createClass({
},

getInitialState: function () {
return {};
return {
routes: this.getRoutes()
};
},

getRoutes: function () {
var routes = [];

React.Children.forEach(this.props.children, function (child) {
if (child = RouteStore.registerRoute(child, this))
routes.push(child);
}, this);

return routes;
},

getLocation: function () {
Expand All @@ -107,12 +120,7 @@ var Routes = React.createClass({
},

componentWillMount: function () {
React.Children.forEach(this.props.children, function (child) {
RouteStore.registerRoute(child);
});

PathStore.setup(this.getLocation());

PathStore.addChangeListener(this.handlePathChange);
},

Expand Down Expand Up @@ -146,15 +154,7 @@ var Routes = React.createClass({
* { route: <PostRoute>, params: { id: '123' } } ]
*/
match: function (path) {
var rootRoutes = this.props.children;
if (!Array.isArray(rootRoutes)) {
rootRoutes = [rootRoutes];
}
var matches = null;
for (var i = 0; matches == null && i < rootRoutes.length; i++) {
matches = findMatches(Path.withoutQuery(path), rootRoutes[i]);
}
return matches;
return findMatches(Path.withoutQuery(path), this.state.routes, this.props.defaultRoute);
},

/**
Expand Down Expand Up @@ -229,53 +229,42 @@ var Routes = React.createClass({

});

function findMatches(path,route){
var matches = null;
function findMatches(path, routes, defaultRoute) {
var matches = null, route, params;

if (Array.isArray(route)) {
for (var i = 0, len = route.length; matches == null && i < len; ++i) {
matches = findMatches(path, route[i]);
}
} else {
matches = findMatchesForRoute(path,route);
}

return matches;
}
for (var i = 0, len = routes.length; i < len; ++i) {
route = routes[i];

function findMatchesForRoute(path, route) {
var children = route.props.children, matches;
var params;
// Check the subtree first to find the most deeply-nested match.
matches = findMatches(path, route.props.children, route.props.defaultRoute);

// Check the subtree first to find the most deeply-nested match.
if (Array.isArray(children)) {
for (var i = 0, len = children.length; matches == null && i < len; ++i) {
matches = findMatches(path, children[i]);
}
} else if (children) {
matches = findMatches(path, children);
}
if (matches != null) {
var rootParams = getRootMatch(matches).params;

params = route.props.paramNames.reduce(function (params, paramName) {
params[paramName] = rootParams[paramName];
return params;
}, {});

if (matches) {
var rootParams = getRootMatch(matches).params;
params = {};
matches.unshift(makeMatch(route, params));

Path.extractParamNames(route.props.path).forEach(function (paramName) {
params[paramName] = rootParams[paramName];
});
return matches;
}

matches.unshift(makeMatch(route, params));
// No routes in the subtree matched, so check this route.
params = Path.extractParams(route.props.path, path);

return matches;
if (params)
return [ makeMatch(route, params) ];
}

// No routes in the subtree matched, so check this route.
params = Path.extractParams(route.props.path, path);
// No routes matched, so try the default route if there is one.
params = defaultRoute && Path.extractParams(defaultRoute.props.path, path);

if (params)
return [ makeMatch(route, params) ];
return [ makeMatch(defaultRoute, params) ];

return null;
return matches;
}

function makeMatch(route, params) {
Expand Down
88 changes: 57 additions & 31 deletions modules/stores/RouteStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,63 +26,89 @@ var RouteStore = {
* from the store.
*/
unregisterRoute: function (route) {
if (route.props.name)
delete _namedRoutes[route.props.name];
var props = route.props;

React.Children.forEach(route.props.children, function (child) {
RouteStore.unregisterRoute(child);
});
if (props.name)
delete _namedRoutes[props.name];

React.Children.forEach(props.children, RouteStore.unregisterRoute);
},

/**
* Registers a <Route> and all of its children with the store. Also,
* does some normalization and validation on route props.
*/
registerRoute: function (route, _parentRoute) {
// Make sure the <Route>'s path begins with a slash. Default to its name.
// We can't do this in getDefaultProps because it may not be called on
// <Route>s that are never actually mounted.
if (route.props.path || route.props.name) {
route.props.path = Path.normalize(route.props.path || route.props.name);
} else {
route.props.path = '/';
}
// Note: When route is a top-level route, _parentRoute
// is actually a <Routes>, not a <Route>. We do this so
// <Routes> can get a defaultRoute like <Route> does.
var props = route.props;

// Make sure the <Route> has a valid React component for a handler.
invariant(
React.isValidClass(route.props.handler),
'The handler for Route "' + (route.props.name || route.props.path) + '" ' +
'must be a valid React component'
React.isValidClass(props.handler),
'The handler for the "%s" route must be a valid React class',
props.name || props.path
);

// Make sure the <Route> has all params that its parent needs.
if (_parentRoute) {
var paramNames = Path.extractParamNames(route.props.path);
// Default routes have no name, path, or children.
var isDefault = !(props.path || props.name || props.children);

Path.extractParamNames(_parentRoute.props.path).forEach(function (paramName) {
if (props.path || props.name) {
props.path = Path.normalize(props.path || props.name);
} else if (_parentRoute && _parentRoute.props.path) {
props.path = _parentRoute.props.path;
} else {
props.path = '/';
}

props.paramNames = Path.extractParamNames(props.path);

// Make sure the route's path has all params its parent needs.
if (_parentRoute && Array.isArray(_parentRoute.props.paramNames)) {
_parentRoute.props.paramNames.forEach(function (paramName) {
invariant(
paramNames.indexOf(paramName) !== -1,
'The nested route path "' + route.props.path + '" is missing the "' + paramName + '" ' +
'parameter of its parent path "' + _parentRoute.props.path + '"'
props.paramNames.indexOf(paramName) !== -1,
'The nested route path "%s" is missing the "%s" parameter of its parent path "%s"',
props.path, paramName, _parentRoute.props.path
);
});
}

// Make sure the <Route> can be looked up by <Link>s.
if (route.props.name) {
var existingRoute = _namedRoutes[route.props.name];
// Make sure the route can be looked up by <Link>s.
if (props.name) {
var existingRoute = _namedRoutes[props.name];

invariant(
!existingRoute || route === existingRoute,
'You cannot use the name "' + route.props.name + '" for more than one route'
'You cannot use the name "%s" for more than one route',
props.name
);

_namedRoutes[route.props.name] = route;
_namedRoutes[props.name] = route;
}

React.Children.forEach(route.props.children, function (child) {
RouteStore.registerRoute(child, route);
if (_parentRoute && isDefault) {
invariant(
_parentRoute.props.defaultRoute == null,
'You may not have more than one <DefaultRoute> per <Route>'
);

_parentRoute.props.defaultRoute = route;

return null;
}

// Make sure children is an array, excluding <DefaultRoute>s.
var children = [];

React.Children.forEach(props.children, function (child) {
if (child = RouteStore.registerRoute(child, route))
children.push(child);
});

props.children = children;

return route;
},

/**
Expand Down
Loading