@@ -2,64 +2,168 @@ import {Component, createElement, PropTypes} from 'react';
22import hoistStatics from 'hoist-non-react-statics' ;
33import { 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} ;
0 commit comments