193 lines
4.9 KiB
JavaScript
193 lines
4.9 KiB
JavaScript
import React, { PropTypes, createElement } from 'react';
|
|
import { Observable, CompositeDisposable } from 'rx';
|
|
import shouldComponentUpdate from 'react-pure-render/function';
|
|
import debug from 'debug';
|
|
|
|
// interface contain {
|
|
// (options?: Object, Component: ReactComponent) => ReactComponent
|
|
// (options?: Object) => (Component: ReactComponent) => ReactComponent
|
|
// }
|
|
//
|
|
// Action: { type: String, payload: Any, ...meta }
|
|
//
|
|
// ActionCreator(...args) => Observable
|
|
//
|
|
// interface options {
|
|
// fetchAction?: ActionCreator,
|
|
// getActionArgs?(props: Object, context: Object) => [],
|
|
// isPrimed?(props: Object, context: Object) => Boolean,
|
|
// handleError?(err) => Void
|
|
// shouldRefetch?(
|
|
// props: Object,
|
|
// nextProps: Object,
|
|
// context: Object,
|
|
// nextContext: Object
|
|
// ) => Boolean,
|
|
// }
|
|
|
|
|
|
const log = debug('fcc:professerx');
|
|
|
|
function getChildContext(childContextTypes, currentContext) {
|
|
|
|
const compContext = { ...currentContext };
|
|
// istanbul ignore else
|
|
if (!childContextTypes || !childContextTypes.professor) {
|
|
delete compContext.professor;
|
|
}
|
|
return compContext;
|
|
}
|
|
|
|
const __DEV__ = process.env.NODE_ENV !== 'production';
|
|
|
|
export default function contain(options = {}, Component) {
|
|
/* istanbul ignore else */
|
|
if (!Component) {
|
|
return contain.bind(null, options);
|
|
}
|
|
|
|
let action;
|
|
let isActionable = false;
|
|
let hasRefetcher = typeof options.shouldRefetch === 'function';
|
|
|
|
const getActionArgs = typeof options.getActionArgs === 'function' ?
|
|
options.getActionArgs :
|
|
(() => []);
|
|
|
|
const isPrimed = typeof options.isPrimed === 'function' ?
|
|
options.isPrimed :
|
|
(() => false);
|
|
|
|
|
|
return class Container extends React.Component {
|
|
constructor(props, context) {
|
|
super(props, context);
|
|
this.__subscriptions = new CompositeDisposable();
|
|
}
|
|
|
|
static displayName = `Container(${Component.displayName})`;
|
|
static contextTypes = {
|
|
...Component.contextTypes,
|
|
professor: PropTypes.object
|
|
};
|
|
|
|
componentWillMount() {
|
|
const { professor } = this.context;
|
|
const { props } = this;
|
|
if (!options.fetchAction) {
|
|
log(`${Component.displayName} has no fetch action defined`);
|
|
return null;
|
|
}
|
|
|
|
action = props[options.fetchAction];
|
|
isActionable = typeof action === 'function';
|
|
|
|
if (__DEV__ && typeof action !== 'function') {
|
|
throw new Error(
|
|
`${options.fetchAction} should return a function but got ${action}.
|
|
Check the fetch options for ${Component.displayName}.`
|
|
);
|
|
}
|
|
|
|
if (
|
|
!professor ||
|
|
!professor.fetchContext
|
|
) {
|
|
log(
|
|
`${Component.displayName} did not have professor defined on context`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
|
|
const actionArgs = getActionArgs(
|
|
props,
|
|
getChildContext(Component.contextTypes, this.context)
|
|
);
|
|
|
|
return professor.fetchContext.push({
|
|
name: options.fetchAction,
|
|
action,
|
|
actionArgs,
|
|
component: Component.displayName || 'Anon'
|
|
});
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (isPrimed(this.props, this.context)) {
|
|
log('container is primed');
|
|
return null;
|
|
}
|
|
if (!isActionable) {
|
|
log(`${Component.displayName} container is not actionable`);
|
|
return null;
|
|
}
|
|
const actionArgs = getActionArgs(this.props, this.context);
|
|
const fetch$ = action.apply(null, actionArgs);
|
|
if (__DEV__ && !Observable.isObservable(fetch$)) {
|
|
console.log(fetch$);
|
|
throw new Error(
|
|
`Action creator should return an Observable but got ${fetch$}.
|
|
Check the action creator for fetch action ${options.fetchAction}`
|
|
);
|
|
}
|
|
|
|
const subscription = fetch$.subscribe(
|
|
() => {},
|
|
options.handleError
|
|
);
|
|
return this.__subscriptions.add(subscription);
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps, nextContext) {
|
|
if (
|
|
!isActionable ||
|
|
!hasRefetcher ||
|
|
!options.shouldRefetch(
|
|
this.props,
|
|
nextProps,
|
|
getChildContext(Component.contextTypes, this.context),
|
|
getChildContext(Component.contextTypes, nextContext)
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
const actionArgs = getActionArgs(
|
|
this.props,
|
|
getChildContext(Component.contextTypes, this.context)
|
|
);
|
|
|
|
const fetch$ = action.apply(null, actionArgs);
|
|
if (__DEV__ && !Observable.isObservable(fetch$)) {
|
|
throw new Error(
|
|
'fetch action should return observable'
|
|
);
|
|
}
|
|
|
|
const subscription = fetch$.subscribe(
|
|
() => {},
|
|
options.errorHandler
|
|
);
|
|
|
|
this.__subscriptions.add(subscription);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.__subscriptions) {
|
|
this.__subscriptions.dispose();
|
|
}
|
|
}
|
|
|
|
shouldComponentUpdate = shouldComponentUpdate;
|
|
|
|
render() {
|
|
const { props } = this;
|
|
|
|
return createElement(
|
|
Component,
|
|
props
|
|
);
|
|
}
|
|
};
|
|
}
|