Skip to content
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: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
},
"license": "CC0-1.0",
"dependencies": {
"core-js": "^2.4.1",
"cuid": "^1.3.8",
"fbjs": "^0.8.4",
"hoist-non-react-statics": "^1.2.0"
Expand Down
21 changes: 15 additions & 6 deletions src/action.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import cuid from 'cuid';

export const ADD_COMPONENT = '@@relocation/ADD_COMPONENT';
export const SET_COMPONENT = '@@relocation/SET_COMPONENT';
export const UPDATE_COMPONENT = '@@relocation/UPDATE_COMPONENT';
export const REMOVE_COMPONENT = '@@relocation/REMOVE_COMPONENT';
export const SET_PREVIOUS_PATH = '@@relocation/SET_PREVIOUS_PATH';
export const SET_ROUTE_COMPONENTS = '@@relocation/SET_ROUTE_COMPONENTS';

export const addComponent = ({type, props, id = cuid()}) => ({
export const addComponent = (type, props) => ({
type: ADD_COMPONENT,
payload: {id: cuid(), type, props},
});

export const setComponent = (type, id = type, props) => ({
type: SET_COMPONENT,
payload: {id, type, props},
});

export const removeComponent = (id) => ({type: REMOVE_COMPONENT, payload: id});
export const updateComponent = (id, props) => ({
type: UPDATE_COMPONENT,
payload: {id, props},
});

export const setPreviousPath = (path) => ({
type: SET_PREVIOUS_PATH,
payload: path,
export const removeComponent = (id) => ({
type: REMOVE_COMPONENT,
payload: {id},
});

export const setRouteComponents = (components) => ({
Expand Down
177 changes: 65 additions & 112 deletions src/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {Component, createElement, PropTypes} from 'react';
import hoistStatics from 'hoist-non-react-statics';
import {connect} from 'react-redux';

import {getMergedComponents, getPreviousPath} from './selector';
import {removeComponent} from './action';
import {getComponents} from './selector';
import {removeComponent, updateComponent} from './action';
import {componentsShape, renderMapShape, getDisplayName} from './util';

/**
Expand All @@ -12,18 +12,18 @@ import {componentsShape, renderMapShape, getDisplayName} from './util';
*
* @param {Object|Function} rawRenderMap An object with component type/render
* function key value pairs or a function returning such an object.
* @param {Object|Function} rawConfig An object or a function returing such an
* @param {Object} defaultProps An object or a function returing such an
* object.
* @returns {Function} Higher-order component wrapper.
*/
export default (rawRenderMap, rawConfig = {}) => (WrappedComponent) => {
export default ({scope, ...defaultProps} = {}) => (WrappedComponent) => {
class Connect extends Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
___relocation___: PropTypes.shape({
___relocationDispatch___: PropTypes.shape({
removeComponent: PropTypes.func.isRequired,
}),
___relocationState___: PropTypes.shape({
components: componentsShape.isRequired,
previousPath: PropTypes.string.isRequired,
config: PropTypes.object.isRequired,
renderMap: renderMapShape.isRequired,
}).isRequired,
};
Expand All @@ -32,107 +32,50 @@ export default (rawRenderMap, rawConfig = {}) => (WrappedComponent) => {
router: PropTypes.object,
}

navigateToPath(path, {useHistoryBack = true} = {}) {
// Check for the react-router context.
if (!this.context.router) {
return;
}

const {previousPath} = this.props.___relocation___;

// The `useHistoryBack` option will trigger the use of the `goBack` method
// instead of `push` in an effort to keep the if the requested path is
// equal to the previous path.
//
// This is useful in scenraios where component that is displayed in
// response to a route change is considered dismissed or completed on
// removal.
if (useHistoryBack && path === previousPath) {
this.context.router.goBack();
} else {
this.context.router.push(path);
}
}

removeComponent(id/* , options */) {
return this.props.dispatch(removeComponent(id));
}

render() {
const {components, renderMap} = this.props.___relocation___;

const assignRender = (component) => ({
...component,
render: renderMap[component.type],
});
const {components, renderMap} = this.props.___relocationState___;
const {
removeComponent,
updateComponent,
} = this.props.___relocationDispatch___;

const inRenderMap = (component) =>
typeof renderMap[component.type] === 'function';

const assignRemoveHandler = (component) => {
let removeHandler = null;

if (typeof component.remove === 'function') {
// The component object remove property is already a function.
// We don't want to override this behavior.
removeHandler = component.remove;
} else if (component.remove === undefined || component.remove) {
// The component object does not have a `remove` property, or it has
// a truthy value that is not a function. Either case indicates that
// it should use the default remove handler.
removeHandler = (options) =>
this.removeComponent(component.id, options);
}

let pathRemoveHandler = null;
const assign = (component) => {
const result = {
...component,
render: renderMap[component.type],
update: (props) => updateComponent(component.id, props),
remove: () => removeComponent(component.id),
};

if (typeof component.removePath === 'string') {
// Create a function that will change the history state when removing
// the component.
pathRemoveHandler = (options) =>
this.navigateToPath(component.removePath, options);
if (scope) {
result.scope = scope;
}

if (pathRemoveHandler && removeHandler) {
// A remove handler function and a
return {
...component,
remove: (options) => {
pathRemoveHandler(options);
return removeHandler(options);
},
};
}
return result;
};

if (pathRemoveHandler && !removeHandler) {
return {
...component,
remove: pathRemoveHandler,
};
}
const currentComponents = components
// Remove components not included in the render function map.
.filter(inRenderMap)
// Assign render update and remove functions and scope if it is defined.
.map(assign);

if (!pathRemoveHandler && removeHandler !== component.remove) {
return {
...component,
remove: removeHandler,
};
}

// `!pathRemoveHandler && removeHandler === component.remove` is true.
// This means `remove` was set and `removePath` was not set on the
// component object. No modification is necessary.
return component;
};
/* eslint-disable no-unused-vars */
const {
___relocationState___,
___relocationDispatch___,
...childProps,
} = this.props;
/* eslint-enable no-unused-vars */

const mergedProps = {
...this.props,
components: components
// Assign render functions.
.map(assignRender)
// Remove components not included in the render function map.
.filter(inRenderMap)
// Assign remove handler functions.
.map(assignRemoveHandler),
...childProps,
...scope
? {[scope]: {components: currentComponents}}
: {components: currentComponents},
};

return <WrappedComponent {...mergedProps}/>;
Expand All @@ -142,28 +85,38 @@ export default (rawRenderMap, rawConfig = {}) => (WrappedComponent) => {
Connect.displayName = `Relocation(${getDisplayName(WrappedComponent)})`;

const mapState = (state, props) => {
const config = typeof rawConfig === 'function' ?
rawConfig(props) : rawConfig;

const renderMap = typeof rawRenderMap === 'function' ?
rawRenderMap(props) : rawRenderMap;
const mergedProps = {
...defaultProps,
...scope ? props[scope] : props,
};

const {getRelocationState} = config;
const {components, getRelocationState} = mergedProps;

const selectorProps = getRelocationState ?
{getRelocationState, ...props} : props;
const selectorProps = getRelocationState
? {getRelocationState, ...props}
: props;

return {
// Put everything in a ___relocation___ namespace to avoid possible
// Put everything in a ___relocationState___ namespace to avoid possible
// conflict with existing props.
___relocation___: {
components: getMergedComponents(state, selectorProps),
previousPath: getPreviousPath(state, selectorProps),
config,
renderMap,
___relocationState___: {
components: getComponents(state, selectorProps),
renderMap: components,
},
};
};

return connect(mapState)(hoistStatics(Connect, WrappedComponent));
const mapDispatch = (dispatch) => ({
// Put everything in a ___relocationDispatch___ namespace to avoid
// possible conflict with existing props.
___relocationDispatch___: {
removeComponent: (id) => dispatch(removeComponent(id)),
updateComponent: (id, props) => dispatch(updateComponent(id, props)),
},
});

return connect(
mapState,
mapDispatch,
)(hoistStatics(Connect, WrappedComponent));
};
1 change: 0 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ export {ADD_COMPONENT, REMOVE_COMPONENT} from './action';
export {addComponent, removeComponent} from './action';
export {default as relocation} from './connect';
export {default as reducer} from './reducer';
export {default as createRelocationRouter} from './router';
export {default} from './connect';
27 changes: 17 additions & 10 deletions src/reducer.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import {combineReducers} from 'redux';
import {
REMOVE_COMPONENT,
ADD_COMPONENT,
SET_PREVIOUS_PATH,
SET_ROUTE_COMPONENTS,
SET_COMPONENT,
UPDATE_COMPONENT,
REMOVE_COMPONENT,
} from './action';

const createReducer = (type, initial) =>
(state = initial, action) => action.type === type ? action.payload : state;

export default combineReducers({
components: (state = [], action) => ({
[ADD_COMPONENT]: (state, {payload}) => [...state, payload],
[ADD_COMPONENT]: (state, {payload}) =>
[...state, payload],

[SET_COMPONENT]: (state, {payload}) =>
[...state.filter((item) => item.id !== payload.id), payload],

[UPDATE_COMPONENT]: (state, {payload}) =>
state.map((item) => item.id === payload.id
? {...item, props: {...item.props, ...payload.props}}
: item
),

[REMOVE_COMPONENT]: (state, {payload}) =>
state.filter((item) => item.id !== payload),
state.filter((item) => item.id !== payload.id),

}[action.type] || (() => state))(state, action),
routeComponents: createReducer(SET_ROUTE_COMPONENTS, []),
previousPath: createReducer(SET_PREVIOUS_PATH, ''),
});
Loading