Skip to content

Commit 41056c8

Browse files
authored
Merge pull request #1 from nealgranger/route-rewrite
Use Redux store for components defined on routes.
2 parents 1997c26 + 31e4136 commit 41056c8

9 files changed

Lines changed: 304 additions & 155 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
},
1616
"license": "CC0-1.0",
1717
"dependencies": {
18+
"core-js": "^2.4.1",
1819
"cuid": "^1.3.8",
20+
"fbjs": "^0.8.4",
1921
"hoist-non-react-statics": "^1.2.0"
2022
},
2123
"peerDependencies": {
2224
"react": "^15.3.0",
2325
"react-redux": "^4.4.5",
24-
"react-router": "^2.7.0",
2526
"redux": "^3.5.2"
2627
},
2728
"devDependencies": {

src/action.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import cuid from 'cuid';
22

3-
export const ADD = '@@relocation/ADD';
4-
export const REMOVE = '@@relocation/REMOVE';
5-
export const UPDATE = '@@relocation/UPDATE';
3+
export const ADD_COMPONENT = '@@relocation/ADD_COMPONENT';
4+
export const REMOVE_COMPONENT = '@@relocation/REMOVE_COMPONENT';
5+
export const SET_PREVIOUS_PATH = '@@relocation/SET_PREVIOUS_PATH';
6+
export const SET_ROUTE_COMPONENTS = '@@relocation/SET_ROUTE_COMPONENTS';
67

7-
export const add = ({type, props, id = cuid()}) => ({
8-
type: ADD,
8+
export const addComponent = ({type, props, id = cuid()}) => ({
9+
type: ADD_COMPONENT,
910
payload: {id, type, props},
1011
});
1112

12-
export const remove = (id) => ({type: REMOVE, payload: id});
13+
export const removeComponent = (id) => ({type: REMOVE_COMPONENT, payload: id});
1314

14-
export const update = (update) => ({
15-
type: UPDATE,
16-
payload: update,
15+
export const setPreviousPath = (path) => ({
16+
type: SET_PREVIOUS_PATH,
17+
payload: path,
18+
});
19+
20+
export const setRouteComponents = (components) => ({
21+
type: SET_ROUTE_COMPONENTS,
22+
payload: components,
1723
});

src/connect.js

Lines changed: 145 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,168 @@ import {Component, createElement, PropTypes} from 'react';
22
import hoistStatics from 'hoist-non-react-statics';
33
import {connect} from 'react-redux';
44

5-
import {getComponents} from './selector';
6-
import {remove} from './action';
7-
import {contextShape, componentShape, getDisplayName} from './util';
5+
import {getMergedComponents, getPreviousPath} from './selector';
6+
import {removeComponent} from './action';
7+
import {componentsShape, renderMapShape, getDisplayName} from './util';
88

9-
export default (renderMap) => (WrappedComponent) => {
9+
/**
10+
* Create a higher-order wrapper which provides an array of components to render
11+
* to its wrapped instance.
12+
*
13+
* @param {Object|Function} rawRenderMap An object with component type/render
14+
* function key value pairs or a function returning such an object.
15+
* @param {Object|Function} rawConfig An object or a function returing such an
16+
* object.
17+
* @returns {Function} Higher-order component wrapper.
18+
*/
19+
export default (rawRenderMap, rawConfig = {}) => (WrappedComponent) => {
1020
class Connect extends Component {
1121
static propTypes = {
12-
list: PropTypes.arrayOf(componentShape),
22+
dispatch: PropTypes.func.isRequired,
23+
___relocation___: PropTypes.shape({
24+
components: componentsShape.isRequired,
25+
previousPath: PropTypes.string.isRequired,
26+
config: PropTypes.object.isRequired,
27+
renderMap: renderMapShape.isRequired,
28+
}).isRequired,
1329
};
1430

1531
static contextTypes = {
16-
relocation: contextShape,
17-
};
32+
router: PropTypes.object,
33+
}
1834

19-
static defaultProps = {
20-
list: [],
21-
};
35+
navigateToPath(path, {useHistoryBack = true} = {}) {
36+
// Check for the react-router context.
37+
if (!this.context.router) {
38+
return;
39+
}
40+
41+
const {previousPath} = this.props.___relocation___;
42+
43+
// The `useHistoryBack` option will trigger the use of the `goBack` method
44+
// instead of `push` in an effort to keep the if the requested path is
45+
// equal to the previous path.
46+
//
47+
// This is useful in scenraios where component that is displayed in
48+
// response to a route change is considered dismissed or completed on
49+
// removal.
50+
if (useHistoryBack && path === previousPath) {
51+
this.context.router.goBack();
52+
} else {
53+
this.context.router.push(path);
54+
}
55+
}
56+
57+
removeComponent(id/* , options */) {
58+
return this.props.dispatch(removeComponent(id));
59+
}
2260

2361
render() {
62+
const {components, renderMap} = this.props.___relocation___;
63+
64+
const assignRender = (component) => ({
65+
...component,
66+
render: renderMap[component.type],
67+
});
68+
69+
const inRenderMap = (component) =>
70+
typeof renderMap[component.type] === 'function';
71+
72+
const assignRemoveHandler = (component) => {
73+
let removeHandler = null;
74+
75+
if (typeof component.remove === 'function') {
76+
// The component object remove property is already a function.
77+
// We don't want to override this behavior.
78+
removeHandler = component.remove;
79+
} else if (component.remove === undefined || component.remove) {
80+
// The component object does not have a `remove` property, or it has
81+
// a truthy value that is not a function. Either case indicates that
82+
// it should use the default remove handler.
83+
removeHandler = (options) =>
84+
this.removeComponent(component.id, options);
85+
}
86+
87+
let pathRemoveHandler = null;
88+
89+
if (typeof component.removePath === 'string') {
90+
// Create a function that will change the history state when removing
91+
// the component.
92+
pathRemoveHandler = (options) =>
93+
this.navigateToPath(component.removePath, options);
94+
}
95+
96+
if (pathRemoveHandler && removeHandler) {
97+
// A remove handler function and a
98+
return {
99+
...component,
100+
remove: (options) => {
101+
pathRemoveHandler(options);
102+
return removeHandler(options);
103+
},
104+
};
105+
}
106+
107+
if (pathRemoveHandler && !removeHandler) {
108+
return {
109+
...component,
110+
remove: pathRemoveHandler,
111+
};
112+
}
113+
114+
if (!pathRemoveHandler && removeHandler !== component.remove) {
115+
return {
116+
...component,
117+
remove: removeHandler,
118+
};
119+
}
120+
121+
// `!pathRemoveHandler && removeHandler === component.remove` is true.
122+
// This means `remove` was set and `removePath` was not set on the
123+
// component object. No modification is necessary.
124+
return component;
125+
};
126+
24127
const mergedProps = {
25128
...this.props,
26-
components: [
27-
// Prepend components from the context to the list of components
28-
// collected from the redux store.
29-
...this.context.relocation.components || [],
30-
31-
...this.props.components
32-
.map((item) => ({
33-
// Assign default remove functions.
34-
remove: () => this.props.remove(item.id),
35-
...item,
36-
})),
37-
]
38-
// Remove components not included in the render function map.
39-
.filter((item) => renderMap.hasOwnProperty(item.type))
40-
129+
components: components
41130
// Assign render functions.
42-
.map((item) => ({
43-
render: renderMap[item.type],
44-
...item,
45-
})),
131+
.map(assignRender)
132+
// Remove components not included in the render function map.
133+
.filter(inRenderMap)
134+
// Assign remove handler functions.
135+
.map(assignRemoveHandler),
46136
};
47137

48-
return createElement(WrappedComponent, mergedProps);
138+
return <WrappedComponent {...mergedProps}/>;
49139
}
50140
}
51141

52142
Connect.displayName = `Relocation(${getDisplayName(WrappedComponent)})`;
53143

54-
const mapState = (state, props) => ({
55-
components: getComponents(state, props),
56-
});
57-
const mapDispatch = (dispatch) => ({
58-
remove: (id) => dispatch(remove(id)),
59-
});
60-
61-
return connect(
62-
mapState,
63-
mapDispatch
64-
)(hoistStatics(Connect, WrappedComponent));
144+
const mapState = (state, props) => {
145+
const config = typeof rawConfig === 'function' ?
146+
rawConfig(props) : rawConfig;
147+
148+
const renderMap = typeof rawRenderMap === 'function' ?
149+
rawRenderMap(props) : rawRenderMap;
150+
151+
const {getRelocationState} = config;
152+
153+
const selectorProps = getRelocationState ?
154+
{getRelocationState, ...props} : props;
155+
156+
return {
157+
// Put everything in a ___relocation___ namespace to avoid possible
158+
// conflict with existing props.
159+
___relocation___: {
160+
components: getMergedComponents(state, selectorProps),
161+
previousPath: getPreviousPath(state, selectorProps),
162+
config,
163+
renderMap,
164+
},
165+
};
166+
};
167+
168+
return connect(mapState)(hoistStatics(Connect, WrappedComponent));
65169
};

src/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
export {ADD, REMOVE, UPDATE, add, remove, update} from './action';
1+
export {ADD_COMPONENT, REMOVE_COMPONENT} from './action';
2+
export {addComponent, removeComponent} from './action';
23
export {default as relocation} from './connect';
3-
export {default as RelocationProvider} from './provider';
44
export {default as reducer} from './reducer';
5+
export {default as createRelocationRouter} from './router';
56
export {default} from './connect';

src/provider.js

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/reducer.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import {combineReducers} from 'redux';
2-
import {REMOVE, ADD, UPDATE} from './action';
2+
import {
3+
REMOVE_COMPONENT,
4+
ADD_COMPONENT,
5+
SET_PREVIOUS_PATH,
6+
SET_ROUTE_COMPONENTS,
7+
} from './action';
8+
9+
const createReducer = (type, initial) =>
10+
(state = initial, action) => action.type === type ? action.payload : state;
311

412
export default combineReducers({
513
components: (state = [], action) => ({
6-
[ADD]: (state, {payload}) => state
7-
.filter((item) => item.id !== payload)
8-
.concat([payload]),
9-
[REMOVE]: (state, {payload}) => state
10-
.filter((item) => item.id !== payload),
11-
[UPDATE]: (state, {payload}) => state
12-
.map((item) => item.id === payload.id ? {...item, ...payload} : item),
14+
[ADD_COMPONENT]: (state, {payload}) => [...state, payload],
15+
[REMOVE_COMPONENT]: (state, {payload}) =>
16+
state.filter((item) => item.id !== payload),
1317
}[action.type] || (() => state))(state, action),
18+
routeComponents: createReducer(SET_ROUTE_COMPONENTS, []),
19+
previousPath: createReducer(SET_PREVIOUS_PATH, ''),
1420
});

0 commit comments

Comments
 (0)