spandrel.js

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

/* eslint-env browser */

/**
 * API Status: **Development**
 * @module bajaux/spandrel
 */
define([
  'log!bajaux.spandrel',
  'bajaux/lifecycle/WidgetManager',
  'Promise',
  'underscore',
  'nmodule/js/rc/asyncUtils/asyncUtils',
  'bajaux/spandrel/buildConfig',
  'bajaux/spandrel/DynamicSpandrelWidget',
  'bajaux/spandrel/jsx',
  'bajaux/spandrel/logging',
  'bajaux/spandrel/SpandrelWidget',
  'bajaux/spandrel/symbols' ], function (
    log,
    WidgetManager,
    Promise,
    _,
    asyncUtils,
    buildConfig,
    DynamicSpandrelWidget,
    jsx,
    logging,
    SpandrelWidget,
    symbols) {

  'use strict';

  const { doRequire } = asyncUtils;

  let defaultManager = new WidgetManager();

  const { IS_ELEMENT_SYMBOL } = symbols;


  /**
   * The purpose of `spandrel` is to provide a reasonably pure-functional,
   * diffable method of defining a nested structure of bajaux Widgets and
   * supporting HTML. Rather than require Widget implementors to manually code
   * calls to `initialize()` or `buildFor()`, `spandrel` allows you to provide
   * your desired structure of HTML elements and their associated Widget
   * instances, and handle the work of updating the document as that structure
   * may change over time.
   *
   * See {@tutorial 50-spandrel} for in-depth information.
   *
   * @alias module:bajaux/spandrel
   * @param {module:bajaux/spandrel~SpandrelData|function(*, module:bajaux/spandrel~WidgetState): module:bajaux/spandrel~SpandrelData} arg
   * @param {object} [params={}] params
   * @param {function(new:module:bajaux/Widget)} [params.extends] optionally specify a Widget superclass to extend
   * @param {string} [params.strategy] optionally specify a known lookup strategy
   * for dynamically building widgets. Currently, the only accepted value is
   * `niagara`, which will instruct `spandrel` to use the Niagara registry to
   * perform widget lookups (introducing a dependency on the `webEditors`
   * module). If included, it overrides the `manager` parameter.
   * @param {module:bajaux/lifecycle/WidgetManager} [params.manager] optionally provide your own WidgetManager to manage Widget lifecycle
   * @returns {Function} a Widget constructor
   * @since Niagara 4.10
   *
   * @example
   * <caption>Generate a static widget</caption>
   * const StaticWidget = spandrel([
   *   '<label>Name: </label>',
   *   '<input type="text" value="{{ props.name }}">',
   *   {
   *     dom: '<span></span>',
   *     value: false,
   *     properties: 'inherit'
   *   }
   * ]);
   * return fe.buildFor({
   *   dom: $('#myStaticWidget'),
   *   type: StaticWidget,
   *   properties: { name: 'Logan', trueText: 'Good', falseText: 'Not So Good' }
   * });
   *
   * @example
   * <caption>Generate a dynamic widget with a field editor for each slot</caption>
   * const DynamicWidget = spandrel(comp => comp.getSlots().toArray().map(slot => ({
   *   dom: '<div class="componentSlot"/>',
   *   kids: [
   *     `<label>${ slot.getName() }: </label>`,
   *     { dom: '<span/>', complex: comp, slot: slot }
   *   ]
   * })));
   *
   * return fe.buildFor({
   *   dom: $('#myDynamicWidget'),
   *   type: DynamicWidget,
   *   value: myComponent
   * });
   *
   * @example
   * <caption>Subclass an existing dynamic spandrel widget, making changes
   * before rendering.</caption>
   *
   * // our superclass will render a <label> element, with a background
   * // determined by a widget property.
   * const LabelWidget = spandrel((value, { properties }) => {
   *   const label = document.createElement('label');
   *   label.innerText = value;
   *   label.style.background = properties.background || '';
   *   return label;
   * });
   *
   * const RedLabelWidget = spandrel((value, { renderSuper }) => {
   *
   *   // renderSuper will call back to the superclass, allowing your subclass
   *   // to edit the data before spandrel renders it to the page.
   *   //
   *   // you can optionally pass a function to renderSuper that will tweak the
   *   // widget state before the superclass renders its data. if no tweaking is
   *   // desired, just renderSuper() is fine.
   *   //
   *   return renderSuper((state) => {
   *     state.properties.background = 'lightpink';
   *
   *     // remember to return the new state.
   *     return state;
   *   })
   *     .then((label) => {
   *       // renderSuper will resolve the data exactly as rendered by the
   *       // superclass.
   *       label.style.color = 'red';
   *       return label;
   *     });
   * }, { extends: LabelWidget });
   */
  function spandrel(arg, params) {
    if (typeof arg === 'function') {
      //dynamically redefine the nested widget structure based on whatever
      //value is being loaded.
      return makeDynamic(arg, params);
    } else {
      // define a static nested widget structure.
      return makeStatic(arg, params);
    }
  }


  /**
   * Given spandrel input (potentially dynamically generated), spit out a build
   * context, where each member may potentially contain more nested data, that
   * will map to one or more fe.buildFor calls.
   *
   * @private
   * @param {module:bajaux/spandrel~SpandrelData|function(*, module:bajaux/spandrel~WidgetState): module:bajaux/spandrel~SpandrelData} arg
   * @param {module:bajaux/spandrel~WidgetState} widgetState configuration data derived
   * from the parent widget to contain all these spandrel-generated widgets
   * @returns {Promise.<module:bajaux/spandrel~BuildContext>}
   */
  spandrel.build = function (arg, widgetState) {
    return buildConfig(arg, widgetState, this);
  };

  /**
   * Use `spandrel.jsx` as your JSX pragma to convert your JSX into spandrel
   * config.
   *
   * @see module:bajaux/spandrel/jsx
   */
  spandrel.jsx = jsx.jsxToSpandrel;

  /**
   * @private
   * @param {module:bajaux/lifecycle/WidgetManager} manager
   */
  spandrel.$installDefaultWidgetManager = function (manager) {
    defaultManager = manager;
  };

  /**
   * @private
   * @returns {module:bajaux/lifecycle/WidgetManager}
   */
  spandrel.$getDefaultWidgetManager = function () {
    return defaultManager;
  };

  /**
   * @private
   */
  spandrel.$trace = function () {
    logging.$trace();
  };

  /**
   * @private
   * @type {Object.<string, Function>}
   */
  spandrel.$KNOWN_MANAGERS = {
    // avoid JsBuild verification failure
    niagara: _.once(() => doRequire('nmodule'.toLowerCase() + '/webEditors/rc/fe/fe')
      .then((fe) => fe.getWidgetManager()))
  };

  /**
   * @param {function(*): module:bajaux/spandrel~SpandrelArg} spandrelFunction
   * @param {Object} [params]
   * @param {Function} [params.extends]
   * @param {module:bajaux/lifecycle/WidgetManager} [params.manager]
   * @returns {function(new:module:bajaux/spandrel/SpandrelWidget)}
   */
  function makeDynamic(spandrelFunction, { extends: Super, strategy, manager } = {}) {
    return DynamicSpandrelWidget.make({
      extends: Super, manager, spandrelFunction, strategy
    });
  }

  function makeStatic(spandrelArg, { manager = defaultManager, [IS_ELEMENT_SYMBOL]: isElement } = {}) {
    return SpandrelWidget.make({ isElement, manager, spandrelArg });
  }

  return spandrel;
});