Source: ReactViews/ObserveModelMixin.js

'use strict';

import defined from 'terriajs-cesium/Source/Core/defined';
import knockout from 'terriajs-cesium/Source/ThirdParty/knockout';
import PureRenderMixin from 'react-addons-pure-render-mixin';

const ObserveModelMixin = {
    componentWillMount() {
        this.__observeModelChangeSubscriptions = undefined;

        const originalRender = this.render;

        this.render = function renderForObserveModelMixin() {
            const that = this;
            let isFirstRender = true;

            // Clean up the previous subscription, if any.
            disposeSubscription(that);

            // Ignore dependencies so that the parent component's render function does not
            // depend on the child component's render function.  If it did, a change to a child
            // would trigger re-rendering of all its ancestors, which is wasteful.

            return knockout.ignoreDependencies(function() {
                const computed = knockout.computed(function() {
                    // The first time our computed render function is called, pass through to the real
                    // render and track dependencies along the way.  Any time after that is a result of
                    // a change in one of the properties that the render used.  But if we re-evaluate
                    // the render function there, we're stepping outside the normal React lifecycle.
                    // Instead, unsubscribe from the computed observable and force an update of the
                    // React component (which will create a new computed observable).
                    if (isFirstRender) {
                        isFirstRender = false;
                        return originalRender.call(that);
                    }
                }).extend({notify: 'always'});

                that.__observeModelChangeSubscriptions = [];

                let updateForced = false;

                /**
                 * Disposes of the subscription to this component and forces an update, which will create a new
                 * computed observable.
                 */
                function disposeAndForceUpdate() {
                    disposeSubscription(that);

                    if (!updateForced) {
                        updateForced = true;
                        that.forceUpdate();
                    }
                }

                // We also need to update on change of anything in props with a __knockoutSubscribable property.  This
                // property is added to arrays by knockout-es5 and is notified whenever the array is modified.
                // Without this, changes in an observable array passed as props won't trigger re-render of the component,
                // even if the array is used in rendering.  This is because Knockout observable arrays don't note a
                // dependency when accessing individual elements of the array.
                for (const prop in that.props) {
                    if (that.props.hasOwnProperty(prop)) {
                        if (defined(that.props[prop]) && defined(that.props[prop].__knockoutSubscribable)) {
                            that.__observeModelChangeSubscriptions.push(that.props[prop].__knockoutSubscribable.subscribe(disposeAndForceUpdate));
                        }
                    }
                }

                that.__observeModelChangeSubscriptions.push(computed.subscribe(disposeAndForceUpdate));

                return computed();
            });
        };
    },

    componentWillUnmount() {
        disposeSubscription(this);
    },

    shouldComponentUpdate: PureRenderMixin.shouldComponentUpdate
};

/**
 * Disposes of all subscriptions that a component currently has.
 *
 * @param component The component to find and dispose subscriptions on.
 */
function disposeSubscription(component) {
    if (defined(component.__observeModelChangeSubscriptions)) {
        for (let i = 0; i < component.__observeModelChangeSubscriptions.length; ++i) {
            component.__observeModelChangeSubscriptions[i].dispose();
        }
        component.__observeModelChangeSubscriptions = undefined;
    }
}

module.exports = ObserveModelMixin;