/* eslint valid-jsdoc: 0 */ const React = require('react'); const shallowEqual = require('react/lib/shallowEqual'); const find = require('lodash/collection/find'); const TheStore = require('../TheStore'); /** * Determine if an object has an array of keys. * * @param {object} object * Object to check. * @param {string[]} keys * Array of keys to check. * * @return {boolean} * If object has all of the keys. */ function hasKeys(object, keys) { let i = keys.length; while (i--) { if (!object.hasOwnProperty([keys[i]])) { return false; } } return true; } /** * Creates a Container component to wrap a given compoent. * * @param {ReactComponent} Component * The Component to wrap with a data layer. * @param {object} options * Key-value options * @param {string} options.name * Name to call this (key to use in TheStore) * @param {ReactComponent} options.Loading * Component to render while still loading. * If undefined, uses Component * @param {ReactComponent} options.Failure * Component to render while in a failure state. * If undefined, uses Component * @param {function} options.load * Function that, given params, promises to load data for this container. * @param {function} options.isStateAccurate * Optional. Given (props, state), returns TRUE if state has accurate data. * @param {string[]} options.storeKeys * An array of keys to validate if state from the Store is in valid. * * @return {ReactComponent} * Wrapping Component w/ Data. */ module.exports = function createContainer(Component, options) { const {name, Loading, Failure, load, storeKeys} = options; const Container = React.createClass({ displayName: name, // Pull Initial State from store (maybe it's already loaded) getInitialState() { return TheStore.getState(); }, componentWillReceiveDispatch() { this.setState(TheStore.getState()); }, // Pick up changes to the store componentDidMount() { this.unsubscribe = TheStore.subscribe(this.componentWillReceiveDispatch); }, componentWillUnmount() { this.unsubscribe(); }, // Load & Pre-load componentWillMount() { // Client: Check if load if needed if (typeof window !== "undefined" && !this.isFailure() && this.isLoading() ) { Container.load(this.props).then(TheStore.set); } }, // React Router is bringing this component in. componentWillReceiveProps(nextProps) { if (!shallowEqual(this.props, nextProps)) { Container.load(nextProps).then(TheStore.set); } }, /** * Determine if a key, we are concerned with, is an error. * * @param {*} value * Value in the store to check. * @param {string} key * Key of the Store to check. * @return {boolean} * True, if we are concerned with this key, and the value is an error. */ isInvalidStoreValue(value, key) { return storeKeys.indexOf(key) >= 0 && (value instanceof Error || value.error); }, /** * Determine if Container has failed for this component. * * @return {boolean} * True if data has, in fact, failed. */ isFailure() { if (storeKeys && hasKeys(this.state, storeKeys) && find(this.state, this.isInvalidStoreValue)) { return true; } return false; }, /** * Determine if Container has data for this component. * * @return {boolean} * True if data is, in fact, loaded. */ isLoading() { if (storeKeys && !hasKeys(this.state, storeKeys)) { return true; } if (options.isStateAccurate) { return !options.isStateAccurate(this.props, this.state); } return false; }, render() { const component = ( (this.isFailure() && Failure) || (this.isLoading() && Loading) || Component ); return component && React.createElement(component, {...this.props, ...this.state}); } }); Container.load = props => load(props); return Container; };