baja/binding/Binding.js

/**
 * @copyright 2020 Tridium, Inc. All Rights Reserved.
 * @author Logan Byam
 */

/**
 * API Status: **Development**
 * @module nmodule/bajaui/rc/baja/binding/Binding
 */
define([ 
  'baja!',
  'Promise',
  'nmodule/bajaui/rc/binding/impl/widgetEvents',
  'log!nmodule.bajaui.rc.baja.binding.Binding' ], function (
   baja,
   Promise,
   widgetEvents,
   log) {

  'use strict';

  const logWarning = log.warning.bind(log);

  /**
   * BajaScript representation of a `bajaui:Binding`. An instance of this class
   * knows how to propagate data values from an `OrdTarget` to a `Widget`.
   *
   * @abstract
   * @class
   * @alias module:nmodule/bajaui/rc/baja/binding/Binding
   * @extends baja.Component
   * @implements module:nmodule/bajaui/rc/binding/IValueProvider
   * @implements module:nmodule/bajaui/rc/binding/IWidgetEventListener
   */
  return class Binding extends baja.Component {

    /**
     * @returns {module:baja/ord/OrdTarget} the OrdTarget this Binding is bound to
     */
    getOrdTarget() {
      return this.$ordTarget;
    }

    /**
     * @param {module:baja/ord/OrdTarget} ordTarget the OrdTarget this Binding is bound to
     */
    setOrdTarget(ordTarget) {
      this.$ordTarget = ordTarget;
    }

    /**
     * Given a certain property name, typically a Widget property, provide the
     * corresponding value for that name. This is roughly analogous to
     * `BBinding#getOnWidget`.
     *
     * @param {string} name the name of the property to retrieve a value for
     * @param {object} cx user context
     * @returns {*|null|Promise.<*|null>} by default, returns `null` which
     * indicates there is no value to be provided by this name. Override in
     * subclasses.
     */
    provide(name, cx) {
      return null;
    }

    /**
     * Every Binding belongs to a BindingList which contains all the other
     * Bindings that are also bound to its target.
     *
     * @returns {module:nmodule/bajaui/rc/model/BindingList}
     */
    getBindingList() {
      return this.$bindingList;
    }

    /**
     * This is a callback that will be run whenever the Binding is updated to
     * point to a new OrdTarget. This may happen when the binding's ORD changes,
     * or when it is re-resolved.
     *
     * @returns {Promise}
     */
    targetChanged() {
      return null;
    }

    /**
     * @returns {module:bajaux/Widget} the Widget this Binding is bound to
     */
    getWidget() {
      return this.$widget;
    }

    /**
     * @param {module:bajaux/Widget} widget the Widget this Binding is bound to
     */
    setWidget(widget) {
      this.$widget = widget;
    }

    /**
     * When the Binding is bound to a Widget, it can start listening for events
     * from that Widget. Override this method as needed.
     *
     * @param {module:bajaux/Widget} widget
     * @see module:nmodule/bajaui/rc/binding/impl/widgetEvents
     */
    addListeners(widget) {}

    /**
     * Registers the provided events on the widget and keeps track of these
     * events which are disarmed with the default implementation of
     * Binding#removeListeners.
     *
     * The added listeners will fire in the order they were added to the widget.
     * The handler for a listener may optionally return false which will cause
     * the later event handlers to no longer fire. The provided handler for an
     * event also executes in order, asynchronously via promises. This means
     * it will wait for the async operation to finish prior to moving on to the
     * next event of the same type. Valid event types include both JQuery and
     * bajaux events.
     *
     * @example
     * binding.addWidgetEvents(widget, {
     *   click: () => {
     *     // do something when click happens
     *   }
     * });
     *
     * @example
     * binding.addWidgetEvents(widget, {
     *   click: () => {
     *    // This promise will resolve prior to executing the second click event
     *    return this.fetchDataAsync()
     *      .then(() => {
     *        // do something with the fetched data and continue to next click
     *      });
     *   }
     * });
     *
     * @example
     * binding.addWidgetEvents(widget, {
     *   loaded: () => {
     *    return false; // prevent firing events that were armed later.
     *   }
     * });
     *
     * @param {module:bajaux/Widget} widget
     * @param {object} events
     */
    addWidgetEvents(widget, events) {
      this.$widgetEventDisarms = this.$widgetEventDisarms || [];
      this.$widgetEventDisarms.push(widgetEvents(widget, events).disarm);
    }

    /**
     * Any event handlers that would not automatically be cleaned up by
     * `widget.destroy()` can be explicitly cleaned up here.
     *
     * By default, this will cleanup the events that were added via
     * addWidgetEvents
     *
     * @param {module:bajaux/Widget} widget
     */
    removeListeners(widget) {
      if (this.$widgetEventDisarms) {
        this.$widgetEventDisarms.forEach((disarm) => disarm());
        this.$widgetEventDisarms = [];
      }
    }

    /**
     * Hide or disable the widget based on whether this binding is currently
     * degraded.
     * @returns {Promise}
     */
    applyDegradeBehavior() {
      const widget = this.$widget;
      const degraded = this.isDegraded();

      switch (this.get('degradeBehavior').getTag()) {
        case 'disable':
          return widget.setEnabled(!degraded);
        case 'hide':
          widget.properties().add('visible', !degraded);
          return Promise.resolve();
        default:
          return Promise.resolve();
      }
    }

    /**
     * @returns {boolean} if the binding is successfully bound to an
     * `OrdTarget`.
     */
    isBound() {
      return !!this.$ordTarget;
    }

    /**
     * @returns {boolean} if the binding is unusable for any reason. The default
     * implementation returns `!this.isBound()`.
     */
    isDegraded() {
      return !this.isBound();
    }

    /**
     * Callback that will be called when the Px page containing this Binding is
     * saved. Each Binding will have a chance to save any user-entered data (it
     * is the Binding's responsibility to check if the user has made any changes
     * or not).
     *
     * @returns {*|Promise} to be resolved when the binding has saved any
     * user-entered data
     */
    save() {}

    /**
     * Fires a event to signal to the framework that the binding is now bound to
     * a different target ORD. The framework should resolve the Binding's new
     * ORD, set a brand-new OrdTarget, and refresh its bound Widget so the UI is
     * brought up to date.
     *
     * Should not be overridden without calling `super()`.
     */
    requestRebind() {
      this.fireHandlers('rebind', logWarning, this);
    }

    /**
     * Fires an event to signal that the Binding has new values to provide. The
     * framework should propagate all the Binding's properties to its bound
     * Widget so the UI is brought up to date.
     *
     * Should not be overridden without calling `super()`.
     */
    requestRefresh() {
      this.fireHandlers('refresh', logWarning, this);
    }
  };
});