From 24c8c17b7e63da7acfa7dc5f930a1a36c20e5ab5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Henrik=20tom=20W=C3=B6rden?= <h.tomwoerden@indiscale.com>
Date: Fri, 18 Dec 2020 12:43:14 +0000
Subject: [PATCH] Add documentation on the forms module

---
 .gitignore                     |    3 +
 README_SETUP.md                |    1 +
 src/core/js/ext_bottom_line.js |   47 +-
 src/core/js/form_elements.js   | 1760 +++++++++++++++++---------------
 src/doc/Makefile               |   10 +-
 src/doc/conf.py                |    9 +-
 src/doc/extension.rst          |   13 +
 src/doc/extension/forms.rst    |   80 ++
 src/doc/index.rst              |    3 +-
 test/docker/Dockerfile         |    1 +
 10 files changed, 1047 insertions(+), 880 deletions(-)
 create mode 100644 src/doc/extension.rst
 create mode 100644 src/doc/extension/forms.rst

diff --git a/.gitignore b/.gitignore
index 859153e6..f69db87a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,9 @@
 /build
 __pycache__
 
+# auto-generated sources
+/src/doc/api
+
 # screen logs
 screenlog.*
 xerr.log
diff --git a/README_SETUP.md b/README_SETUP.md
index efad321c..485835fd 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -81,5 +81,6 @@ Build documentation in `build/` with `make doc`.
 - sphinx
 - sphinx-autoapi
 - jsdoc (`npm install jsdoc`)
+- jsdoc-sphinx  (`npm install jsdoc-sphinx`)
 - sphinx-js
 - recommonmark
diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js
index bd7ec6a9..e09ee3d6 100644
--- a/src/core/js/ext_bottom_line.js
+++ b/src/core/js/ext_bottom_line.js
@@ -23,6 +23,28 @@
 
 'use strict';
 
+/**
+ * @typedef {BottomLineConfig}
+ * @property {string|HTMLElement} fallback - Fallback content if none of
+ *     the creators are applicable.
+ * @property {string} version - the version of the configuration which must
+ *     match this module's version.
+ * @property {CreatorConfig[]} creators - an array of creators.
+ */
+
+/**
+ * @typedef {CreatorConfig}
+ * @property {string} [id] - a unique id for the creator. optional, for
+ *     debuggin purposes.
+ * @property {function|string} is_applicable - If this is a string this has
+ *     to be valid javascript! An asynchronous function which accepts one
+ *     parameter, an entity in html representation, and which returns true
+ *     iff this creator is applicable for the given entity.
+ * @property {string} create - This has to be valid javascript! An
+ *     asynchronous function which accepts one parameter, an entity in html
+ *     representation. It returns a HTMLElement or text node which will be
+ *     shown in the bottom line container iff the creator is applicable.
+ */
 
 /**
  * Add a special section to each entity one the current page where a thumbnail,
@@ -45,6 +67,7 @@
  * @requires UTIF (from utif.js library)
  * @requires ext_table_preview (module from ext_table_preview.js)
  */
