spandrel/jsx.js

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

/* eslint-env browser */

define([
  'bajaux/spandrel/buildConfig',
  'bajaux/spandrel/util',
  'underscore' ], function (
  buildConfig,
  util,
  _) {

  'use strict';

  const { $IS_SPANDREL_SYMBOL, $KEY_SYMBOL } = buildConfig;
  const { isDynamic } = util;
  const WIDGET_ATTRIBUTES = {
    enabled: true,
    formFactor: true,
    properties: true,
    readonly: true,
    value: true
  };

  /**
   * API Status: **Development**
   * @exports bajaux/spandrel/jsx
   */
  const exports = {};

  /**
   * @param {string|Function} type
   * @param {object|null} props
   * @param kids
   * @returns {module:bajaux/spandrel~SpandrelArg}
   */
  function jsxToSpandrel(type, props, kids) {
    let dom, widgetType;
    props = props || {};

    const { spandrelKey, spandrelSrc } = props;

    if (typeof type === 'string') {
      dom = document.createElement(type);
    } else {
      dom = document.createElement(props.tagName || 'div');
      widgetType = type;
    }

    setProps(dom, props, !!widgetType);

    if (spandrelSrc) {
      if (kids.length) {
        throw new Error('spandrelSrc cannot be combined with children');
      }
      if (typeof type !== 'string') {
        throw new Error('spandrelSrc can only be applied to a DOM element');
      }
      const sp = spandrelSrc.toSpandrel({ dom });
      if (typeof sp === 'object' && !Array.isArray(sp)) {
        sp[$IS_SPANDREL_SYMBOL] = true;
        if (spandrelKey) {
          sp[$KEY_SYMBOL] = spandrelKey;
        }
      }
      return sp;
    }

    let { enabled, formFactor, properties, readonly, value } = props;
    if (typeof enabled === 'string') { enabled = enabled !== 'false'; }
    if (typeof readonly === 'string') { readonly = readonly !== 'false'; }
    if (typeof value === 'undefined' && isDynamic(type)) { value = null; }


    if (!kids.length) {
      kids = undefined;
    } else {
      kids.forEach((kid, i) => {
        if (!kid) {
          return;
        }

        if (typeof kid === 'string') {
          dom.appendChild(document.createTextNode(kid));
          kids = undefined;
        } else {
          kid.key = kid.key || String(i);
        }
      });
    }

    return {
      [$IS_SPANDREL_SYMBOL]: true,
      [$KEY_SYMBOL]: spandrelKey,
      dom,
      enabled,
      formFactor,
      kids,
      properties,
      readonly,
      type: widgetType,
      value
    };
  }

  function setProps(el, props, isWidget) {
    Object.keys(props).forEach((name) => setProp(el, name, props[name], isWidget));
  }

  function setProp(el, name, value, isWidget) {
    if (isWidget && WIDGET_ATTRIBUTES[name]) {
      return;
    }

    if (name === '$init') { return value(el); }

    if (name === 'spandrelKey' || name === 'spandrelSrc') { return; }

    if (name === 'className') { name = 'class'; }

    if (name === 'style' && typeof value === 'object') {
      const { style } = el;
      Object.keys(value).forEach((prop) => { style[prop] = value[prop]; });
    } else if (typeof value === 'boolean') {
      if (value) { el.setAttribute(name, true); }
      el[name] = value;
    } else {
      el.setAttribute(name, value);
    }
  }

  /**
   * @param {string|Function} type HTML tag name, or a Widget constructor to instantiate
   * @param {object|null} props
   * @param {Array.<object>} children
   * @returns {module:bajaux/spandrel~SpandrelArg}
   *
   * @example
   * <caption>Basic JSX->spandrel example</caption>
   * &#37;** @jsx spandrel.jsx *&#37;
   * class ComponentToHTML extends spandrel((comp) => {
   *   return (
   *     <table>
   *     {
   *       comp.getSlots().properties().toArray().map((prop) => {
   *         return <tr>
   *           <td>{ prop.getName() }</td>
   *           <td>{ prop.getType() }</td>
   *         </tr>;
   *       })
   *     }
   *     </table>
   *   );
   * }) {}
   */
  exports.jsxToSpandrel = function (type, props, ...children) {
    return jsxToSpandrel(type, props, _.flatten(children));
  };

  return exports;
});