diff --git a/package.json b/package.json index b5a03d2..a81dbca 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/action.js b/src/action.js index 4a47037..a94454c 100644 --- a/src/action.js +++ b/src/action.js @@ -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) => ({ diff --git a/src/connect.js b/src/connect.js index beff7a0..93349d9 100644 --- a/src/connect.js +++ b/src/connect.js @@ -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'; /** @@ -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, }; @@ -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 ; @@ -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)); }; diff --git a/src/index.js b/src/index.js index 2ef6cc1..88c790b 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; diff --git a/src/reducer.js b/src/reducer.js index 6388816..c5dd16a 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -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, ''), }); diff --git a/src/router.js b/src/router.js deleted file mode 100644 index 2d61286..0000000 --- a/src/router.js +++ /dev/null @@ -1,123 +0,0 @@ -import {Component, PropTypes, Children} from 'react'; -import {connect} from 'react-redux'; -import invariant from 'fbjs/lib/invariant'; - -import {setRouteComponents, setPreviousPath} from './action'; - -/** - * [description] - * @param {Function} formatPattern - The React Router `formatPattern` function. - * @returns {[type]} [description] - */ -export default (formatPattern) => { - invariant( - typeof formatPattern === 'function', - 'The `formatPattern` function from `react-redux` must be provided. ' + - 'Add `import {formatPattern} from "react-router"` and add it as the ' + - 'argument to your call to `createRelocationRouter`.' - ); - - const finalFormatPattern = typeof formatPattern === 'function' ? - formatPattern : (path) => path; - - class RelocationRouter extends Component { - static propTypes = { - routes: PropTypes.array.isRequired, - params: PropTypes.object.isRequired, - location: PropTypes.object.isRequired, - children: PropTypes.element.isRequired, - dispatch: PropTypes.func.isRequired, - }; - - // Use `componentWillMount` over `constructor` when calling `dispatch`. - // see: https://github.com/reactjs/react-redux/issues/129 - componentWillMount() { - this.updateRouteComponents(this.props); - } - - componentWillReceiveProps(nextProps) { - this.updatePreviousPath(nextProps); - this.updateRouteComponents(nextProps); - } - - updateRouteComponents(props) { - const {routes, params, location} = props; - const {dispatch} = this.props; - - if (this.routes === params && this.prams === params) { - return; - } - - this.routes = routes; - this.params = params; - - const join = (base, path) => { - return base + (path && !/\/$/.test(base) ? '/' : '') + path; - }; - - const getFormattedPathName = (i) => { - let path = ''; - - for (let current = i; current >= 0; current--) { - const route = routes[current]; - - if (route && route.path) { - path = join(route.path, path); - if (/^\//.test(path)) { - return finalFormatPattern(path, params); - } - } - } - - return null; - }; - - const components = routes.reduce((components, route, i) => { - const {relocation} = route; - - if (relocation) { - components.push({ - // Use the formatted path that matched as the default id. - id: getFormattedPathName(i), - - // Merge component props with redux-router props. - props: { - ...relocation.props, - location, - params, - }, - - // If `route.relocation` is a string, assign it the type. Otherwise - // merge its values. - ...typeof relocation === 'string' ? {type: relocation} : relocation, - - removePath: relocation.removePath !== undefined ? - relocation.removePath : - getFormattedPathName(i - 1), - }); - } - - return components; - }, []); - - dispatch(setRouteComponents(components)); - } - - updatePreviousPath(nextProps) { - const {location: {pathname: previousPath}, dispatch} = this.props; - const {location: {pathname: currentPath}} = nextProps; - - if (previousPath && previousPath !== currentPath) { - dispatch(setPreviousPath(previousPath)); - } - } - - render() { - const {children} = this.props; - - return Children.only(children); - } - } - - return connect()(RelocationRouter); -}; diff --git a/src/selector.js b/src/selector.js index 4df09e2..36c370f 100644 --- a/src/selector.js +++ b/src/selector.js @@ -1,41 +1,7 @@ -import findIndex from 'core-js/library/fn/array/find-index'; - export const getRelocation = (state, props) => - (props && props.getRelocationState) ? - props.getRelocationState(state, props) : - state.relocation; + (props && props.getRelocationState) + ? props.getRelocationState(state, props) + : state.relocation; export const getComponents = (state, props) => getRelocation(state, props).components; - -export const getRouteComponents = (state, props) => - getRelocation(state, props).routeComponents; - -export const getPreviousPath = (state, props) => - getRelocation(state, props).previousPath; - -export const getMergedComponents = (state, props) => - [...getRouteComponents(state, props), ...getComponents(state, props)]. - reduce((components, component) => { - const index = findIndex(components, ({id}) => id === component.id); - - if (index === -1) { - components.push(component); - return components; - } - - const existing = components[index]; - - components[index] = { - ...existing, - ...component, - ...existing.props && component.props ? { - props: { - ...existing.props, - ...component.props, - }, - } : null, - }; - - return components; - }, []); diff --git a/src/util.js b/src/util.js index 1e6ac87..ee100af 100644 --- a/src/util.js +++ b/src/util.js @@ -12,9 +12,10 @@ export const renderMapShape = PropTypes.objectOf(renderShape); export const componentShape = PropTypes.shape({ id: PropTypes.string.isRequired, type: PropTypes.string.isRequired, + scope: PropTypes.string, props: PropTypes.object, remove: PropTypes.func, - removePath: PropTypes.string, + update: PropTypes.func, render: renderShape, });