+
 var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, UTIF, ext_table_preview) {
 
     /**
@@ -52,28 +75,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
      *     (entity) Note: This property can as well be a
      *     javascript string which evaluates to a function.
      */
-    /**
-     * @type {BottomLineConfig}
-     * @property {string|HTMLElement} fallback - Fallback content if none of
-     *     the creators are applicable.
-     * @property {string} version - the version of the configuration which must
-     *     match this module's version.
-     * @property {CreatorConfig[]} creators - an array of creators.
-     */
 
-    /**
-     * @type {CreatorConfig}
-     * @property {string} [id] - a unique id for the creator. optional, for
-     *     debuggin purposes.
-     * @property {function|string} is_applicable - If this is a string this has
-     *     to be valid javascript! An asynchronous function which accepts one
-     *     parameter, an entity in html representation, and which returns true
-     *     iff this creator is applicable for the given entity.
-     * @property {string} create - This has to be valid javascript! An
-     *     asynchronous function which accepts one parameter, an entity in html
-     *     representation. It returns a HTMLElement or text node which will be
-     *     shown in the bottom line container iff the creator is applicable.
-     */
 
     /**
      * Check if an entity has a path attribute and one of a set of extensions.
@@ -547,6 +549,9 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
 
     }
 
+    /**
+     * @exports ext_bottom_line
+     */
     return {
         previewShownEvent: previewShownEvent,
         previewReadyEvent: previewReadyEvent,
diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js
index 47c8e0ce..d4cd4234 100644
--- a/src/core/js/form_elements.js
+++ b/src/core/js/form_elements.js
@@ -25,8 +25,6 @@
  * form_elements module for reusable form elemenst which already have a basic
  * css styling.
  *
- * @version 0.2
- *
  * IMPORTANCE CONCEPTS
  *
  * FIELD - an HTMLElement which wraps a LABEL element (the fields name) and the
@@ -48,24 +46,53 @@
  * SUBFORM - an HTMLElement which contains FIELDS and other SUBFORMS. SUBFORMS
  * can be used to nest FIELDS, which is not supported by HTML5 but allows only
  * for flat key-value pairs.
- */
-
-/**
- * The configuration for double, integer, date input elements.
- *
- * @typedef {object} input_config
- * @property {string} name
- * @property {string} type
- * @property {string} label
- */
-
-/**
- * The configuration for reference_select input fields
- *
- * TODO
  *
+ * @version 0.2
+ * @exports form_elements
  */
 var form_elements = new function () {
+    /**
+     * Config for an alert
+     *
+     * @typedef {object} AlertConfig
+     * @property {string} [title] - an optional title for the alert.
+     * @property {string} [severity="danger"] - a bootstrap class suffix. Other
+     *     examples: warning, info
+     * @property {string} message - informs the user what they are about to do.
+     * @property {function} proceed_callback - the function which is called
+     *     then the user hits the "Proceed" button.
+     * @property {function} [cancel_callback] - a callback which is called then
+     *     the cancel button is clicked. By default, only the alert is being
+     *     closed an nothing happens.
+     * @property {string} [proceed_text="Proceed"] - the text on the proceed button.
+     * @property {string} [cancel_text="Cancel"] - the text on the cancel button.
+     * @property {string} [remember_my_decision_id] - if this parameter is
+     *     present, a checkbox is appended to the alert ("Don't ask me
+     *     again."). If the checkbox is checked the next time the make_alert
+     *     function is called with the same remember_my_decision_id is created,
+     *     the alert won't show up and the proceed_callback is called without
+     *     any user interaction.
+     * @property {string} [remember_my_decision_text="Don't ask me again."] -
+     *     label text for the checkbox.
+     * @property {HTMLElement} [proceed_button] - an optional custom proceed
+     *     button.
+     * @property {HTMLElement} [cancel_button] - an optional custom cancel
+     *     button.
+     */
+
+
+    /**
+     * The configuration for double, integer, date input elements.
+     *
+     * There are specializations of this configuration object. See
+     * {@link ReferenceDropDownConfig}
+     *
+     * @typedef {object} FieldConfig
+     * @property {string} name
+     * @property {string} type
+     * @property {string} label
+     * @see {@link ReferenceDropDownConfig}
+     */
 
     this.version = "0.1";
     this.dependencies = ["log", "caosdb_utils", "markdown"];
@@ -171,33 +198,6 @@ var form_elements = new function () {
         localStorage["form_elements.alert_decision." + key] = val;
     }
 
-    /**
-     * @type {AlertConfig}
-     * @property {string} [title] - an optional title for the alert.
-     * @property {string} [severity="danger"] - a bootstrap class suffix. Other
-     *     examples: warning, info
-     * @property {string} message - informs the user what they are about to do.
-     * @property {function} proceed_callback - the function which is called
-     *     then the user hits the "Proceed" button.
-     * @property {function} [cancel_callback] - a callback which is called then
-     *     the cancel button is clicked. By default, only the alert is being
-     *     closed an nothing happens.
-     * @property {string} [proceed_text="Proceed"] - the text on the proceed button.
-     * @property {string} [cancel_text="Cancel"] - the text on the cancel button.
-     * @property {string} [remember_my_decision_id] - if this parameter is
-     *     present, a checkbox is appended to the alert ("Don't ask me
-     *     again."). If the checkbox is checked the next time the make_alert
-     *     function is called with the same remember_my_decision_id is created,
-     *     the alert won't show up and the proceed_callback is called without
-     *     any user interaction.
-     * @property {string} [remember_my_decision_text="Don't ask me again."] -
-     *     label text for the checkbox.
-     * @property {HTMLElement} [proceed_button] - an optional custom proceed
-     *     button.
-     * @property {HTMLElement] [cancel_button] - an optional custom cancel
-     *     button.
-     */
-
     /**
      * Make an alert, that is a dialog which can intercept a function call and
      * asks the user to proceed or cancel.
@@ -270,37 +270,39 @@ var form_elements = new function () {
         return _alert[0];
     }
 
+
+    this.init = function () {
+        this.logger.trace("enter init");
+    }
+
     /**
-     * (Re-)set this module's functions to standard implementation.
+     * Return an OPTION element with entity reference.
+     *
+     * The OPTION element for a SELECT form input shows a short
+     * summary/description of an entity and has the entity's id as value.
+     *
+     * If the `desc` parameter is undefined, the entity_id is shown
+     * instead.
+     *
+     * @param {string} entity_id - the entity's id.
+     * @param {string} [desc] - the description for the entity.
+     * @returns {HTMLElement} OPTION element.
      */
-    this._init_functions = function () {
-
-        this.init = function () {
-            this.logger.trace("enter init");
+    this.make_reference_option = function (entity_id, desc) {
+        caosdb_utils.assert_string(entity_id, "param `entity_id`");
+        if (typeof desc == "undefined") {
+            desc = entity_id;
         }
+        var opt_str = '<option value="' + entity_id + '">' + desc +
+            "</option>";
+        return $(opt_str)[0];
+    }
 
-        /**
-         * Return an OPTION element with entity reference.
-         *
-         * The OPTION element for a SELECT form input shows a short
-         * summary/description of an entity and has the entity's id as value.
-         *
-         * If the `desc` parameter is undefined, the entity_id is shown
-         * instead.
-         *
-         * @param {string} entity_id - the entity's id.
-         * @param {string} [desc] - the description for the entity.
-         * @returns {HTMLElement} OPTION element.
-         */
-        this.make_reference_option = function (entity_id, desc) {
-            caosdb_utils.assert_string(entity_id, "param `entity_id`");
-            if (typeof desc == "undefined") {
-                desc = entity_id;
-            }
-            var opt_str = '<option value="' + entity_id + '">' + desc +
-                "</option>";
-            return $(opt_str)[0];
-        }
+
+    /**
+     * (Re-)set this module's functions to standard implementation.
+     */
+    this._init_functions = function () {
 
         /**
          * Return SELECT form element with entity references.
@@ -350,8 +352,6 @@ var form_elements = new function () {
         }
 
         /**
-         * @typedef {option} ReferenceDropDownConfig
-         *
          * Configuration object for a drop down menu for selecting references.
          * `make_reference_drop_down` generates such a drop down menu using a
          * SELECT input with the references as its OPTION elements.
@@ -369,6 +369,10 @@ var form_elements = new function () {
          * defined by `label`. If the `label` property is undefined, the `name`
          * is shown instead.
          *
+         * The ReferenceDropDownConfig is a specialisation of a
+         * {@link FieldConfig}.
+         *
+         * @typedef {option} ReferenceDropDownConfig
          * @property {string} name - The name of the select input.
          * @property {string} query - Query for entities.
          * @property {function} [make_value] - Call-back for the generation of
@@ -383,129 +387,9 @@ var form_elements = new function () {
          *     undefined. This property is used by `make_form_field` to decide
          *     which type of field is to be generated.
          *
+         * @see {@link FieldConfig}
          */
 
-        /**
-         * Search and retrieve entities and create a SELECT from element.
-         *
-         * @param {ReferenceDropDownConfig} config - all necessary parameters
-         *     for the configuration.
-         * @returns {HTMLElement} SELECT element.
-         */
-        this.make_reference_drop_down = function (config) {
-            let ret = $(this._make_field_wrapper(config.name));
-            let label = this._make_input_label_str(config);
-            let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>');
-            let input_col = $('<div class="col-sm-9"/>');
-
-            input_col.append(loading);
-            this._query(config.query).then(async function (entities) {
-                let select = $(await form_elements.make_reference_select(
-                    entities, config.make_desc, config.make_value, config.multiple,
-                    config.value));
-                select.attr("name", config.name);
-                loading.remove();
-                input_col.append(select);
-                form_elements.init_select_picker(ret[0], config.value);
-                ret[0].dispatchEvent(form_elements.field_ready_event);
-                select.change(function () {
-                    ret[0].dispatchEvent(form_elements.field_changed_event);
-                });
-            }).catch(err => {
-                form_elements.logger.error(err);
-                loading.remove();
-                input_col.append(err);
-                ret[0].dispatchEvent(form_elements.field_error_event);
-            });
-
-            return ret.append(label, input_col)[0];
-        }
-
-
-        this.init_select_picker = function (field, value) {
-            caosdb_utils.assert_html_element(field, "parameter `field`");
-            const select = $(field).find("select")[0];
-            const select_picker_options = {};
-            if ($(select).prop("multiple")) {
-                select_picker_options["actionsBox"] = true;
-            }
-            if ($(select).find("option").length > 8) {
-                select_picker_options["liveSearch"] = true;
-                select_picker_options["liveSearchNormalize"] = true;
-                select_picker_options["liveSearchPlaceholder"] = "search...";
-            }
-            $(select).selectpicker(select_picker_options);
-            $(select).selectpicker("val", value);
-            this.init_actions_box(field);
-        }
-
-
-        this.init_actions_box = function (field) {
-            this.logger.trace("enter init_actions_box", field);
-            caosdb_utils.assert_html_element(field, "parameter `field`");
-            const select = $(field).find("select");
-            var actions_box = select.siblings().find(".bs-actionsbox");
-            if (actions_box.length === 0) {
-                actions_box = $(`<div class="bs-actionsbox">
-                            <div class="btn-group btn-group-sm btn-block">
-                                <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button>
-                            </div>
-                        </div>`)
-                    .hide();
-
-                select
-                    .siblings(".dropdown-menu")
-                    .prepend(actions_box);
-
-                field.addEventListener(
-                    form_elements.field_changed_event.type,
-                    (e) => {
-                        if (form_elements.is_set(field)) {
-                            actions_box.show();
-                        } else {
-                            actions_box.hide();
-                        }
-                    }, true);
-
-                actions_box
-                    .find(".bs-deselect-all")
-                    .click((e) => {
-                        select.val(null)
-                            .selectpicker("render")
-                            .parent().toggleClass("open", false);
-                        select[0].dispatchEvent(form_elements.field_changed_event);
-                    });
-            }
-        }
-
-        /**
-         * Return a promise which resolves with the field when the field is ready.
-         *
-         * This function is especially useful if the caller can not be sure if
-         * the field_ready_event has been dispatched already and the field is
-         * ready or if the fields creation is still pending.
-         *
-         * @param {HTMLElement} field
-         * @return {Promise} the field-ready promise
-         */
-        this.field_ready = function (field) {
-            // TODO add support for field name (string) as field parameter
-            // TODO check type of param field (not an array!)
-            caosdb_utils.assert_html_element(field, "parameter `field`");
-            return new Promise(function (resolve, reject) {
-                try {
-                    if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) {
-                        resolve(field);
-                    } else {
-                        field.addEventListener(form_elements.field_ready_event.type,
-                            (e) => resolve(e.target), true);
-                    }
-                } catch (err) {
-                    reject(err);
-                }
-            });
-        }
-
         this._query = async function (q) {
             const result = await query(q);
             this.logger.debug("query returned", result);
@@ -527,802 +411,978 @@ var form_elements = new function () {
             return this.parse_script_result(result);
         }
 
-        this.parse_script_result = function (result) {
-            console.log(result);
-            const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
-            const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+    }
 
-            const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
-            const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
-            const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+    /**
+     * @typedef {object} ScriptingResult
+     * @property {string} code
+     * @property {string} call
+     * @property {string} stdout
+     * @property {string} stderr
+     */
 
-            return {
-                "code": code,
-                "call": call,
-                "stdout": stdout,
-                "stderr": stderr
-            };
+    /**
+     * Bla, TODO
+     *
+     * @param {XMLDocument} result
+     * @return {ScriptingResult}
+     */
+    this.parse_script_result = function (result) {
+        console.log(result);
+        const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
+        const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+
+        const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+        const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+        const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+
+        const ret = {
+            "code": code,
+            "call": call,
+            "stdout": stdout,
+            "stderr": stderr
+        };
+
+        return ret;
+    }
+
+    /**
+     * Search and retrieve entities and create a SELECT from element.
+     *
+     * @param {ReferenceDropDownConfig} config - all necessary parameters
+     *     for the configuration.
+     * @returns {HTMLElement} SELECT element.
+     */
+    this.make_reference_drop_down = function (config) {
+        let ret = $(this._make_field_wrapper(config.name));
+        let label = this._make_input_label_str(config);
+        let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>');
+        let input_col = $('<div class="col-sm-9"/>');
+
+        input_col.append(loading);
+        this._query(config.query).then(async function (entities) {
+            let select = $(await form_elements.make_reference_select(
+                entities, config.make_desc, config.make_value, config.multiple,
+                config.value));
+            select.attr("name", config.name);
+            loading.remove();
+            input_col.append(select);
+            form_elements.init_select_picker(ret[0], config.value);
+            ret[0].dispatchEvent(form_elements.field_ready_event);
+            select.change(function () {
+                ret[0].dispatchEvent(form_elements.field_changed_event);
+            });
+        }).catch(err => {
+            form_elements.logger.error(err);
+            loading.remove();
+            input_col.append(err);
+            ret[0].dispatchEvent(form_elements.field_error_event);
+        });
+
+        return ret.append(label, input_col)[0];
+    }
+
+
+    /**
+     * Test 16
+     */
+    this.init_select_picker = function (field, value) {
+        caosdb_utils.assert_html_element(field, "parameter `field`");
+        const select = $(field).find("select")[0];
+        const select_picker_options = {};
+        if ($(select).prop("multiple")) {
+            select_picker_options["actionsBox"] = true;
         }
+        if ($(select).find("option").length > 8) {
+            select_picker_options["liveSearch"] = true;
+            select_picker_options["liveSearchNormalize"] = true;
+            select_picker_options["liveSearchPlaceholder"] = "search...";
+        }
+        $(select).selectpicker(select_picker_options);
+        $(select).selectpicker("val", value);
+        this.init_actions_box(field);
+    }
 
-        /**
-         * generate a java script object representation of a form
-         */
-        this.form_to_object = function (form) {
-            this.logger.trace("entity form_to_json", form);
-            caosdb_utils.assert_html_element(form, "parameter `form`");
 
-            const _to_json = (element, data) => {
-                this.logger.trace("enter element_to_json", element, data);
+    /**
+     * Test 17
+     */
+    this.init_actions_box = function (field) {
+        this.logger.trace("enter init_actions_box", field);
+        caosdb_utils.assert_html_element(field, "parameter `field`");
+        const select = $(field).find("select");
+        var actions_box = select.siblings().find(".bs-actionsbox");
+        if (actions_box.length === 0) {
+            actions_box = $(`<div class="bs-actionsbox">
+                        <div class="btn-group btn-group-sm btn-block">
+                            <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button>
+                        </div>
+                    </div>`)
+                .hide();
+
+            select
+                .siblings(".dropdown-menu")
+                .prepend(actions_box);
+
+            field.addEventListener(
+                form_elements.field_changed_event.type,
+                (e) => {
+                    if (form_elements.is_set(field)) {
+                        actions_box.show();
+                    } else {
+                        actions_box.hide();
+                    }
+                }, true);
 
-                for (const child of element.children) {
-                    // ignore disabled fields and subforms
-                    if ($(child).hasClass("caosdb-f-field-disabled")) {
-                        continue;
+            actions_box
+                .find(".bs-deselect-all")
+                .click((e) => {
+                    select.val(null)
+                        .selectpicker("render")
+                        .parent().toggleClass("open", false);
+                    select[0].dispatchEvent(form_elements.field_changed_event);
+                });
+        }
+    }
+
+    /**
+     * Return a promise which resolves with the field when the field is ready.
+     *
+     * This function is especially useful if the caller can not be sure if
+     * the field_ready_event has been dispatched already and the field is
+     * ready or if the fields creation is still pending.
+     *
+     * @param {HTMLElement} field
+     * @return {Promise} the field-ready promise
+     */
+    this.field_ready = function (field) {
+        // TODO add support for field name (string) as field parameter
+        // TODO check type of param field (not an array!)
+        caosdb_utils.assert_html_element(field, "parameter `field`");
+        return new Promise(function (resolve, reject) {
+            try {
+                if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) {
+                    resolve(field);
+                } else {
+                    field.addEventListener(form_elements.field_ready_event.type,
+                        (e) => resolve(e.target), true);
+                }
+            } catch (err) {
+                reject(err);
+            }
+        });
+    }
+
+    /**
+     * generate a java script object representation of a form
+     *
+     * @function
+     */
+    this.form_to_object = function (form) {
+        this.logger.trace("entity form_to_json", form);
+        caosdb_utils.assert_html_element(form, "parameter `form`");
+
+        const _to_json = (element, data) => {
+            this.logger.trace("enter element_to_json", element, data);
+
+            for (const child of element.children) {
+                // ignore disabled fields and subforms
+                if ($(child).hasClass("caosdb-f-field-disabled")) {
+                    continue;
+                }
+                const name = $(child).attr("name");
+                const is_subform = $(child).hasClass("caosdb-f-form-elements-subform");
+                if (is_subform) {
+                    const subform = $(child).data("subform-name");
+                    // recursive
+                    var subform_obj = _to_json(child, {});
+                    if (typeof data[subform] === "undefined") {
+                        data[subform] = subform_obj;
+                    } else if (Array.isArray(data[subform])) {
+                        data[subform].push(subform_obj);
+                    } else {
+                        data[subform] = [data[subform], subform_obj]
                     }
-                    const name = $(child).attr("name");
-                    const is_subform = $(child).hasClass("caosdb-f-form-elements-subform");
-                    if (is_subform) {
-                        const subform = $(child).data("subform-name");
-                        // recursive
-                        var subform_obj = _to_json(child, {});
-                        if (typeof data[subform] === "undefined") {
-                            data[subform] = subform_obj;
-                        } else if (Array.isArray(data[subform])) {
-                            data[subform].push(subform_obj);
-                        } else {
-                            data[subform] = [data[subform], subform_obj]
-                        }
-                    } else if (name && name !== "") {
-                        // input elements
-                        const not_checkbox = !$(child).is(":checkbox");
-                        if (not_checkbox || $(child).is(":checked")) {
-                            // checked or not a checkbox
-                            var value = $(child).val();
-                            if (typeof data[name] === "undefined") {
-                                data[name] = value;
-                            } else if (Array.isArray(data[name])) {
-                                data[name].push(value);
-                            } else {
-                                data[name] = [data[name], value]
-                            }
+                } else if (name && name !== "") {
+                    // input elements
+                    const not_checkbox = !$(child).is(":checkbox");
+                    if (not_checkbox || $(child).is(":checked")) {
+                        // checked or not a checkbox
+                        var value = $(child).val();
+                        if (typeof data[name] === "undefined") {
+                            data[name] = value;
+                        } else if (Array.isArray(data[name])) {
+                            data[name].push(value);
                         } else {
-                            // TODO checkbox
+                            data[name] = [data[name], value]
                         }
-                    } else if (child.children.length > 0) {
-                        // recursive
-                        _to_json(child, data);
+                    } else {
+                        // TODO checkbox
                     }
+                } else if (child.children.length > 0) {
+                    // recursive
+                    _to_json(child, data);
                 }
+            }
 
-                this.logger.trace("leave element_to_json", element, data);
-                return data;
-            };
-
-            const ret = _to_json(form, {});
-            this.logger.trace("leave form_to_json", ret);
-            return ret;
-        }
+            this.logger.trace("leave element_to_json", element, data);
+            return data;
+        };
 
-        this.make_submit_button = function () {
-            var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>');
-            return ret[0];
-        }
+        const ret = _to_json(form, {});
+        this.logger.trace("leave form_to_json", ret);
+        return ret;
+    }
 
-        this.make_cancel_button = function (form) {
-            var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>');
-            ret.on("click", e => {
-                this.logger.debug("cancel form", e, form);
-                form.dispatchEvent(this.cancel_form_event);
-            });
-            return ret[0];
-        }
+    this.make_submit_button = function () {
+        var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>');
+        return ret[0];
+    }
 
-        /**
-         * TODO make syncronous
-         */
-        this.make_form_field = async function (config) {
-            caosdb_utils.assert_type(config, "object", "param `config`");
-            caosdb_utils.assert_string(config.type, "`config.type` of param `config`");
-
-            var field = undefined;
-            const type = config.type;
-            if (type === "date") {
-                field = this.make_date_input(config);
-            } else if (type === "checkbox") {
-                field = this.make_checkbox_input(config);
-            } else if (type === "text") {
-                field = this.make_text_input(config);
-            } else if (type === "double") {
-                field = this.make_double_input(config);
-            } else if (type === "integer") {
-                field = this.make_integer_input(config);
-            } else if (type === "range") {
-                field = await this.make_range_input(config);
-            } else if (type === "reference_drop_down") {
-                field = this.make_reference_drop_down(config);
-            } else if (type === "subform") {
-                // TODO handle cache and required for subforms
-                return await this.make_subform(config);
-            } else {
-                throw new TypeError("undefined field type `" + type + "`");
-            }
+    this.make_cancel_button = function (form) {
+        var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>');
+        ret.on("click", e => {
+            this.logger.debug("cancel form", e, form);
+            form.dispatchEvent(this.cancel_form_event);
+        });
+        return ret[0];
+    }
 
-            if (config.required) {
-                this.set_required(field);
-            }
-            if (config.cached) {
-                this.set_cached(field);
-            }
-            if (config.help) {
-                this.add_help(field, config.help);
-            }
+    /**
+     * TODO make syncronous
+     *
+     * @return {HTMLElement}
+     */
+    this.make_form_field = async function (config) {
+        caosdb_utils.assert_type(config, "object", "param `config`");
+        caosdb_utils.assert_string(config.type, "`config.type` of param `config`");
+
+        var field = undefined;
+        const type = config.type;
+        if (type === "date") {
+            field = this.make_date_input(config);
+        } else if (type === "checkbox") {
+            field = this.make_checkbox_input(config);
+        } else if (type === "text") {
+            field = this.make_text_input(config);
+        } else if (type === "double") {
+            field = this.make_double_input(config);
+        } else if (type === "integer") {
+            field = this.make_integer_input(config);
+        } else if (type === "range") {
+            field = await this.make_range_input(config);
+        } else if (type === "reference_drop_down") {
+            field = this.make_reference_drop_down(config);
+        } else if (type === "subform") {
+            // TODO handle cache and required for subforms
+            return await this.make_subform(config);
+        } else {
+            throw new TypeError("undefined field type `" + type + "`");
+        }
 
-            return field;
+        if (config.required) {
+            this.set_required(field);
+        }
+        if (config.cached) {
+            this.set_cached(field);
+        }
+        if (config.help) {
+            this.add_help(field, config.help);
         }
 
+        return field;
+    }
 
-        this.add_help = function (field, config) {
-            var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>')
-                .css({
-                    "cursor": "pointer"
-                });
 
-            if (typeof config === "string" || config instanceof String) {
-                help_button.attr("data-content", config);
-                help_button.popover();
-            } else {
-                help_button.popover(config);
-            }
+    this.add_help = function (field, config) {
+        var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>')
+            .css({
+                "cursor": "pointer"
+            });
 
+        if (typeof config === "string" || config instanceof String) {
+            help_button.attr("data-content", config);
+            help_button.popover();
+        } else {
+            help_button.popover(config);
+        }
 
-            var label = $(field).children("label");
-            if (label.length > 0) {
-                help_button.css({
-                    "margin-left": "4px"
-                });
-                label.first().append(help_button);
-            } else {
-                $(field).append(help_button);
-            }
+
+        var label = $(field).children("label");
+        if (label.length > 0) {
+            help_button.css({
+                "margin-left": "4px"
+            });
+            label.first().append(help_button);
+        } else {
+            $(field).append(help_button);
         }
+    }
 
-        this.make_heading = function (config) {
-            if (typeof config.header === "undefined") {
-                return;
-            } else if (typeof config.header === "string" || config.header instanceof String) {
-                return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0];
-            }
-            caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`");
-            return config.header;
+    this.make_heading = function (config) {
+        if (typeof config.header === "undefined") {
+            return;
+        } else if (typeof config.header === "string" || config.header instanceof String) {
+            return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0];
         }
+        caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`");
+        return config.header;
+    }
 
-        this.make_form_wrapper = function (form, config) {
-            var wrapper = $('<div class="caosdb-f-form-wrapper"/>');
+    this.make_form_wrapper = function (form, config) {
+        var wrapper = $('<div class="caosdb-f-form-wrapper"/>');
 
-            var header = this.make_heading(config);
-            wrapper.append(header);
+        var header = this.make_heading(config);
+        wrapper.append(header);
 
-            var loading = $('<div>loading...</div>');
-            var logger = this.logger;
-            var cancel = (e) => {
-                logger.trace("cancel form", e);
-                wrapper.remove();
-            };
+        var loading = $('<div>loading...</div>');
+        var logger = this.logger;
+        var cancel = (e) => {
+            logger.trace("cancel form", e);
+            wrapper.remove();
+        };
 
-            wrapper.append(loading);
+        wrapper.append(loading);
 
-            Promise.resolve(form).then(form => {
-                // form ready
-                loading.remove();
-                wrapper.append(form);
-                wrapper[0].dispatchEvent(this.form_ready_event);
+        Promise.resolve(form).then(form => {
+            // form ready
+            loading.remove();
+            wrapper.append(form);
+            wrapper[0].dispatchEvent(this.form_ready_event);
 
-            }).catch(err => {
-                logger.error("form loading error", err);
-                loading.remove();
-                wrapper.append(err);
-            });
+        }).catch(err => {
+            logger.error("form loading error", err);
+            loading.remove();
+            wrapper.append(err);
+        });
 
-            wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true);
+        wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true);
 
-            return wrapper[0];
-        }
+        return wrapper[0];
+    }
 
-        this.make_form = function (config) {
-            var form = undefined;
+    /**
+     * Configuration objects which are passed to {@link make_form}.
+     *
+     * Note: either the `script` or the `name` property must be defined. If the former is defined, the latter will be overriden.
+     *
+     * @typedef {object} FormConfig
+     *
+     * @property {FieldConfig[]} fields - array of fields. The order is the
+     *     order in which they appear in the resulting form.
+     * @property {string} [script] - if present the form will call a
+     *     server-side script on submission.
+     * @property {string} [name] - The name of the form. This is being
+     *     overridden by the `script` parameter if present.
+     * @property {function} [submit] - a callback which handles the submission
+     *     of the form. This parameter is being overridden if the `script`
+     *     parameter is present.
+     */
 
-            if (config.script) {
-                form = this.make_script_form(config, config.script);
-            } else {
-                form = this.make_generic_form(config);
-            }
-            var wrapper = this.make_form_wrapper(form, config);
-            return wrapper;
-        }
+    /**
+     * Create a form.
+     *
+     * The returned element is a container which will eventually contain a HTML
+     * form element. The container emits a {@link form_ready_event} when the
+     * form is ready.
+     *
+     * @param {FormConfig} config
+     * @return {HTMLElement}
+     */
+    this.make_form = function (config) {
+        var form = undefined;
 
-        /**
-         * TODO make syncronous
-         */
-        this.make_subform = async function (config) {
-            this.logger.trace("enter make_subform");
-            caosdb_utils.assert_type(config, "object", "param `config`");
-            caosdb_utils.assert_string(config.name, "`config.name` of param `config`");
-            caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
+        if (config.script) {
+            form = this.make_script_form(config, config.script);
+        } else {
+            form = this.make_generic_form(config);
+        }
+        var wrapper = this.make_form_wrapper(form, config);
+        return wrapper;
+    }
 
-            const name = config.name;
-            var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>');
+    /**
+     * TODO make syncronous
+     */
+    this.make_subform = async function (config) {
+        this.logger.trace("enter make_subform");
+        caosdb_utils.assert_type(config, "object", "param `config`");
+        caosdb_utils.assert_string(config.name, "`config.name` of param `config`");
+        caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
+
+        const name = config.name;
+        var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>');
+
+        for (let field of config.fields) {
+            this.logger.trace("add subform field", field);
+            let elem = await this.make_form_field(field);
+            form.append(elem);
+        }
 
-            for (let field of config.fields) {
-                this.logger.trace("add subform field", field);
-                let elem = await this.make_form_field(field);
-                form.append(elem);
-            }
+        this.logger.trace("leave make_subform", form[0]);
+        return form[0];
+    }
 
-            this.logger.trace("leave make_subform", form[0]);
-            return form[0];
+    this.dismiss_form = function (form) {
+        if (form.tagName === "FORM") {
+            form.dispatchEvent(this.cancel_form_event);
         }
-
-        this.dismiss_form = function (form) {
-            if (form.tagName === "FORM") {
-                form.dispatchEvent(this.cancel_form_event);
-            }
-            var _form = $(form).find("form");
-            if (_form.length > 0) {
-                _form[0].dispatchEvent(this.cancel_form_event);
-            }
+        var _form = $(form).find("form");
+        if (_form.length > 0) {
+            _form[0].dispatchEvent(this.cancel_form_event);
         }
+    }
 
-        this.enable_group = function (form, group) {
-            this.enable_fields(this.get_group_fields(form, group));
-        }
+    this.enable_group = function (form, group) {
+        this.enable_fields(this.get_group_fields(form, group));
+    }
 
-        this.disable_group = function (form, group) {
-            this.disable_fields(this.get_group_fields(form, group));
-        }
+    this.disable_group = function (form, group) {
+        this.disable_fields(this.get_group_fields(form, group));
+    }
 
-        this.get_group_fields = function (form, group) {
-            return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray();
-        }
+    this.get_group_fields = function (form, group) {
+        return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray();
+    }
 
-        /**
-         * Return an array of field with name
-         *
-         * @param {string} name - the field name
-         * @return {HTMLElement[]} array of fields
-         */
-        this.get_fields = function (form, name) {
-            caosdb_utils.assert_html_element(form, "parameter `form`");
-            caosdb_utils.assert_string(name, "parameter `name`");
-            return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray();
-        }
+    /**
+     * Return an array of field with name
+     *
+     * @param {string} name - the field name
+     * @return {HTMLElement[]} array of fields
+     */
+    this.get_fields = function (form, name) {
+        caosdb_utils.assert_html_element(form, "parameter `form`");
+        caosdb_utils.assert_string(name, "parameter `name`");
+        return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray();
+    }
 
-        this.add_field_to_group = function (field, group) {
-            this.logger.trace("enter add_field_to_group", field, group);
-            var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")";
-            $(field).attr("data-groups", groups);
-        }
+    this.add_field_to_group = function (field, group) {
+        this.logger.trace("enter add_field_to_group", field, group);
+        var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")";
+        $(field).attr("data-groups", groups);
+    }
 
-        this.disable_fields = function (fields) {
-            $(fields).toggleClass("caosdb-f-field-disabled", true).hide();
-            for (const field of $(fields)) {
-                field.dispatchEvent(this.field_disabled_event);
-            }
+    this.disable_fields = function (fields) {
+        $(fields).toggleClass("caosdb-f-field-disabled", true).hide();
+        for (const field of $(fields)) {
+            field.dispatchEvent(this.field_disabled_event);
         }
+    }
 
-        this.enable_fields = function (fields) {
-            $(fields).toggleClass("caosdb-f-field-disabled", false).show();
-            for (const field of $(fields)) {
-                field.dispatchEvent(this.field_enabled_event);
-            }
+    this.enable_fields = function (fields) {
+        $(fields).toggleClass("caosdb-f-field-disabled", false).show();
+        for (const field of $(fields)) {
+            field.dispatchEvent(this.field_enabled_event);
         }
+    }
 
-        this.enable_name = function (form, name) {
-            this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
-        }
+    this.enable_name = function (form, name) {
+        this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
+    }
 
-        this.disable_name = function (form, name) {
-            this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
-        }
+    this.disable_name = function (form, name) {
+        this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
+    }
 
-        this.make_script_form = async function (config, script) {
-            this.logger.trace("enter make_script_form");
+    this.make_script_form = async function (config, script) {
+        this.logger.trace("enter make_script_form");
 
-            const submit_callback = async function (form) {
-                form = $(form);
+        const submit_callback = async function (form) {
+            form = $(form);
 
 
-                // actually submit the form
-                var response = await form_elements._run_script(script, form);
-                var result = [];
+            // actually submit the form
+            var response = await form_elements._run_script(script, form);
+            var result = [];
 
-                if (response.code === "0") {
-                    // handle success
-                    result.push(form_elements.make_success_message(response.stdout));
-                    return result;
+            if (response.code === "0") {
+                // handle success
+                result.push(form_elements.make_success_message(response.stdout));
+                return result;
 
-                } else {
-                    // handle scripting error
-                    result.push(form_elements.make_error_message(response.call));
-                    result.push(form_elements.make_error_message(response.stderr));
-                    throw result;
-                }
-            };
+            } else {
+                // handle scripting error
+                result.push(form_elements.make_error_message(response.call));
+                result.push(form_elements.make_error_message(response.stderr));
+                throw result;
+            }
+        };
+
+        this.logger.trace("leave make_script_form");
+        const new_config = $.extend({}, {
+            name: script,
+            submit: submit_callback
+        }, config);
+        return await this.make_generic_form(new_config);
+    }
 
-            this.logger.trace("leave make_script_form");
-            const new_config = $.extend({}, {
-                name: script,
-                submit: submit_callback
-            }, config);
-            return await this.make_generic_form(new_config);
-        }
+    /**
+     * Return a generic form, bind the config.submit to the submit event
+     * of the form.
+     *
+     * The `config.fields` array may contain `form_elements.field_config`
+     * objects or HTMLElements.
+     *
+     * TODO
+     */
+    this.make_generic_form = async function (config) {
+        this.logger.trace("enter make_generic_form");
 
-        /**
-         * Return a generic form, bind the config.submit to the submit event
-         * of the form.
-         *
-         * The `config.fields` array may contain `form_elements.field_config`
-         * objects or HTMLElements.
-         *
-         * TODO
-         */
-        this.make_generic_form = async function (config) {
-            this.logger.trace("enter make_generic_form");
+        caosdb_utils.assert_type(config, "object", "param `config`");
+        caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true);
+        caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
 
-            caosdb_utils.assert_type(config, "object", "param `config`");
-            caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true);
-            caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
+        const form = $('<form class="form-horizontal" action="#" method="post" />');
 
-            const form = $('<form class="form-horizontal" action="#" method="post" />');
+        // set name
+        if (config.name) {
+            form.attr("name", config.name);
+        }
 
-            // set name
-            if (config.name) {
-                form.attr("name", config.name);
+        // add fields
+        for (let field of config.fields) {
+            this.logger.trace("add field", field);
+            if (field instanceof HTMLElement) {
+                form.append(field);
+            } else {
+                let elem = await this.make_form_field(field);
+                form.append(elem);
             }
+        }
+
+        // set groups
+        if (config.groups) {
+            for (let group of config.groups) {
+                this.logger.trace("add group", group);
+                for (let fieldname of group.fields) {
+                    let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']");
+                    this.logger.trace("set group", field, group);
+                    this.add_field_to_group(field, group.name)
 
-            // add fields
-            for (let field of config.fields) {
-                this.logger.trace("add field", field);
-                if (field instanceof HTMLElement) {
-                    form.append(field);
+                }
+                // disable if necessary
+                if (typeof group.enabled === "undefined" || group.enabled) {
+                    this.enable_group(form, group.name);
                 } else {
-                    let elem = await this.make_form_field(field);
-                    form.append(elem);
+                    this.disable_group(form, group.name);
                 }
             }
+        }
 
-            // set groups
-            if (config.groups) {
-                for (let group of config.groups) {
-                    this.logger.trace("add group", group);
-                    for (let fieldname of group.fields) {
-                        let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']");
-                        this.logger.trace("set group", field, group);
-                        this.add_field_to_group(field, group.name)
+        const footer = this.make_footer();
+        form.append(footer);
 
-                    }
-                    // disable if necessary
-                    if (typeof group.enabled === "undefined" || group.enabled) {
-                        this.enable_group(form, group.name);
-                    } else {
-                        this.disable_group(form, group.name);
-                    }
-                }
+        if (!(typeof config.submit === 'boolean' && config.submit === false)) {
+            // add submit button unless config.submit is false
+            footer.append(this.make_submit_button());
+        }
+        form[0].addEventListener("submit", (e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            if (form.find(".caosdb-f-form-submitting").length > 0) {
+                // do not submit twice
+                return;
             }
 
-            const footer = this.make_footer();
-            form.append(footer);
-
-            if (!(typeof config.submit === 'boolean' && config.submit === false)) {
-                // add submit button unless config.submit is false
-                footer.append(this.make_submit_button());
-            }
-            form[0].addEventListener("submit", (e) => {
-                e.preventDefault();
-                e.stopPropagation();
-                if (form.find(".caosdb-f-form-submitting").length > 0) {
-                    // do not submit twice
-                    return;
-                }
+            this.logger.debug("submit form", e);
 
-                this.logger.debug("submit form", e);
+            form[0].dispatchEvent(this.submit_form_event);
 
-                form[0].dispatchEvent(this.submit_form_event);
+            form.find(":input").prop("disabled", true);
+            var submitting = form_elements.make_submitting_info();
+            form.find(".caosdb-f-form-elements-footer").before(submitting);
 
-                form.find(":input").prop("disabled", true);
-                var submitting = form_elements.make_submitting_info();
-                form.find(".caosdb-f-form-elements-footer").before(submitting);
 
+            form[0].addEventListener(this.form_success_event.type, (e) => {
+                submitting.remove();
+            }, true);
+            form[0].addEventListener(this.form_error_event.type, (e) => {
+                submitting.remove();
+            }, true);
 
-                form[0].addEventListener(this.form_success_event.type, (e) => {
-                    submitting.remove();
-                }, true);
-                form[0].addEventListener(this.form_error_event.type, (e) => {
-                    submitting.remove();
-                }, true);
 
+            // remove old messages
+            const error_handler = config.error;
+            const success_handler = config.success;
+            const submit_callback = config.submit;
+            form.find(".caosdb-f-form-elements-message").remove();
+            if (typeof config.submit === "function") {
+                // wrap callback in async function
+                const _wrap_callback = async function () {
+                    try {
+                        var results = await submit_callback(form[0]);
 
-                // remove old messages
-                const error_handler = config.error;
-                const success_handler = config.success;
-                const submit_callback = config.submit;
-                form.find(".caosdb-f-form-elements-message").remove();
-                if (typeof config.submit === "function") {
-                    // wrap callback in async function
-                    const _wrap_callback = async function () {
-                        try {
-                            var results = await submit_callback(form[0]);
-
-                            // success_handler
-                            if (typeof success_handler === "function") {
-                                var processed = await success_handler(form[0], results);
-                                if (typeof processed !== "undefined") {
-                                    form_elements.show_results(form[0], processed);
-                                }
-                            } else {
-                                form_elements.show_results(form[0], results);
+                        // success_handler
+                        if (typeof success_handler === "function") {
+                            var processed = await success_handler(form[0], results);
+                            if (typeof processed !== "undefined") {
+                                form_elements.show_results(form[0], processed);
                             }
+                        } else {
+                            form_elements.show_results(form[0], results);
+                        }
 
-                            form[0].dispatchEvent(form_elements.form_success_event);
-                        } catch (err) {
-
-                            // error_handler
-                            if (typeof error_handler === "function") {
-                                var processed = await error_handler(form[0], err);
-                                if (typeof processed !== "undefined") {
-                                    form_elements.show_results(form[0], processed);
-                                }
-                            } else {
-                                form_elements.show_errors(form[0], err);
-                            }
+                        form[0].dispatchEvent(form_elements.form_success_event);
+                    } catch (err) {
 
-                            form[0].dispatchEvent(form_elements.form_error_event);
+                        // error_handler
+                        if (typeof error_handler === "function") {
+                            var processed = await error_handler(form[0], err);
+                            if (typeof processed !== "undefined") {
+                                form_elements.show_results(form[0], processed);
+                            }
+                        } else {
+                            form_elements.show_errors(form[0], err);
                         }
 
-                    }();
-                }
-                return false;
+                        form[0].dispatchEvent(form_elements.form_error_event);
+                    }
 
+                }();
+            }
+            return false;
 
-            }, true);
 
-            form[0].addEventListener(this.form_success_event.type, function (e) {
-                // remove submit button, show ok button
-                form.find("button[type='submit']").remove();
-                form.find("button:contains('Cancel')").text("Ok").prop("disabled", false);
-            }, true);
-            form[0].addEventListener(this.form_error_event.type, function (e) {
-                // reenable inputs
-                form.find(":input").prop("disabled", false);
-            }, true);
+        }, true);
 
-            // add cancel button
-            $(footer).append(this.make_cancel_button(form[0]));
+        form[0].addEventListener(this.form_success_event.type, function (e) {
+            // remove submit button, show ok button
+            form.find("button[type='submit']").remove();
+            form.find("button:contains('Cancel')").text("Ok").prop("disabled", false);
+        }, true);
+        form[0].addEventListener(this.form_error_event.type, function (e) {
+            // reenable inputs
+            form.find(":input").prop("disabled", false);
+        }, true);
 
-            // init caching for this form
-            form_elements.init_form_caching(config, form[0]);
+        // add cancel button
+        $(footer).append(this.make_cancel_button(form[0]));
 
-            // init validation
-            form_elements.init_validator(form[0]);
+        // init caching for this form
+        form_elements.init_form_caching(config, form[0]);
 
-            this.logger.trace("leave make_generic_form");
-            return form[0];
-        }
+        // init validation
+        form_elements.init_validator(form[0]);
 
-        this.init_form_caching = function (config, form) {
-            var default_config = {
-                "cache_event": form_elements.submit_form_event.type,
-                "cache_storage": localStorage
-            };
-            var lconfig = $.extend({}, default_config, config);
+        this.logger.trace("leave make_generic_form");
+        return form[0];
+    }
 
-            this.logger.trace("init_form_caching", lconfig, form);
+    this.init_form_caching = function (config, form) {
+        var default_config = {
+            "cache_event": form_elements.submit_form_event.type,
+            "cache_storage": localStorage
+        };
+        var lconfig = $.extend({}, default_config, config);
 
-            form.addEventListener(lconfig.cache_event, (e) => {
-                form_elements.cache_form(lconfig.cache_storage, form);
-            }, true);
-            form_elements.load_cached(lconfig.cache_storage, form);
-        }
+        this.logger.trace("init_form_caching", lconfig, form);
 
-        this.show_results = function (form, results) {
-            $(form).append(results);
-        }
+        form.addEventListener(lconfig.cache_event, (e) => {
+            form_elements.cache_form(lconfig.cache_storage, form);
+        }, true);
+        form_elements.load_cached(lconfig.cache_storage, form);
+    }
 
-        this.show_errors = function (form, errors) {
-            $(form).append(errors);
-        }
+    this.show_results = function (form, results) {
+        $(form).append(results);
+    }
 
-        this.make_footer = function () {
-            return $('<div class="text-right caosdb-f-form-elements-footer"/>')
-                .css({
-                    "margin": "20px",
-                }).append(this.make_required_marker())
-                .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0];
-        }
+    this.show_errors = function (form, errors) {
+        $(form).append(errors);
+    }
 
-        this.make_error_message = function (message) {
-            return this.make_message(message, "error");
-        }
+    this.make_footer = function () {
+        return $('<div class="text-right caosdb-f-form-elements-footer"/>')
+            .css({
+                "margin": "20px",
+            }).append(this.make_required_marker())
+            .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0];
+    }
 
-        this.make_success_message = function (message) {
-            return this.make_message(message, "success");
-        }
+    this.make_error_message = function (message) {
+        return this.make_message(message, "error");
+    }
 
-        this.make_submitting_info = function () {
-            // TODO styling
-            return $(this.make_message("Submitting... please wait. This might take some time.", "info"))
-                .toggleClass("h3", true)
-                .toggleClass("caosdb-f-form-submitting", true)
-                .toggleClass("text-right", true)[0];
-        }
+    this.make_success_message = function (message) {
+        return this.make_message(message, "success");
+    }
 
-        this.make_message = function (message, type) {
-            var ret = $('<div class="caosdb-f-form-elements-message"/>');
-            if (type) {
-                ret.addClass("caosdb-f-form-elements-message-" + type);
-            }
-            return ret.append(markdown.textToHtml(message))[0];
+    this.make_submitting_info = function () {
+        // TODO styling
+        return $(this.make_message("Submitting... please wait. This might take some time.", "info"))
+            .toggleClass("h3", true)
+            .toggleClass("caosdb-f-form-submitting", true)
+            .toggleClass("text-right", true)[0];
+    }
+
+    this.make_message = function (message, type) {
+        var ret = $('<div class="caosdb-f-form-elements-message"/>');
+        if (type) {
+            ret.addClass("caosdb-f-form-elements-message-" + type);
         }
+        return ret.append(markdown.textToHtml(message))[0];
+    }
 
-        /**
-         * TODO make syncronous
-         */
-        this.make_range_input = async function (config) {
-
-            // TODO 
-            // 1. wrapp both inputs to separate it from the label into a container
-            // 2. make two rows for each input
-            // 3. make inline-block for all included elements
-            const from_config = $.extend({}, {
-                cached: config.cached,
-                required: config.required,
-                type: "double"
-            }, config.from);
-            const to_config = $.extend({}, {
-                cached: config.cached,
-                required: config.required,
-                type: "double"
-            }, config.to);
-
-            const from_input = await this.make_form_field(from_config);
-            const to_input = await this.make_form_field(to_config);
-
-            const ret = $(this._make_field_wrapper(config.name));
-            if (config.label) {
-                ret.append(this._make_input_label_str(config));
-            }
+    /**
+     * TODO make syncronous
+     */
+    this.make_range_input = async function (config) {
+
+        // TODO 
+        // 1. wrapp both inputs to separate it from the label into a container
+        // 2. make two rows for each input
+        // 3. make inline-block for all included elements
+        const from_config = $.extend({}, {
+            cached: config.cached,
+            required: config.required,
+            type: "double"
+        }, config.from);
+        const to_config = $.extend({}, {
+            cached: config.cached,
+            required: config.required,
+            type: "double"
+        }, config.to);
+
+        const from_input = await this.make_form_field(from_config);
+        const to_input = await this.make_form_field(to_config);
+
+        const ret = $(this._make_field_wrapper(config.name));
+        if (config.label) {
+            ret.append(this._make_input_label_str(config));
+        }
 
-            ret.append(from_input);
-            ret.append(to_input);
+        ret.append(from_input);
+        ret.append(to_input);
 
-            // styling
-            $(from_input).toggleClass("form-group", false);
-            $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1");
-            $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
-            $(to_input).toggleClass("form-group", false);
-            $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1");
-            $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
+        // styling
+        $(from_input).toggleClass("form-group", false);
+        $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1");
+        $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
+        $(to_input).toggleClass("form-group", false);
+        $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1");
+        $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
 
-            return ret[0];
-        }
+        return ret[0];
+    }
 
-        /**
-         * Return a DIV with class `caosdb-f-field` and a data attribute
-         * `data-field-name` which contains the name.
-         *
-         * The DIV is used to wrap LABEL and INPUT elements of a form together.
-         *
-         * @param {string} name - the name of the field.
-         * @returns {HTMLElement} a DIV.
-         */
-        this._make_field_wrapper = function (name) {
-            caosdb_utils.assert_string(name, "param `name`");
-            return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />')
-                .css({"padding": "0"})[0];
-        }
+    /**
+     * Return a DIV with class `caosdb-f-field` and a data attribute
+     * `data-field-name` which contains the name.
+     *
+     * The DIV is used to wrap LABEL and INPUT elements of a form together.
+     *
+     * @param {string} name - the name of the field.
+     * @returns {HTMLElement} a DIV.
+     */
+    this._make_field_wrapper = function (name) {
+        caosdb_utils.assert_string(name, "param `name`");
+        return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />')
+            .css({"padding": "0"})[0];
+    }
 
-        this.make_date_input = function (config) {
-            return this._make_input(config);
-        }
+    this.make_date_input = function (config) {
+        return this._make_input(config);
+    }
 
-        this.make_text_input = function (config) {
-            return this._make_input(config);
-        }
+    this.make_text_input = function (config) {
+        return this._make_input(config);
+    }
 
 
-        /**
-         * Return an input field which accepts double values.
-         *
-         * `config.type` is set to "number" and overrides any other type.
-         *
-         * @param {form_elements.input_config} config.
-         * @returns {HTMLElement} a double form field.
-         */
-        this.make_double_input = function (config) {
-            var clone = $.extend({}, config, {
-                type: "number"
-            });
-            var ret = $(this._make_input(clone))
-            ret.find("input").attr("step", "any");
-            return ret[0];
-        }
+    /**
+     * Return an input field which accepts double values.
+     *
+     * `config.type` is set to "number" and overrides any other type.
+     *
+     * @param {form_elements.input_config} config.
+     * @returns {HTMLElement} a double form field.
+     */
+    this.make_double_input = function (config) {
+        var clone = $.extend({}, config, {
+            type: "number"
+        });
+        var ret = $(this._make_input(clone))
+        ret.find("input").attr("step", "any");
+        return ret[0];
+    }
 
 
-        /**
-         * Return an input field which accepts integers.
-         *
-         * `config.type` is set to "number" and overrides any other type.
-         *
-         * @param {form_elements.input_config} config.
-         * @returns {HTMLElement} an integer form field.
-         */
-        this.make_integer_input = function (config) {
-            var ret = $(this.make_double_input(config));
-            ret.find("input").attr("step", "1");
-            return ret[0];
-        }
+    /**
+     * Return an input field which accepts integers.
+     *
+     * `config.type` is set to "number" and overrides any other type.
+     *
+     * @param {form_elements.input_config} config.
+     * @returns {HTMLElement} an integer form field.
+     */
+    this.make_integer_input = function (config) {
+        var ret = $(this.make_double_input(config));
+        ret.find("input").attr("step", "1");
+        return ret[0];
+    }
 
 
-        /**
-         * Return a checkbox input field.
-         *
-         * @param {form_elements.checkbox_config} config.
-         * @returns {HTMLElement} a checkbox form field.
-         */
-        this.make_checkbox_input = function (config) {
-            var clone = $.extend({}, config, {
-                type: "checkbox"
-            });
-            var ret = $(this._make_input(clone));
-            ret.find("input:checkbox").prop("checked", false);
-            ret.find("input:checkbox").toggleClass("form-control", false);
-            if (config.checked) {
-                ret.find("input:checkbox").prop("checked", true);
-                ret.find("input:checkbox").attr("checked", "checked");
-            }
-            if (config.value) {
-                ret.find("input:checkbox").attr("value", config.value);
-            }
-            return ret[0];
+    /**
+     * Return a checkbox input field.
+     *
+     * @param {form_elements.checkbox_config} config.
+     * @returns {HTMLElement} a checkbox form field.
+     */
+    this.make_checkbox_input = function (config) {
+        var clone = $.extend({}, config, {
+            type: "checkbox"
+        });
+        var ret = $(this._make_input(clone));
+        ret.find("input:checkbox").prop("checked", false);
+        ret.find("input:checkbox").toggleClass("form-control", false);
+        if (config.checked) {
+            ret.find("input:checkbox").prop("checked", true);
+            ret.find("input:checkbox").attr("checked", "checked");
+        }
+        if (config.value) {
+            ret.find("input:checkbox").attr("value", config.value);
         }
+        return ret[0];
+    }
 
 
-        /**
-         * Add `caosdb-f-form-field-required` class to form field.
-         *
-         * @param {HTMLElement} field - the required form field.
-         */
-        this.set_required = function (field) {
-            $(field).toggleClass("caosdb-f-form-field-required", true);
-            $(field).find(":input").prop("required", true);
-            $(field).find("label").prepend(this.make_required_marker());
-        }
+    /**
+     * Add `caosdb-f-form-field-required` class to form field.
+     *
+     * @param {HTMLElement} field - the required form field.
+     */
+    this.set_required = function (field) {
+        $(field).toggleClass("caosdb-f-form-field-required", true);
+        $(field).find(":input").prop("required", true);
+        $(field).find("label").prepend(this.make_required_marker());
+    }
 
-        /**
-         * Return a span which is to be inserted before a field's label text
-         * and which marks that field as required.
-         *
-         * @returns {HTMLElement} span element.
-         */
-        this.make_required_marker = function () {
-            // TODO create class and move to css file
-            return $('<span>*</span>')
-                .css({
-                    "font-size": "10px",
-                    "color": "red",
-                    "margin-right": "4px",
-                    "font-weight": "100",
-                })[0];
-        }
+    /**
+     * Return a span which is to be inserted before a field's label text
+     * and which marks that field as required.
+     *
+     * @returns {HTMLElement} span element.
+     */
+    this.make_required_marker = function () {
+        // TODO create class and move to css file
+        return $('<span>*</span>')
+            .css({
+                "font-size": "10px",
+                "color": "red",
+                "margin-right": "4px",
+                "font-weight": "100",
+            })[0];
+    }
 
 
-        this.get_enabled_required_fields = function (form) {
-            return $(this.get_enabled_fields(form))
-                .filter(".caosdb-f-form-field-required")
-                .toArray();
-        }
+    this.get_enabled_required_fields = function (form) {
+        return $(this.get_enabled_fields(form))
+            .filter(".caosdb-f-form-field-required")
+            .toArray();
+    }
 
 
-        this.get_enabled_fields = function (form) {
-            return $(form)
-                .find(".caosdb-f-field")
-                .filter(function (idx) {
-                    // remove disabled fields from results
-                    return !$(this).hasClass("caosdb-f-field-disabled");
-                })
-                .toArray();
-        }
+    this.get_enabled_fields = function (form) {
+        return $(form)
+            .find(".caosdb-f-field")
+            .filter(function (idx) {
+                // remove disabled fields from results
+                return !$(this).hasClass("caosdb-f-field-disabled");
+            })
+            .toArray();
+    }
 
 
-        this.all_required_fields_set = function (form) {
-            const req = form_elements.get_enabled_required_fields(form);
-            for (const field of req) {
-                if (!form_elements.is_set(field)) {
-                    return false;
-                }
+    this.all_required_fields_set = function (form) {
+        const req = form_elements.get_enabled_required_fields(form);
+        for (const field of req) {
+            if (!form_elements.is_set(field)) {
+                return false;
             }
-            return true;
         }
+        return true;
+    }
 
-        /**
-         * @param {HTMLElement} form - the form be validated.
-         */
-        this.is_valid = function (form) {
-            return form_elements.all_required_fields_set(form);
-        }
+    /**
+     * @param {HTMLElement} form - the form be validated.
+     */
+    this.is_valid = function (form) {
+        return form_elements.all_required_fields_set(form);
+    }
 
 
-        this.toggle_submit_button_form_valid = function (form, submit) {
-            // TODO do not change the submit button directly. change the
-            // `submittable` state of the form and handle the case where a form
-            // is submitting when this function is called.
-            if (form_elements.is_valid(form)) {
-                $(submit).prop("disabled", false);
-            } else {
-                $(submit).prop("disabled", true);
-            }
+    this.toggle_submit_button_form_valid = function (form, submit) {
+        // TODO do not change the submit button directly. change the
+        // `submittable` state of the form and handle the case where a form
+        // is submitting when this function is called.
+        if (form_elements.is_valid(form)) {
+            $(submit).prop("disabled", false);
+        } else {
+            $(submit).prop("disabled", true);
         }
+    }
 
 
-        this.init_validator = function (form) {
-            const submit = $(form).find(":input[type='submit']")[0];
-            if (submit) {
-                form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
-                form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
-                form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
-            }
+    this.init_validator = function (form) {
+        const submit = $(form).find(":input[type='submit']")[0];
+        if (submit) {
+            form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
+            form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
+            form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
         }
+    }
 
 
-        /**
-         * Return an input and a label, wrapped in a div with class
-         * `caosdb-f-field`.
-         *
-         * @param {object} config - config object with `name`, `type` and
-         *      optional `label`
-         * @returns {HTMLElement} a form field.
-         */
-        this._make_input = function (config) {
-            caosdb_utils.assert_string(config.name, "the name of a form field");
-            let ret = $(this._make_field_wrapper(config.name));
-            let name = config.name;
-            let label = this._make_input_label_str(config);
-            let type = config.type || "text";
-            let value = config.value;
-            let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type +
-                '" name="' + name +
-                '" />');
-            input.change(function () {
-                ret[0].dispatchEvent(form_elements.field_changed_event);
-            });
-            let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>');
-            input_col.append(input);
-            if (value) {
-                input.val(value);
-            }
-            return ret.append(label, input_col)[0];
-        }
-
-        /**
-         * Return a string representation of a LABEL element, ready for parsing.
-         *
-         * This function is used by other functions to generate a LABEL element.
-         *
-         * The config's `name` goes to the `for` attribute, the `label` is the
-         * text node of the resulting LABEL element.
-         *
-         * @param {object} config - a config object with `name` and `label`.
-         * @returns {string} a html string for a LABEL element.
-         */
-        this._make_input_label_str = function (config) {
-            let name = config.name;
-            let label = config.label;
-            return label ? '<label for="' + name +
-                '" data-property-name="' + name +
-                '" class="control-label col-sm-3">' + label +
-                '</label>' : "";
+    /**
+     * Return an input and a label, wrapped in a div with class
+     * `caosdb-f-field`.
+     *
+     * @param {object} config - config object with `name`, `type` and
+     *      optional `label`
+     * @returns {HTMLElement} a form field.
+     */
+    this._make_input = function (config) {
+        caosdb_utils.assert_string(config.name, "the name of a form field");
+        let ret = $(this._make_field_wrapper(config.name));
+        let name = config.name;
+        let label = this._make_input_label_str(config);
+        let type = config.type || "text";
+        let value = config.value;
+        let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type +
+            '" name="' + name +
+            '" />');
+        input.change(function () {
+            ret[0].dispatchEvent(form_elements.field_changed_event);
+        });
+        let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>');
+        input_col.append(input);
+        if (value) {
+            input.val(value);
         }
+        return ret.append(label, input_col)[0];
+    }
 
+    /**
+     * Return a string representation of a LABEL element, ready for parsing.
+     *
+     * This function is used by other functions to generate a LABEL element.
+     *
+     * The config's `name` goes to the `for` attribute, the `label` is the
+     * text node of the resulting LABEL element.
+     *
+     * @param {object} config - a config object with `name` and `label`.
+     * @returns {string} a html string for a LABEL element.
+     */
+    this._make_input_label_str = function (config) {
+        let name = config.name;
+        let label = config.label;
+        return label ? '<label for="' + name +
+            '" data-property-name="' + name +
+            '" class="control-label col-sm-3">' + label +
+            '</label>' : "";
     }
+
     this._init_functions();
 }
 
diff --git a/src/doc/Makefile b/src/doc/Makefile
index b3c35b4d..f3519f27 100644
--- a/src/doc/Makefile
+++ b/src/doc/Makefile
@@ -33,8 +33,9 @@ BUILDDIR       = ../../build/doc
 
 # npm is not always in the global PATH
 NPM_PATH = $(shell npm bin)
+NPM_PREFIX = $(shell npm prefix)
 
-.PHONY: doc-help Makefile apidoc
+.PHONY: doc-help Makefile api
 
 # Put it first so that "make" without argument is like "make help".
 doc-help:
@@ -42,10 +43,9 @@ doc-help:
 
 # Catch-all target: route all unknown targets to Sphinx using the new
 # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
+%: Makefile api
 	PATH=$(NPM_PATH):$$PATH $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 #	sphinx-build -M html . ../../build/doc
 
-# Not necessary in this repository, apidoc is doone with the sphinx-autoapi extension
-# apidoc:
-# 	@$(SPHINXAPIDOC) -o _apidoc --update --title="CaosDB Server" ../main/
+api:
+	PATH=$(NPM_PATH):$$PATH jsdoc -t $(NPM_PREFIX)/node_modules/jsdoc-sphinx/template -d $@ -r "../../src/core"
diff --git a/src/doc/conf.py b/src/doc/conf.py
index 988df3c3..8e627dd0 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -41,13 +41,15 @@ release = '0.x.y-beta-rc2'
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx_js',
+#    'sphinx_js',
     'sphinx.ext.todo',
     "sphinx.ext.autodoc",
-    'autoapi.extension',
+#    'autoapi.extension',
     "recommonmark",            # For markdown files.
     "sphinx_rtd_theme",
-    # 'sphinx.ext.intersphinx',
+    'sphinx.ext.intersphinx',
+    'sphinx.ext.mathjax',
+    'sphinx.ext.ifconfig',
     # 'sphinx.ext.napoleon',     # For Google style docstrings
 ]
 
@@ -207,3 +209,4 @@ autodoc_default_options = {
 
 autoapi_type = 'javascript'
 autoapi_dirs = ['../core/js/']
+autoapi_add_toctree_entry = False
diff --git a/src/doc/extension.rst b/src/doc/extension.rst
new file mode 100644
index 00000000..18fd0f25
--- /dev/null
+++ b/src/doc/extension.rst
@@ -0,0 +1,13 @@
+
+Extending the CaosDB Web Interface
+==================================
+
+Here we collect information on how to extend the web interface as a developer.
+
+.. toctree::
+   :maxdepth: 1
+   :glob:
+
+   extension/*
+
+
diff --git a/src/doc/extension/forms.rst b/src/doc/extension/forms.rst
new file mode 100644
index 00000000..1bced612
--- /dev/null
+++ b/src/doc/extension/forms.rst
@@ -0,0 +1,80 @@
+
+Creating forms for the CaosDB Web Interface
+===========================================
+
+The ``form_elements`` module provides a library for generating forms from simple config objects. The forms are styled for the seamless integration into the CaosDB web interface and are especially useful for calling server side scripts.
+
+See also the :doc:`API documentation <../api/module-form_elements>`
+
+Examples
+--------
+
+Generating a generic form
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The following code snippet adds a form to the body of the HTML document.
+
+.. code-block:: javascript
+
+    function my_special_submit_handler (form) {
+        // handle form submision
+    };
+    const config = {
+        name: "my_form",
+        fields: [
+            { type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true },
+            { type: "integer", name: "number", label: "A Number", required: true },
+            { type: "date", name: "date", label: "A Date", required: false },
+            { type: "text", name: "comment", label: "A Comment", required: false },
+        ],
+        submit: my_special_submit_handler
+    };
+    const form = form_elements.make_form(config);
+    $("body").append(form);
+
+The form has four fields:
+
+    1. A drop-down menu which contains all Records of type "Experiment" as options,
+    2. an integer field, labeled "A Number",
+    3. a date field, labeled "A Date", and
+    4. a text field, labeled "A Comment".
+
+The first two fields are required and the form cannot be submitted without it. The latter are optional.
+
+On submission, the function ``my_special_submit_handler`` is being called with the form element as only parameter.
+
+As the generated form is a plain HTML form, the javascript form API can be used. However, there are special methods in the ``form_elements`` module e.g. :doc:`get_fields <../api/module-form_elements>` which are especially designed to interact with the forms generated by the ``make_form`` factory.
+
+Calling a server-side script
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you intend to call a server-side script, the config has to be changed a litte bit and the script calling is done by the ``form_elements`` module. There is no need to define the submit_hander anymore. Instead, just name the script which is to be called.
+
+.. code-block:: javascript
+
+    const config = {
+        script: "process.py",
+        fields: [
+            { type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true },
+            { type: "integer", name: "number", label: "A Number", required: true },
+            { type: "date", name: "date", label: "A Date", required: false },
+            { type: "text", name: "comment", label: "A Comment", required: false },
+        ],
+    };
+    const form = form_elements.make_form(config);
+    $("body").append(form);
+
+On submission, the form data will be send as a json file to the script and passed as the first parameter. The call would look like ``./process.py form.json`` and the file would contain, for example,
+
+.. code-block:: json
+
+    {
+      "experiment_id": "234234",
+      "number": "400",
+      "date": "2020-12-24",
+      "comment": "This is a comment",
+    }
+
+For more and advanced options for the form see the :doc:`API documentation <../api/module-form_elements>`
+
+
diff --git a/src/doc/index.rst b/src/doc/index.rst
index bc6fd9ed..107c9052 100644
--- a/src/doc/index.rst
+++ b/src/doc/index.rst
@@ -10,7 +10,8 @@ Welcome to the documentation of CaosDB's web UI!
    Getting started <getting_started>
    Tutorials <tutorials/index>
    Concepts <concepts>
-   API Index<genindex>
+   Extending the UI <extension>
+   API <api/index>
 
 
 This documentation helps you to :doc:`get started<getting_started>`, explains the most important
diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile
index 8eff9aae..79f2dfcc 100644
--- a/test/docker/Dockerfile
+++ b/test/docker/Dockerfile
@@ -13,4 +13,5 @@ RUN pip3 install pandas xlrd==1.2.0
 RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev
 # For automatic documentation
 RUN npm install -g jsdoc
+RUN npm install -g jsdoc-sphinx
 RUN pip3 install sphinx-js sphinx-autoapi recommonmark sphinx-rtd-theme
-- 
GitLab