Home Reference Source

application/hocs/draftable.js

import React from 'react';
import moment from 'moment';

import {DraftService, DraftsStore} from './../services/draft-service';

/**
 * Draftable HOC
 *
 * Wrap a component in withDraftable, then call
 * <code>
 * this.props.draftable.shouldCreateDraft({'srn': ..., 'raw': ..., 'html': ...});
 * </code>
 *
 * To create drafts. Draftable is single threaded, timed and stateful so you won't
 * create more than one draft every 5 seconds, or create more than one draft at a time,
 * or create a draft when nothing has changed.
 *
 * To check on the status of a draft, you set a hook:
 * <code>
 *     componentWillMount() {
 *         this.props.draftable.doesHaveDraft({'srn': ...}, (draft) => {
 *             if (!draft) {
 *                 ... no draft
 *             } else {
 *                 this.setState({foo: draft.content_raw});
 *             }
 *         });
 *     }
 * </code>
 *
 * @param  {Object} WrappedComponent React component to wrap
 * @return {Object}                  Draftable component
 */
export const withDraftable = (WrappedComponent) => class extends React.Component {
    constructor(props) {
        super(props);

        this.draftCreateHook = false;

        this.interval = false;
        this.isDirty = false;
        this.isRunning = false;
        this.stopAll = false;

        this.hasDraftHook = () => {};
        this.draftCreatedHook = () => {};

        this.state = {};

        this.onChange = this.onChange.bind(this);
        this.onStoreChange = this.onStoreChange.bind(this);
        this.onDraftCallback = this.onDraftCallback.bind(this);

        this.draftable = {
            setDraftCreatedHook: (cb) => {
                this.draftCreatedHook = cb;
            },
            shouldCreateDraft: (state) => {
                this.onChange(state);
            },
            doesHaveDraft: (state, cb) => {
                this.hasDraftHook = cb;
                this.setState(state, this.checkDraft);
            },
            haltDraft: (stopAll) => {
                this.stopAll = stopAll;
            }
        }
    }

    componentWillMount() {
        this.interval = setInterval(this.createDraft.bind(this), 5000);
        DraftsStore.listen(this.onStoreChange);
    }

    componentWillUnmount() {
        clearInterval(this.interval);
        DraftsStore.unlisten(this.onStoreChange);
    }

    /**
     * Creates the draft, this is called via the 'shouldCreateDraft' method
     * exposed to the wrapped component
     */
    createDraft() {
        if (!this.isDirty || this.stopAll) {
            return;
        }

        // avoid concurrent requests
        if (this.isRunning) {
            return;
        }

        this.isRunning = true;
        DraftService.create(this.state.srn, {
            'content': this.state.html,
            'content_raw': this.state.raw
        }).then(() => {
            this.isDirty = false;
            this.isRunning = false;

            this.draftCreatedHook();
        });
    }

    checkDraft() {
        DraftService.fetchOne(this.state.srn).then(() => {
            this.onDraftCallback(this.state.draft);
        });
    }

    onDraftCallback(draft) {
        if (draft && draft.size) {
            this.hasDraftHook(draft);
            return;
        }

        this.hasDraftHook(false);
        return;
    }

    onStoreChange(state) {
        this.setState(state);
    }

    onChange(state) {
        if (JSON.stringify(state.raw) != JSON.stringify(this.state.raw)) {
            this.isDirty = true;
            this.setState(state);
        }
    }

    render() {
        return <WrappedComponent {...this.props} draftable={this.draftable}/>
    }
}