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;