Home Reference Source

src/Form.js

import Element from './Element';
import { Formio } from './Formio';
import Displays from './displays';
import templates from './templates';
import * as FormioUtils from './utils/utils';

export default class Form extends Element {
  /**
   * Creates an easy to use interface for embedding webforms, pdfs, and wizards into your application.
   *
   * @param {Object} element - The DOM element you wish to render this form within.
   * @param {Object | string} form - Either a Form JSON schema or the URL of a hosted form via. form.io.
   * @param {Object} options - The options to create a new form instance.
   * @param {boolean} options.readOnly - Set this form to readOnly
   * @param {boolean} options.noAlerts - Set to true to disable the alerts dialog.
   * @param {boolean} options.i18n - The translation file for this rendering. @see https://github.com/formio/formio.js/blob/master/i18n.js
   * @param {boolean} options.template - Provides a way to inject custom logic into the creation of every element rendered within the form.
   *
   * @example
   * import Form from '@formio/js/Form';
   * const form = new Form(document.getElementById('formio'), 'https://examples.form.io/example');
   * form.build();
   */
  constructor(...args) {
    let options = args[0] instanceof HTMLElement ? args[2] : args[1];
    if (Formio.options && Formio.options.form) {
      options = Object.assign(options, Formio.options.form);
    }

    super(options);

    if (this.options.useSessionToken) {
      Formio.useSessionToken(this.options);
    }

    this.ready = new Promise((resolve, reject) => {
      this.readyResolve = resolve;
      this.readyReject = reject;
    });

    this.instance = null;
    if (args[0] instanceof HTMLElement) {
      if (this.element) {
        delete this.element.component;
      }
      this.element = args[0];
      this.options = args[2] || {};
      this.options.events = this.events;
      this.setForm(args[1])
        .then(() => this.readyResolve(this.instance))
        .catch(this.readyReject);
    }
    else if (args[0]) {
      this.element = null;
      this.options = args[1] || {};
      this.options.events = this.events;
      this.setForm(args[0])
        .then(() => this.readyResolve(this.instance))
        .catch(this.readyReject);
    }
    else {
      this.element = null;
      this.options = {};
      this.options.events = this.events;
    }
    this.display = '';
  }

  createElement(tag, attrs, children) {
    const element = document.createElement(tag);
    for (const attr in attrs) {
      if (attrs.hasOwnProperty(attr)) {
        element.setAttribute(attr, attrs[attr]);
      }
    }
    (children || []).forEach(child => {
      element.appendChild(this.createElement(child.tag, child.attrs, child.children));
    });
    return element;
  }

  set loading(load) {
    if (!this.element || this.options.noLoader) {
      return;
    }
    if (load) {
      if (this.loader) {
        return;
      }
      this.loader = this.createElement('div', {
        'class': 'formio-loader'
      }, [{
        tag: 'div',
        attrs: {
          class: 'loader-wrapper'
        },
        children: [{
          tag: 'div',
          attrs: {
            class: 'loader text-center'
          }
        }]
      }]);
      this.element.appendChild(this.loader);
    }
    else if (this.loader) {
      this.element.removeChild(this.loader);
      this.loader = null;
    }
  }

  /**
   * Create a new form instance provided the display of the form.
   *
   * @param {string} display - The display of the form, either "wizard", "form", or "pdf"
   * @return {*}
   */
  create(display) {
    if (this.options && (this.options.flatten || this.options.renderMode === 'flat')) {
      display = 'form';
    }
    this.display = display;
    if (Displays.displays[display]) {
      return new Displays.displays[display](this.element, this.options);
    }
    else {
      // eslint-disable-next-line new-cap
      return new Displays.displays['webform'](this.element, this.options);
    }
  }

  /**
   * Sets the form. Either as JSON or a URL to a form JSON schema.
   *
   * @param {string|object} formParam - Either the form JSON or the URL of the form json.
   * @return {*}
   */
  set form(formParam) {
    this.setForm(formParam);
  }

  errorForm(err) {
    return {
      components: [
        {
          'label': 'HTML',
          'tag': 'div',
          'className': 'error error-message alert alert-danger ui red message',
          'attrs': [
            {
              'attr': 'role',
              'value': 'alert'
            }
          ],
          'key': 'errorMessage',
          'type': 'htmlelement',
          'input': false,
          'content': typeof err === 'string' ? err : err.message,
        }
      ]
    };
  }

