All files / boundless/packages/boundless-async index.js

96.3% Statements 26/27
88.89% Branches 16/18
100% Functions 13/13
100% Lines 23/23
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          63x                                                                                                                                                                               7x       7x 7x 6x   6x     1x         17x   17x 8x 9x 2x     7x     7x   7x       15x 15x       10x 10x 5x 10x     21x   21x                        
import React, {PropTypes} from 'react';
import cx from 'classnames';
 
import omit from 'boundless-utils-omit-keys';
 
const get = (base, path, fallback) => path.split('.').reduce((current, fragment) => current[fragment] || fallback, base);
 
/**
 * __A higher-order component for rendering data that isn't ready yet.__
 *
 * There are plenty of situations where you need to fetch content to be displayed, but want
 * to show some sort of loading graphic in the interim. This component helps to simplify
 * that pattern by handling common types of promises and providing a simple mechanism
 * for materializing the fulfilled payload into JSX.
 */
export default class Async extends React.PureComponent {
    static propTypes = {
        /**
         * any [React-supported attribute](https://facebook.github.io/react/docs/tags-and-attributes.html#html-attributes)
         */
        '*': PropTypes.any,
 
        /**
         * a promise, function that returns a promise, or other type of renderable content; if a function is passed, it will
         * be called with the current props
         *
         * Promise example:
         *
         * ```jsx
         * const listDataPromise = fetch('/some/list/data/endpoint').then(
         *     (response) => response.ok ? response.json() : 'Failed to receive list data',
         *     (error) => error.message,
         * ).then((payload) => {
         *     if (typeof payload === 'string') {
         *         return (<div className='error'>{payload}</div>);
         *     }
         *
         *     return (
         *         <ul>
         *             {payload.map((item) => (<li key={item.id}>{item.content}</li>))}
         *         </ul>
         *     );
         * });
         *
         * <Async>{listDataPromise}</Async>
         *
         * Function example:
         *
         * ```jsx
         * const fetchListData = (props) => fetch(props['data-endpoint']).then(
         *     (response) => response.ok ? response.json() : 'Failed to receive list data',
         *     (error) => error.message,
         * ).then((payload) => {
         *     if (typeof payload === 'string') {
         *         return (<div className='error'>{payload}</div>);
         *     }
         *
         *     return (
         *         <ul>
         *             {payload.map((item) => (<li key={item.id}>{item.content}</li>))}
         *         </ul>
         *     );
         * });
         *
         * <Async data-endpoint='/some/list/data/endpoint'>{fetchListData}</Async>
         * ```
         */
        children: PropTypes.oneOfType([
            PropTypes.func,
            PropTypes.node,
            PropTypes.instanceOf(Promise),
        ]).isRequired,
 
        /** a callback for when real content has been rendered; this will be called immediately if normal JSX is passed to Async, or, in the case of a promise, upon resolution or rejection */
        childrenDidRender: PropTypes.func,
 
        /** content to be shown while the promise is in "pending" state (like a loading graphic, perhaps) */
        pendingContent: PropTypes.node,
    }
 
    static defaultProps = {
        children: <div />,
        childrenDidRender: () => {},
        pendingContent: <div />,
    }
 
    static internalKeys = Object.keys(Async.defaultProps)
 
    mounted = false
    promise = null
    state = {}
 
    handlePromiseFulfillment(context, payload) {
        Iif (!this.mounted) { return; }
 
        // only set the component if the promise that is fulfilled matches
        // the one we're tracking in state, otherwise ignore it and retain the previous data
        this.setState(function renderPayloadIfPromiseMatches(state) {
            if (this.promise === context) {
                this.promise = null;
 
                return {component: payload};
            }
 
            return state;
        }, this.fireRenderCallback);
    }
 
    handleChildren(children) {
        let content = children;
 
        if (React.isValidElement(content)) {
            return this.setState({component: content}, this.fireRenderCallback);
        } else if (typeof content === 'function') {
            return this.handleChildren(content(this.props));
        }
 
        const boundHandler = this.handlePromiseFulfillment.bind(this, content);
 
        // this is kept outside state so it can be set immediately if the props change
        this.promise = content;
 
        this.setState({component: null}, () => content.then(boundHandler, boundHandler));
    }
 
    fireRenderCallback() {
        Eif (this.state.component) {
            this.props.childrenDidRender();
        }
    }
 
    componentWillMount()                 { this.handleChildren(this.props.children); }
    componentDidMount()                  { this.mounted = true; }
    componentWillReceiveProps(nextProps) { this.handleChildren(nextProps.children); }
    componentWillUnmount()               { this.mounted = false; }
 
    render() {
        const {props, state} = this;
 
        return React.cloneElement(state.component || props.pendingContent, {
            ...omit(props, Async.internalKeys),
            className: cx(
                'b-async',
                props.className,
                state.component === null && get(props, 'pendingContent.props.className'),
                state.component && get(state, 'component.props.className', ''),
                {'b-async-pending': state.component === null}
            ),
        });
    }
}