Skip to content
This repository has been archived by the owner on Dec 15, 2018. It is now read-only.

Dynamic fragments #189

Merged
merged 2 commits into from
Jun 19, 2017
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
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"presets": ["es2015", "react", "stage-2"],
"plugins": [
["lodash", { "id": ["lodash", "recompose"] }]
],
"env": {
"coverage": {
"plugins": [ [ "istanbul", { "ignore": "test" } ] ]
Expand Down
58 changes: 3 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ goBack();
goForward();
```

These actions will be performed once dispatched, e.g. redirect using a [thunk](https://github.com/gaearon/redux-thunk):
These actions will execute once dispatched. For example, here's how to redirect using a [thunk](https://github.com/gaearon/redux-thunk):

```js
import { push } from 'redux-little-router';
Expand Down Expand Up @@ -226,10 +226,9 @@ Your custom reducers or selectors can derive a large portion of your app's state
`redux-little-router` provides the following to make React integration easier:

- A `<Fragment>` component that conditionally renders children based on current route and/or location conditions.
- A `<Link>` component that sends navigation actions to the middleware when tapped or clicked. `<Link>` respects default modifier key and right-click behavior. A sibling component, `<PersistentQueryLink>`, persists the existing query string on navigation
- A `provideRouter` HOC that passes down everything `<Fragment>` and `<Link>` need via context.
- A `<Link>` component that sends navigation actions to the middleware when tapped or clicked. `<Link>` respects default modifier key and right-click behavior. A sibling component, `<PersistentQueryLink>`, persists the existing query string on navigation.

`redux-little-router` assumes and requires that your root component is a direct or indirect child of `<Provider>` from `react-redux`. Both `provideRouter` and `<RouterProvider>` automatically `connect()` to updates from the router state.
Instances of each component automatically `connect()` to the router state with `react-redux`.

You can inspect the router state in any child component by using `connect()`:

Expand Down Expand Up @@ -334,57 +333,6 @@ Alternatively, you can pass in a location object to `href`. This is useful for p

`<Link>` takes an optional valueless prop, `replaceState`, that changes the link navigation behavior from `pushState` to `replaceState` in the History API.

### `provideRouter` or `<RouterProvider>`

Like React Router's `<Router>` component, you'll want to wrap `provideRouter` around your app's top-level component like so:

```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { provideRouter } from 'redux-little-router';
import YourAppComponent from './';

import createYourStore from './state';

const AppComponentWithRouter = provideRouter({
store: createYourStore()
})(YourAppComponent);

ReactDOM.render(
<Provider store={store}>
<AppComponentWithRouter />
</Provider>,
document.getElementById('root')
);
```

This allows `<Fragment>` and `<Link>` to obtain their `history` and `dispatch` instances without manual prop passing.

If you'd rather use a plain component instead of a higher-ordered component, use `<RouterProvider>` like so:

```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { RouterProvider } from 'redux-little-router';
import YourAppComponent from './';

import createYourStore from './state';
const store = createYourStore();

ReactDOM.render(
<Provider store={store}>
<RouterProvider store={store}>
<YourAppComponent />
</RouterProvider>
</Provider>,
document.getElementById('root')
);
```

Note: Provider and RouteProvider can wrap in either order, it is only important that they are parent to your app component.

## Environment

`redux-little-router` requires an ES5 compatible environment (no IE8).
Expand Down
14 changes: 9 additions & 5 deletions demo/client/demo.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-magic-numbers */
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import chunk from 'lodash.chunk';
import { Link, Fragment } from '../../src';
Expand Down Expand Up @@ -35,7 +36,7 @@ Gallery.propTypes = {
};

// eslint-disable-next-line react/no-multi-comp
const Demo = ({ router }) => {
const Demo = ({ location }) => {
const demoRoutes = ['/cheese', '/cat', '/dog', '/hipster'];
return (
<div className={styles.container}>
Expand All @@ -60,9 +61,9 @@ const Demo = ({ router }) => {
{demoRoutes.map(route => (
<Fragment key={route} forRoute={route}>
<div>
<p>{router.result.text}</p>
<p>{location.result.text}</p>
<Gallery
images={router.result.images}
images={location.result.images}
columns={COLUMN_COUNT}
/>
</div>
Expand All @@ -76,7 +77,10 @@ const Demo = ({ router }) => {
};

Demo.propTypes = {
router: PropTypes.object
location: PropTypes.object
};

export default Demo;
const mapStateToProps = state => ({
location: state.router
});
export default connect(mapStateToProps)(Demo);
5 changes: 1 addition & 4 deletions demo/client/wrap.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import { RouterProvider } from '../../src';

export default store => Root =>
<Provider store={store}>
<RouterProvider store={store}>
<Root />
</RouterProvider>
<Root />
</Provider>;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"lodash.assign": "^4.2.0",
"prop-types": "^15.5.8",
"query-string": "^4.3.2",
"recompose": "^0.23.5",
"url-pattern": "^1.0.3"
},
"devDependencies": {
Expand All @@ -56,6 +57,7 @@
"babel-eslint": "^7.2.0",
"babel-loader": "^6.4.1",
"babel-plugin-istanbul": "^4.1.1",
"babel-plugin-lodash": "^3.2.11",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.0",
"babel-preset-react": "^6.23.0",
Expand Down
213 changes: 118 additions & 95 deletions src/components/fragment.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,67 @@
// @flow
/* eslint-disable react/sort-comp */
import type { Location } from '../types';

import UrlPattern from 'url-pattern';
import React, { Children, Component } from 'react';
import { connect } from 'react-redux';
import { compose, withContext, getContext } from 'recompose';
import PropTypes from 'prop-types';

import matchCache from '../util/match-cache';
import generateId from '../util/generate-id';

const withId = ComposedComponent =>
class WithId extends Component {
id: string;

constructor() {
super();
this.id = generateId();
}

render() {
return <ComposedComponent {...this.props} id={ this.id } />;
}
};

const resolveChildRoute = (parentRoute, currentRoute) => {
const parentIsRootRoute =
parentRoute &&
parentRoute !== '/' &&
parentRoute !== currentRoute;

return parentIsRootRoute
? `${parentRoute}${currentRoute || ''}`
: currentRoute;
};

const resolveCurrentRoute = (parentRoute, currentRoute) => {
if (!currentRoute) { return null; }

// First route will always be a wildcard
if (!parentRoute) { return `${currentRoute}*`; }

const currentIsRootRoute = currentRoute === '/';
const parentIsRootRoute = parentRoute === '/';

// Only prefix non-root parent routes
const routePrefix = !parentIsRootRoute && parentRoute || '';

// Support "index" routes:
// <Fragment forRoute='/home'>
// <Fragment forRoute='/'>
// </Fragment>
// </Fragment>
const routeSuffix = currentIsRootRoute &&
!parentIsRootRoute ? '' : currentRoute;

const wildcard = currentIsRootRoute &&
parentIsRootRoute ? '' : '*';

return `${routePrefix}${routeSuffix}${wildcard}`;
};

type Props = {
location: Location,
matchRoute: Function,
Expand All @@ -17,111 +73,78 @@ type Props = {
children: React.Element<*>
};

const relativePaths = (ComposedComponent: ReactClass<*>) => {
class RelativeFragment extends Component {
constructor() {
super();
this.id = generateId();
}
class Fragment extends Component {
matcher: ?Object;

getChildContext() {
const { parentRoute } = this.context;
const { forRoute } = this.props;

return {
// Append the parent route if this isn't the first
// RelativeFragment in the hierarchy.
parentRoute: parentRoute &&
parentRoute !== '/' &&
parentRoute !== forRoute
? `${parentRoute}${forRoute || ''}`
: forRoute,
parentId: this.id
};
}
constructor(props: Props, context: typeof Fragment.contextTypes) {
super(props, context);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe you need context to be declared in super() if it's not being used in the constructor

Copy link
Contributor

Choose a reason for hiding this comment

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

You also don't need to pass props, unless you're accessing this.props in the constructor. React will populate them both on the instance for you.


props: Props;
id: string;
const currentRoute = resolveCurrentRoute(
props.parentRoute,
props.forRoute
);

render() {
const { children, forRoute, ...rest } = this.props;
const { router, parentRoute, parentId } = this.context;
const { store } = router;

const location = store.getState().router;

const routePrefix = parentRoute && parentRoute !== '/'
? parentRoute : '';

const routeSuffix = (forRoute === '/' && parentRoute && parentRoute !== '/')
? '' : forRoute || '';

const combinedRoute = forRoute && `${routePrefix}${routeSuffix}`;

return (
<ComposedComponent
parentId={parentId}
location={location}
matchRoute={store.matchRoute}
matchWildcardRoute={store.matchWildcardRoute}
parentRoute={parentRoute}
forRoute={combinedRoute}
children={children}
{...rest}
/>
);
}
this.matcher = currentRoute &&
new UrlPattern(currentRoute) || null;
}

// Consumes this context...
RelativeFragment.contextTypes = {
router: PropTypes.object,
parentRoute: PropTypes.string,
parentId: PropTypes.string
};
componentWillReceiveProps(nextProps: Props) {
if (this.props.forRoute !== nextProps.forRoute) {
throw new Error('Updating route props is not yet supported.');
}
}

// ...and provides this context.
RelativeFragment.childContextTypes = {
parentRoute: PropTypes.string,
parentId: PropTypes.string
};
render() {
const { matcher } = this;
const {
children,
forRoute,
withConditions,
location,
parentRoute,
parentId
} = this.props;

return RelativeFragment;
};
const currentRoute = resolveCurrentRoute(parentRoute, forRoute);

const Fragment = (props: Props) => {
const {
location,
matchRoute,
matchWildcardRoute,
forRoute,
withConditions,
children,
parentId,
parentRoute
} = props;

const matcher = (forRoute === parentRoute) ? matchRoute : matchWildcardRoute;
const matchResult = matcher(location.pathname, forRoute);

if (
!matchResult ||
(withConditions && !withConditions(location)) ||
(forRoute && matchResult.route !== forRoute)
) {
return null;
}
if (matcher && !matcher.match(location.pathname)) {
return null;
}

if (parentId) {
const previousMatch = matchCache.get(parentId);
if (previousMatch && previousMatch !== forRoute) {
if (withConditions && !withConditions(location)) {
return null;
} else {
matchCache.add(parentId, forRoute);
}
}

return Children.only(children);
};
if (parentId) {
const previousMatch = matchCache.get(parentId);
if (previousMatch && previousMatch !== currentRoute) {
return null;
} else {
matchCache.add(parentId, currentRoute);
}
}

export default relativePaths(Fragment);
return Children.only(children);
}
}

export default compose(
Copy link
Contributor

Choose a reason for hiding this comment

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

this is some next-level composition 馃敟

connect(state => ({
location: state.router
})),
getContext({
parentRoute: PropTypes.string,
parentId: PropTypes.string
}),
withId,
withContext({
parentRoute: PropTypes.string,
parentId: PropTypes.string
}, props => ({
parentRoute: resolveChildRoute(
props.parentRoute,
props.forRoute
),
parentId: props.id
}))
)(Fragment);
Loading