  setForm(formParam) {
    let result;
    formParam = formParam || this.form;
    if (typeof formParam === 'string') {
      const formio = new Formio(formParam);
      let error;
      this.loading = true;
      result = this.getSubmission(formio, this.options)
        .catch((err) => {
          error = err;
        })
        .then((submission) => {
          return formio.loadForm()
          // If the form returned an error, show it instead of the form.
            .catch(err => {
              error = err;
            })
            .then((form) => {
              // If the submission returned an error, show it instead of the form.
              if (error) {
                form = this.errorForm(error);
              }
              this.loading = false;
              this.instance = this.instance || this.create(form.display);
              this.instance.url = formParam;
              this.instance.nosubmit = false;
              this._form = this.instance.form = form;
              if (submission) {
                this.instance.submission = submission;
              }
              if (error) {
                throw error;
              }
              return this.instance;
            });
        });
    }
    else {
      this.instance = this.instance || this.create(formParam.display);
      this._form = this.instance.form = formParam;
      result = this.instance.ready;
    }

    // A redraw has occurred so save off the new element in case of a setDisplay causing a rebuild.
    return result.then(() => {
      if (this.element) {
        delete this.element.component;
      }
      this.element = this.instance.element;
      return this.instance;
    });
  }

  getSubmission(formio, opts) {
    if (formio.submissionId) {
      return formio.loadSubmission(null, opts);
    }
    return Promise.resolve();
  }

  /**
   * Returns the loaded forms JSON.
   *
   * @return {object} - The loaded form's JSON
   */
  get form() {
    return this._form;
  }

  /**
   * Changes the display of the form.
   *
   * @param {string} display - The display to set this form. Either "wizard", "form", or "pdf"
   * @return {Promise<T>}
   */
  setDisplay(display) {
    if ((this.display === display) && this.instance) {
      return Promise.resolve(this.instance);
    }

    this.form.display = display;
    this.instance.destroy();
    this.instance = this.create(display);
    return this.setForm(this.form);
  }

  empty() {
    if (this.element) {
      while (this.element.firstChild) {
        this.element.removeChild(this.element.firstChild);
      }
    }
  }

  static embed(embed) {
    return new Promise((resolve) => {
      if (!embed || !embed.src) {
        resolve();
      }
      const id = this.id || `formio-${Math.random().toString(36).substring(7)}`;
      const className = embed.class || 'formio-form-wrapper';
      let code = embed.styles ? `<link rel="stylesheet" href="${embed.styles}">` : '';
      code += `<div id="${id}" class="${className}"></div>`;
      document.write(code);
      let attempts = 0;
      const wait = setInterval(() => {
        attempts++;
        const formElement = document.getElementById(id);
        if (formElement || attempts > 10) {
          resolve(new Form(formElement, embed.src).ready);
          clearInterval(wait);
        }
      }, 10);
    });
  }

  /**
   * Sanitize an html string.
   *
   * @param string
   * @returns {*}
   */
  sanitize(dirty, forceSanitize) {
    // If Sanitize is turned off
    if (this.options.sanitize === false && !forceSanitize) {
      return dirty;
    }
    return FormioUtils.sanitize(dirty, this.options);
  }

  setContent(element, content, forceSanitize) {
    if (element instanceof HTMLElement) {
      element.innerHTML = this.sanitize(content, forceSanitize);
      return true;
    }
    return false;
  }

  /**
   * Build a new form.
   *
   * @return {Promise<T>}
   */
  build() {
    if (!this.instance) {
      return Promise.reject('Form not ready. Use form.ready promise');
    }

    if (!this.element) {
      return Promise.reject('No DOM element for form.');
    }

    // Add temporary loader.
    const template = (this.options && this.options.template) ? this.options.template : 'bootstrap';
    const loader = templates[template].loader || templates.bootstrap.loader;
    this.setContent(this.element, loader.form);

    return this.render().then(html => {
      this.setContent(this.element, html);
      return this.attach(this.element).then(() => this.instance);
    })
      .then((param) => {
        this.emit('build', param);
        return param;
      });
  }

  render() {
    if (!this.instance) {
      return Promise.reject('Form not ready. Use form.ready promise');
    }
    return Promise.resolve(this.instance.render())
      .then((param) => {
        this.emit('render', param);
        return param;
      });
  }

  attach(element) {
    if (!this.instance) {
      return Promise.reject('Form not ready. Use form.ready promise');
    }
    if (this.element) {
      delete this.element.component;
    }
    this.element = element;
    return this.instance.attach(this.element)
      .then((param) => {
        this.emit('attach', param);
        return param;
      });
  }

  teardown() {
    super.teardown();
    delete this.instance;
    delete this.ready;
  }
}

// Allow simple embedding.
Formio.embedForm = (embed) => Form.embed(embed);

/**
 * Factory that creates a new form based on the form parameters.
 *
 * @param element {HMTLElement} - The HTML Element to add this form to.
 * @param form {string|Object} - The src of the form, or a form object.
 * @param options {Object} - The options to create this form.
 *
 * @return {Promise} - When the form is instance is ready.
 */
Formio.createForm = (...args) => {
  return (new Form(...args)).ready;
};

Formio.Form = Form;