beanworks/React.withBackbone

View on GitHub
src/index.js

Summary

Maintainability
A
3 hrs
Test Coverage
import React    from 'react';
import Backbone from 'backbone';
import debounce from 'lodash.debounce';

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

function getNonIntersectIn(setA, setB) {
    const presentOnlyInA = new Set(setA);
    const presentOnlyInB = new Set(setB);
    
    for (const elem of presentOnlyInB) {
        if ([...presentOnlyInA].indexOf(elem) !== -1) {
            presentOnlyInB.delete(elem);
            presentOnlyInA.delete(elem);
        }
    }

    return [presentOnlyInA, presentOnlyInB];
}

const withBackbone = (WrappedComponent) => {
    class WithBackbone extends React.Component {
        constructor(props) {
            super(props);

            this.modelListener = Object.assign({}, Backbone.Events);
            this.setOfModels = this.getSetOfBackbone(props, Backbone.Model);
            this.subscribeTo(this.modelListener, this.setOfModels, 'change');

            this.collectionListener = Object.assign({}, Backbone.Events);
            this.setOfCollections = this.getSetOfBackbone(props, Backbone.Collection);
            this.subscribeCollection(this.collectionListener, this.setOfCollections);

            this.debouncedForceUpdate = debounce(this.forceUpdateWrapper, 0);
        }

        forceUpdateWrapper() {
            this.forceUpdate();
        }

        getSetOfBackbone(props, instance) {
            const result = new Set();
            for (const propName in props) {
                if (this.props[propName] instanceof instance) {
                    result.add(this.props[propName]);
                }
            }

            return result;
        }

        subscribeCollection(listener, setOfCollection) {
            this.subscribeTo(listener, setOfCollection, 'add remove reset sort');
        }

        subscribeTo(listener, setOfObj, event = 'change') {
            for (const obj of setOfObj) {
                listener.listenTo(obj, event, () => {
                    this.debouncedForceUpdate();
            });
            }
        }

        unsubscribeFrom(listener, setOfObj) {
            setOfObj.forEach((obj) => {
                listener.stopListening(obj);
            });
        }

        componentWillReceiveProps(nextProps) {
            const newSetOfModels = this.getSetOfBackbone(nextProps, Backbone.Model);
            const newSetOfCollections = this.getSetOfBackbone(nextProps, Backbone.Collection);

            const [modelsToUnsubscribe, modelsToSubscribe] = getNonIntersectIn(this.setOfModels, newSetOfModels);
            const [collectionsToUnsubscribe, collectionsToSubscribe] = getNonIntersectIn(this.setOfCollections, newSetOfCollections);

            this.subscribeTo(this.modelListener, modelsToSubscribe, 'change');
            this.subscribeCollection(this.collectionListener, collectionsToSubscribe);

            this.unsubscribeFrom(this.modelListener, modelsToUnsubscribe);
            this.unsubscribeFrom(this.collectionListener, collectionsToUnsubscribe);

            this.setOfModels = newSetOfModels;
            this.setOfCollections = newSetOfCollections;
        }

        componentWillUnmount() {
            this.modelListener.stopListening();
            this.collectionListener.stopListening();
        }

        render() {
            return (
                <WrappedComponent ref={(wrappedComponent) => {this.wrappedComponent = wrappedComponent}}
                    {...this.props}
                />
            );
        }
    }

    WithBackbone.displayName = `WithBackbone(${getDisplayName(WrappedComponent)})`;

    return WithBackbone;
};

export {withBackbone};
export default withBackbone;