forked from artsy/react-redux-controller
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
154 lines (135 loc) · 6.22 KB
/
index.js
File metadata and controls
154 lines (135 loc) · 6.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { default as React, PropTypes } from 'react';
import { connect } from 'react-redux';
import R from 'ramda';
import co from 'co';
import { aggregateSelectors } from './selector_utils';
const toDispatchSymbol = Symbol('toDispatch');
/** Request to get the props object at a specific time */
export const getProps = Symbol('getProps');
/**
* Request to get a function that will return the controller `props` object,
* when called.
*/
export const getPropsGetter = Symbol('getPropsGetter');
/**
* Conveniece request to dispatch an action directly from a controller
* generator.
* @param {*} action a Redux action
* @return {*} the result of dispatching the action
*/
export function toDispatch(action) {
return { [toDispatchSymbol]: action };
}
/**
* The default function for converting the controllerGenerators to methods that
* can be directly called. It resolves `yield` statements in the generators by
* delegating Promise to `co` and processing special values that are used to
* request data from the controller.
* @param {Function} propsGetter gets the current controller props.
* @return {GeneratorToMethod} a function that converts a generator to a method
* forwarding on the arguments the generator receives.
*/
export function runControllerGenerator(propsGetter) {
return controllerGenerator => co.wrap(function* coWrapper() {
const gen = controllerGenerator.apply(this, arguments);
let value;
let done;
let toController;
for ({ value, done } = gen.next(); !done; { value, done } = gen.next(toController)) {
const props = propsGetter();
// In the special cases that the yielded value has one of our special
// tags, process it, and then we'll send the result on to `co` anyway
// in case whatever we get back is a promise.
if (value && value[toDispatchSymbol]) {
// Dispatch an action
toController = props.dispatch(value[toDispatchSymbol]);
} else if (value === getProps) {
// Return all props
toController = props;
} else if (value === getPropsGetter) {
// Return the propsGetter itself, so the controller can get props
// values in async continuations
toController = propsGetter;
} else {
// Defer to `co`
toController = yield value;
}
}
return value;
});
}
/**
* This higher-order component introduces a concept of a Controller, which is a
* component that acts as an interface between the proper view component tree
* and the Redux state modeling, building upon react-redux. It attempts to
* solve a couple problems:
*
* - It provides a way for event handlers and other helpers to access the
* application state and dispatch actions to Redux.
* - It conveys those handlers, along with the data from the react-redux
* selectors, to the component tree, using React's [context](bit.ly/1QWHEfC)
* feature.
*
* It was designed to help keep UI components as simple and domain-focused
* as possible (i.e. [dumb components](bit.ly/1RFh7Ui), while concentrating
* the React-Redux integration point at a single place. It frees intermediate
* components from the concern of routing dependencies to their descendents,
* reducing coupling of components to the UI layout.
*
* @param {React.Component} RootComponent is the root of the app's component
* tree.
* @param {Object} controllerGenerators contains generator methods to be used
* to create controller methods, which are distributed to the component tree.
* These are called from UI components to trigger state changes. These
* generators can `yield` Promises to be resolved via `co`, can `yield`
* requests to receive application state or dispatch actions, and can
* `yield*` to delegate to other controller generators.
* @param {(Object|Object[])} selectorBundles maps property names to selector
* functions, which produce property value from the Redux store.
* @param {Function} [controllerGeneratorRunner = runControllerGenerator] is
* the generator wrapper that will be used to run the generator methods.
* @return {React.Component} a decorated version of RootComponent, with
* `context` set up for its descendents.
*/
export function controller(RootComponent, controllerGenerators, selectorBundles, controllerGeneratorRunner = runControllerGenerator) {
// Combine selector bundles into one mapStateToProps function.
const mapStateToProps = aggregateSelectors(R.mergeAll(R.flatten([selectorBundles])));
const selectorPropTypes = mapStateToProps.propTypes;
// All the controller method propTypes should simply be "function" so we can
// synthensize those.
const controllerMethodPropTypes = R.map(() => PropTypes.func.isRequired, controllerGenerators);
// Declare the availability of all of the selectors and controller methods
// in the React context for descendant components.
const contextPropTypes = R.merge(selectorPropTypes, controllerMethodPropTypes);
class Controller extends React.Component {
constructor(...constructorArgs) {
super(...constructorArgs);
const injectedControllerGeneratorRunner = controllerGeneratorRunner(() => this.props);
this.controllerMethods = R.map(controllerGenerator =>
injectedControllerGeneratorRunner(controllerGenerator)
, controllerGenerators);
// Ensure controller methods can access each other via `this`
for (const methodName of Object.keys(this.controllerMethods)) {
this.controllerMethods[methodName] = this.controllerMethods[methodName].bind(this.controllerMethods);
}
}
componentWillMount() {
if (this.controllerMethods.initialize) { this.controllerMethods.initialize(); }
}
getChildContext() {
// Rather than injecting all of the RootComponent props into the context,
// we only explictly pass selector and controller method props.
const selectorProps = R.pick(R.keys(selectorPropTypes), this.props);
const childContext = R.merge(selectorProps, this.controllerMethods);
return childContext;
}
render() {
return (
<RootComponent {...this.props} />
);
}
}
Controller.propTypes = R.merge(selectorPropTypes, RootComponent.propTypes || {});
Controller.childContextTypes = contextPropTypes;
return connect(mapStateToProps)(Controller);
}