freeCodeCamp/common/app/utils/professor-x.js

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
);
}
};
}