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