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
19 changes: 17 additions & 2 deletions src/action.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
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_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 removeComponent = (id) => ({
type: REMOVE_COMPONENT,
payload: {id},
});

export const setRouteComponents = (components) => ({
type: SET_ROUTE_COMPONENTS,
Expand Down
89 changes: 23 additions & 66 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} from './selector';
import {removeComponent} from './action';
import {getComponents} from './selector';
import {removeComponent, updateComponent} from './action';
import {componentsShape, renderMapShape, getDisplayName} from './util';

/**
Expand Down Expand Up @@ -43,80 +43,36 @@ export default ({scope, ...defaultProps} = {}) => (WrappedComponent) => {

render() {
const {components, renderMap} = this.props.___relocationState___;
const {removeComponent} = this.props.___relocationDispatch___;
const {
removeComponent,
updateComponent,
} = this.props.___relocationDispatch___;

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

const assignRender = (component) => ({
...component,
render: renderMap[component.type],
});

const assignScope = (component) => ({...component, scope});

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 = () => removeComponent(component.id);
}

let pathRemoveHandler = null;

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

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

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

if (!pathRemoveHandler && removeHandler !== component.remove) {
return {
...component,
remove: removeHandler,
};
const assign = (component) => {
const result = {
...component,
render: renderMap[component.type],
update: (props) => updateComponent(component.id, props),
remove: typeof component.removePath === 'string'
? () => this.navigateToPath(component.removePath)
: () => removeComponent(component.id),
};

if (scope) {
result.scope = scope;
}

// `!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;
return result;
};

const currentComponents = components
// Remove components not included in the render function map.
.filter(inRenderMap)
// Assign render functions.
.map(assignRender)
// Assign scope, if configured.
.map(scope ? assignScope : (component) => component)
// Assign remove handler functions.
.map(assignRemoveHandler);
// Assign render update and remove functions and scope if it is defined.
.map(assign);

/* eslint-disable no-unused-vars */
const {
Expand Down Expand Up @@ -155,7 +111,7 @@ export default ({scope, ...defaultProps} = {}) => (WrappedComponent) => {
// Put everything in a ___relocationState___ namespace to avoid possible
// conflict with existing props.
___relocationState___: {
components: getMergedComponents(state, selectorProps),
components: getComponents(state, selectorProps),
renderMap: components,
},
};
Expand All @@ -166,6 +122,7 @@ export default ({scope, ...defaultProps} = {}) => (WrappedComponent) => {
// possible conflict with existing props.
___relocationDispatch___: {
removeComponent: (id) => dispatch(removeComponent(id)),
updateComponent: (id, props) => dispatch(updateComponent(id, props)),
},
});

Expand Down
29 changes: 22 additions & 7 deletions src/reducer.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
import {combineReducers} from 'redux';
import {
REMOVE_COMPONENT,
ADD_COMPONENT,
SET_COMPONENT,
UPDATE_COMPONENT,
REMOVE_COMPONENT,
SET_ROUTE_COMPONENTS,
} 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, []),

routeComponents: (state = [], action) =>
action.type === SET_ROUTE_COMPONENTS
? action.payload
: state,
});
52 changes: 15 additions & 37 deletions src/selector.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
import findIndex from 'core-js/library/fn/array/find-index';

export const getRelocation = (state, props) =>
(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 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;
}, []);
(props && props.getRelocationState)
? props.getRelocationState(state, props)
: state.relocation;

export const getComponents = (state, props) => {
const routeComponents = getRelocation(state, props).routeComponents;
const components = getRelocation(state, props).components;

// Concat components and route components uniquely by id.
return routeComponents
.filter(({id: routeId}) =>
!components.filter(({id}) => id === routeId).length
)
.concat(components);
};