Source

fields/BaseField.js

/**
 * @category Fields
 * @class
 *
 * The BaseField class provides a generic structure and some default functionality
 * for all components.
 *
 * Extend this class to create custom widgets by overriding the default
 * lifecycle methods.
 *
 * **Lifecycle methods:**
 *
 * - _setElement_: Aaves the HTML element indicated by the id parameter as
 *    the main element of the widget.
 * - _buildDOM_: Builds the initial HTML representation of the widget.
 * - _addListeners_: Adds any listeners to listen for user interaction and
 *    react to it, for example by changing the inner state of the component.
 * - _setDefault_: Updates the widget state and visual representation with
 *    the default value specified in the constructor.
 * - _setValue_: Updates the widget state programmatically with an arbitrary
 *    value.
 * - _setDOM_: Updates the visual representation of the component, usually
 *    executed after state changes to reflect them.
 *
 * **Saving and restoring state in the data tree:**
 *
 * The widget state can be saved or restored from the data tree by using
 * the _save()_ and _restore()_ methods.
 * Override them to provide custom saving and restoring functionality.
 * Call _save()_ and _restore()_ from outside the widget to save its state
 * on the data tree or restore it.
 *
 * **Changing the component state:**
 *
 * The component state can be changed by three methods:
 *
 *    - By setting the value programmatically calling _setValue()_.
 *    - By interacting with it.
 *    - By changing the associated datanode and calling _widget.restore()_.
 *
 * **Reacting to changes in the component state from outside of it:**
 *
 * Either subscribe to changes on the datanode or pass an apply
 * function to the constructor:
 *
 * ```
 * datanode.subscribe('lt-my-widget-data', data => { });
 *
 * let myWidget = new MyWidget({
 *   id: 'lt-my-widget',
 *   key: 'lt-my-widget-data',
 *   apply : function(value) {  }
 * }
 * ```
 */
class BaseField {
  /**
   * @constructor
   * @param {Object} props - Object of properties
   * @param {String} props.id - Id of the field
   * @param {*} [props.default=null] - Default value
   * @param {Boolean} [props.preview=true] - If the value can be previewed
   * @param {Function} [props.onSave=null] - Callback for the save action. With one param as the new value.
   * @param {Boolean} [props.canSave=true] - If the field can save ??? TODO: Better description
   */
  constructor(props) {
    Object.assign(
      this,
      {
        onChange: null, // On the change of the field this method is trigger with
        // the value
        onSave: null, // When the value has to be stored, it is manually
        // trigger by save() method
        previous: null, // Previous value
      },
      props
    );

    // Get application reference
    this.app = document.querySelector(".lt-vapp");

    // Set the DOM element
    this.setElement();
    if (!this.el) {
      console.error(`The field id '${this.id}' does not exists.`);
      return undefined;
    }

    // Initial value initialization
    this.value = null;
    this.setDefault();

    // Builds the DOM representation of the widget
    // Override it to specify custom HTML
    this.buildDOM();
    this.setDOM();

    // Add listeners for the field
    this.addListeners();

    return this;
  }

  // Initialization
  // ---------------------------------------------------------------------------

  /**
   * Set default value (Normally only in the constructor)
   */
  setDefault() {
    if (this.default !== null) {
      this.value = this.default;
    }
  }

  /**
   * Set the element to listen on changes
   * Overload in some cases
   */
  setElement() {
    this.el = this.app.querySelector(this.id);
  }

  /**
   * Listener for on change properties
   * By default is change event for a Input[type=text] field
   */
  addListeners() {
    this.el.addEventListener("change", () => {
      this.setValue(this.el.value);
      if (this.onChange) this.onChange(this.value);
    });
  }

  // Getters & Setters
  // ---------------------------------------------------------------------------

  /**
   * Programmatically set the value of the widget and update the DOM
   * to reflect the changes.
   * @param {*} value
   */
  setValue(value) {
    if (this.value != null) this.previous = this.value;
    this.value = value;
    this.setDOM();
  }

  /**
   * Get the current value of the field
   * @return {*} Value of the field
   */
  getValue() {
    return this.value;
  }

  /**
   * Get the clean id name (without # or .)
   * @returns {String} Clean id
   */
  getCleanId() {
    return this.id.replace("#", "").replace(".", "");
  }

  // DOM Operations
  // ---------------------------------------------------------------------------

  /**
   * Builds the DOM representation of the widget
   * By default is empty for a Input[type=text] field
   */
  buildDOM() {}

  /**
   * Set the value to the DOM element
   * By default apply like a Input[type=text]
   */
  setDOM() {
    this.el.value = this.value;
  }

  // Save operations
  // ---------------------------------------------------------------------------

  /**
   * Save action trigger onSave method
   */
  save() {
    this.previous = null;
    if (this.onSave) this.onSave(this.value);
  }

  /**
   * Restore the previous value & trigger onChange callback
   */
  restore() {
    if (this.previous != null) {
      this.setValue(this.previous);
      this.previous = null;
      if (this.onChange) this.onChange(this.value);
    }
  }
}

export default BaseField;