baja/comp/ComponentSpace.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.ComponentSpace}.
 * @module baja/comp/ComponentSpace
 */
define([
  "bajaScript/nav",
  "bajaScript/baja/comp/compUtil",
  "bajaScript/baja/comm/Callback",
  "bajaPromises" ], function (
    baja,
    compUtil,
    Callback,
    bajaPromises) {
  
  "use strict";
  
  var subclass = baja.subclass,
      callSuper = baja.callSuper,
      bajaDef = baja.def,
      bajaError = baja.error,
      strictArg = baja.strictArg,
      objectify = baja.objectify,
            
      unlease = compUtil.unlease;
  
  /**
   * Represents a `baja:ComponentSpace` in BajaScript.
   *
   * @class
   * @alias baja.ComponentSpace
   * @extends baja.NavContainer
   * @implements baja.comm.ServerHandlerProxy
   *
   * @param {String} name
   * @param {String} ordInHost
   * @param host
   */   
  var ComponentSpace = function ComponentSpace(name, ordInHost, host) {
    var that = this;
    callSuper(ComponentSpace, that, [ { navName: name } ]);
    that.$map = {};
    that.$root = null;
    that.$callbacks = null;
    that.$host = bajaDef(host, null);
    that.$ordInHost = baja.Ord.make(ordInHost || '');
    that.$isReadonly = false;
  };
  
  subclass(ComponentSpace, baja.NavContainer); 
  
  /**
   * Called to initialize the Component Space.
   * 
   * @private
   *
   * @returns {Promise} Resolved once the space has initialized.
   */
  ComponentSpace.prototype.init = function () {
    return bajaPromises.resolve();
  };

  /**
   * Return true if the entire ComponentSpace is readonly.
   * 
   * @return {Boolean} true if readonly.
   */
  ComponentSpace.prototype.isSpaceReadonly = function () {
    return this.$isReadonly;
  };
  
  /**
   * Mount a `Component` in the Component Space.
   * 
   * This is a private internal method to mount `Component`s into a Component Space.
   *
   * @ignore
   */
  function mount(space, comp) {    
    var h = comp.getHandle(),
        cursor;
    
    if (h === null) {
      // TODO: What about automatically generating a handle for a non Proxy Component Space?
      // TODO: Generate error if not a handle?
      return;
    }
    
    if (space.$map.hasOwnProperty(h)) {
      throw new Error("Fatal error: handle already used in Component Space: " + h);
    }
    
    // Set up the Space and Handle reference
    comp.$space = space;
    space.$map[h] = comp;
        
    // Mount any child Components
    cursor = comp.getSlots().properties();
    
    while (cursor.next()) {
      const slotValue = comp.get(cursor.get());
      if (slotValue.getType().isComponent()) {
        mount(space, slotValue);
      }
    }
  }
  
  /**
   * Unmount a `Component` in the Component Space.
   * 
   * This is a private internal method to unmount `Component`s into a Component Space.
   *
   * @ignore
   */
  function unmount(space, comp) {    
    var h = comp.getHandle(),
        prevSub,
        subs,
        cursor,
        i;
    
    if (h === null) {
      // TODO: Generate error if not a handle?
      return;
    }
    
    if (!space.$map.hasOwnProperty(h)) {
      throw new Error("Fatal error: handle not mapped into Space: " + h);
    }
    
    prevSub = comp.isSubscribed();
    
    // delete the Space and Handle reference (after this isMounted() will return false)
    comp.$space = null;
    delete space.$map[h];
        
    // Unsubscribe Component from all Subscribers
    subs = comp.$subs.slice();
    
    for (i = 0; i < subs.length; ++i) {
      subs[i].unsubscribe({
        "comps": comp
      });
    }
    
    // Make sure we're not leased    
    unlease(comp);
    
    // If this was subscribed before, make sure this callback is made
    if (prevSub && typeof comp.unsubscribed === "function") {
      try {
        comp.unsubscribed();
      } catch (err) {
        bajaError(err);
      }
    }
    
    // TODO: What about nullifying the handle reference on the Component? 
             
    // Unmount any child Components
    cursor = comp.getSlots().properties();
    while (cursor.next()) {
      const slotValue = comp.get(cursor.get());
      if (slotValue.getType().isComponent()) {
        unmount(space, slotValue);
      }
    }
  }
    
  /**
   * Private framework handler for a `Component` Space.
   * 
   * This is a private internal method for framework developers.
   *
   * @private
   */
  ComponentSpace.prototype.$fw = function (x, a, b, c) {    
    if (x === "mount") {
      // Mount this Component    
      mount(this, /*Component*/a);
    } else if (x === "unmount") {
      // Unmount this Component    
      unmount(this, /*Component*/a);
    }
  };  
    
  /**
   * Return the root `Component` of the Component Space.
   * 
   * @returns {baja.Component} the root Component for the Space.
   */
  ComponentSpace.prototype.getRootComponent = function () {
    return this.$root;
  };
  
  /**
   * Return the ORD in Session for the Component Space.
   *
   * @returns {baja.Ord}
   */
  ComponentSpace.prototype.getOrdInSession = function () {
    return this.$ordInHost.relativizeToSession();
  };
    
  /**
   * Return absolute ORD for the Component Space. May be session-aware.
   *
   * @param {object} [params]
   * @param {boolean} [params.sessionAware]
   * @returns {baja.Ord}
   * @see baja.NavContainer#getNavOrd
   */
  ComponentSpace.prototype.getAbsoluteOrd = function (params) {
    if (!this.$host) {
      return baja.Ord.DEFAULT;
    }

    var sessionAware = baja.objectify(params).sessionAware;
    return baja.Ord.make({
      base: this.$host.getAbsoluteOrd(params),
      child: sessionAware ? this.$ordInHost : this.getOrdInSession()
    });
  };

  /**
   * @private
   * @override
   * @returns {String} the server handler ID to use when making server calls.
   */
  ComponentSpace.prototype.getServerHandlerId = function () {
    return this.getAbsoluteOrd().relativizeToSession().toString();
  };

    
  /**
   * Find the `Component` via its handle (null if not found).
   * 
   * This method does not result in any network calls.
   *
   * @private
   *
   * @param {String} handle the Component's handle.
   *
   * @returns {baja.Component} the Component via its handle (null if not found).
   */
  ComponentSpace.prototype.findByHandle = function (handle) {
    strictArg(handle, String);
    return bajaDef(this.$map[handle], null);
  }; 
  
  /**
   * Find the `Component` via its handle (null if not found).
   * 
   * This method may result in an **asynchronous** network call if 
   * the `Component` can't be found locally and the Space is a Proxy.
   * 
   * @private
   *
   * @param {Object} [obj] the object literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called if the Component is resolved. The Component instance will be passed
   * to this function.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * Called if there's an error or the Component can't be resolved.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise.<baja.Component>} a promise that will be resolved once
   * the component has been resolved.
   */
  ComponentSpace.prototype.resolveByHandle = function (obj) {
    obj = objectify(obj);
    
    var handle = obj.handle,
        cb = new Callback(obj.ok, obj.fail),
        comp;
    
    try {
      comp = this.findByHandle(handle);
      
      if (comp !== null) {
        cb.ok(comp);
      } else {
        throw new Error("Could not find Component from Handle");
      }
    } catch (err) {
      cb.fail(err);
    }
    return cb.promise();
  }; 

  /**
   * Resolve to a list of enabled mix-in Types for the Component Space.
   *
   * An Object Literal is used for the method's arguments.
   *
   * @name baja.ComponentSpace#toEnabledMixIns
   * @function
   *
   * @param {Object} [obj] the Object Literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) Callback handler
   * invoked once the enabled mix-in Types have been resolved.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise.<Array.<Object>>} a promise that will be resolved once
   * the mixin information has been retrieved.
   */  
  ComponentSpace.prototype.toEnabledMixIns = function (obj) {
    obj = baja.objectify(obj);
    
    var cb = new Callback(obj.ok, obj.fail, obj.batch); 
    cb.ok([]);
           
    return cb.promise();
  };
  
  /**
   * Return true if this Component Space has Space callbacks.
   * 
   * Space callbacks are normally used to make network calls.
   *
   * @private
   *
   * @returns {Boolean}
   */   
  ComponentSpace.prototype.hasCallbacks = function () {
    return this.$callbacks !== null;
  };
  
  /**
   * Return the Space Callbacks.
   *
   * @private
   *
   * @returns Space Callbacks
   */ 
  ComponentSpace.prototype.getCallbacks = function () {
    return this.$callbacks;
  };  
  
  /**
   * Sync the Component Space.
   * 
   * If the Space is a Proxy, this method will result in an 
   * **asynchronous** network call to sync the master Space with this one.
   * 
   * @param {Object} [obj] the object literal for the method's arguments.
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
   * Called once the Component Space has been successfully synchronized with the
   * Server.
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
   * Called If the Component Space can't be synchronized.
   * @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
   * batched into this object.
   * @returns {Promise} a promise that will be resolved once the space has been
   * synced.
   */
  ComponentSpace.prototype.sync = function (obj) {
    obj = objectify(obj, "ok");
    var cb = new Callback(obj.ok, obj.fail);
    cb.ok();
    return cb.promise();
  };
  
  /**
   * Return the Nav ORD of the Root Component.
   *
   * @private
   *
   * @returns {baja.Ord}
   */
  ComponentSpace.prototype.getNavOrd = function () {
    return this.$root === null ? null : this.$root.getNavOrd();
  };
  
  /**
   * Access the Nav Children.
   *
   * @see baja.NavContainer#getNavChildren
   *
   * @returns promise a promise that will be resolved once the callbacks have been invoked.
   */
  ComponentSpace.prototype.getNavChildren = function (obj) {
    obj = objectify(obj, "ok");
    if (this.$root) {
      return this.$root.getNavChildren(obj);
    }
    var cb = new Callback(obj.ok);
    cb.ok([]);
    return cb.promise();
  };
  
  /**
   * Return the Nav Icon.
   *
   * @returns {baja.Icon}
   */
  ComponentSpace.prototype.getNavIcon = function () {
    return this.$root ? this.$root.getNavIcon() : callSuper("getNavIcon", ComponentSpace, this);
  };
  
  return ComponentSpace;
});