diff --git a/development/issues/closed/277.html b/development/issues/closed/277.html
index 570eaaf080de96bd0dcf8402b916950ee180b6df..ea5bb32eb8f0870868f820cbedebdf8a47441f01 100644
--- a/development/issues/closed/277.html
+++ b/development/issues/closed/277.html
@@ -15,7 +15,7 @@
 </ul>
 <main>
 
-    <monster-host><monster-config-manager></monster-config-manager></monster-host>
+    <!--monster-host><monster-config-manager></monster-config-manager></monster-host -->
     <monster-notify data-monster-option-orientation="bottom right"></monster-notify>
     <monster-monitor-attribute-errors
             data-monster-option-features-notifyUser="false"
@@ -27,13 +27,14 @@
     <monster-datasource-rest id="ds277"
                              data-monster-option-features-autoInit="true"
                              data-monster-option-write-url="/issue-274.json"
-                             data-monster-option-read-url="/issue-274.json?limit=5&page=${page}&q=${query}"
+                             data-monster-option-read-url="/issue-274.json?limit=5&page=${page}&q=${query}&order=${order}"
                              data-monster-option-filter-id="ds277-filter"
                              data-monster-option-features-filter="true"
     ></monster-datasource-rest>
 
 
-    <monster-datatable data-monster-datasource-selector="#ds277" id="dt277">
+    <monster-datatable data-monster-datasource-selector="#ds277" id="dt277"
+                       data-monster-option-datasource-orderdelimiter="::">
 
         <monster-collapse id="filter-collapse" data-monster-role="filter-collapse">
             <div class="flex">
@@ -81,11 +82,11 @@
         </div>
 
         <template id="dt277-row">
-            <div data-monster-grid-template="2rem" data-monster-mode="fixed" data-monster-head="id" data-monster-replace="path:dt277-row.id"></div>
+            <div data-monster-order-template="${field}=${direction}" data-monster-sortable="id" data-monster-grid-template="3rem" data-monster-mode="fixed" data-monster-head="id" data-monster-replace="path:dt277-row.id"></div>
             <div data-monster-grid-template="2rem" data-monster-mode="fixed" data-monster-features="select"></div>
             <div data-monster-head="username" data-monster-replace="path:dt277-row.username"></div>
             <div data-monster-head="email" data-monster-replace="path:dt277-row.email"></div>
-            <div data-monster-head="full_name" data-monster-replace="path:dt277-row.full_name"></div>
+            <div data-monster-sortable="full_name" data-monster-head="full_name" data-monster-replace="path:dt277-row.full_name"></div>
             <div data-monster-head="age" data-monster-replace="path:dt277-row.age"></div>
             <div data-monster-head="country" data-monster-replace="path:dt277-row.country"></div>
             <div data-monster-head="registered_date" data-monster-replace="path:dt277-row.registered_date"></div>
diff --git a/development/templates/vite.config.mjs b/development/templates/vite.config.mjs
index 2ad5a59b0e40bef58ddc068ca37fbc417af06348..67f59bf675f621f50107c701bb39f0e6f9f536c2 100644
--- a/development/templates/vite.config.mjs
+++ b/development/templates/vite.config.mjs
@@ -95,7 +95,10 @@ export default defineConfig({
             cert: "${LOCALHOST_CERTS_DIR}/localhost.alvine.dev.crt"
         },
         watch: {
-            ignored: ['**/node_modules/**']
+            ignored: [
+                '**/node_modules',
+                "**/development/mock",
+            ]
         },
         debug: true,
         proxy: {
diff --git a/nix/config/release.nix b/nix/config/release.nix
index 8154d8885b2e0d29d30e661b72d692090c22d867..08b205ed559c70d38121d400b3c8800376794ddd 100644
--- a/nix/config/release.nix
+++ b/nix/config/release.nix
@@ -3,4 +3,4 @@
   commit = "8a4a2438b30a52ae5ea89dd62fd676a5de945f7e";
   name = "Monster";
   mnemonic = "monster";
-}
\ No newline at end of file
+}
diff --git a/source/components/datatable/dataset.mjs b/source/components/datatable/dataset.mjs
index 79301f9fa2b8b25b8178c6d43a42f3bdecbd69a8..3097021c4412e1484729700114f330cf23921e47 100644
--- a/source/components/datatable/dataset.mjs
+++ b/source/components/datatable/dataset.mjs
@@ -150,8 +150,13 @@ class DataSet extends CustomElement {
 	 */
 	refresh() {
 		// makes sure that handleDataSourceChanges is called
-		this.setOption("data", {});
-		return Promise.resolve(this);
+		return new Promise((resolve) => {
+			this.setOption("data", {});
+			queueMicrotask(() => {
+				handleDataSourceChanges.call(this);
+				resolve();
+			});
+		});
 	}
 
 	/**
diff --git a/source/components/datatable/datasource/rest.mjs b/source/components/datatable/datasource/rest.mjs
index c17b4752e2ba37187ec433b48d6e10d850a449cb..39b90061f13fb0b4c9c06a2562bf6fbacc3ea318 100644
--- a/source/components/datatable/datasource/rest.mjs
+++ b/source/components/datatable/datasource/rest.mjs
@@ -118,7 +118,7 @@ class Rest extends Datasource {
 	 * @property {string} read.method The method of the rest api
 	 * @property {Object} read.parameters The parameters of the rest api
 	 * @property {Object} read.parameters.filter The filter of the rest api
-	 * @property {Object} read.parameters.orderBy The order by of the rest api
+	 * @property {Object} read.parameters.order The order by of the rest api
 	 * @property {Object} read.parameters.page The page of the rest api
 	 * @property {string} read.mapping.currentPage The current page
 	 * @property {Object} write Write configuration
@@ -131,7 +131,7 @@ class Rest extends Datasource {
 
 		restOptions.read.parameters = {
 			filter: null,
-			orderBy: null,
+			order: null,
 			page: "1",
 		};
 
@@ -175,10 +175,10 @@ class Rest extends Datasource {
 	 *
 	 * @param {string} page
 	 * @param {string} query
-	 * @param {string} orderBy
+	 * @param {string} order
 	 * @return {Rest}
 	 */
-	setParameters({ page, query, orderBy }) {
+	setParameters({ page, query, order }) {
 		const parameters = this.getOption("read.parameters");
 		if (query !== undefined) {
 			parameters.query = `${query}`;
@@ -187,7 +187,7 @@ class Rest extends Datasource {
 
 		// after a query the page is set to 1, so if the page is not set, it is set to 1
 		if (page !== undefined) parameters.page = `${page}`;
-		if (orderBy !== undefined) parameters.order = `${orderBy}`;
+		if (order !== undefined) parameters.order = `${order}`;
 		this.setOption("read.parameters", parameters);
 		return this;
 	}
@@ -305,8 +305,8 @@ class Rest extends Datasource {
 			param.page = "1";
 		}
 
-		if (param.orderBy === null || param.orderBy === undefined) {
-			param.orderBy = "";
+		if (param.order === null || param.order === undefined) {
+			param.order = "";
 		}
 
 		const formatter = new Formatter(param);
diff --git a/source/components/datatable/datatable.mjs b/source/components/datatable/datatable.mjs
index 8c5327c73a4edc801aa7741b3119b294576d09de..e86a0e3f41e757e91918bb0616c350598049ad57 100644
--- a/source/components/datatable/datatable.mjs
+++ b/source/components/datatable/datatable.mjs
@@ -12,79 +12,79 @@
  * SPDX-License-Identifier: AGPL-3.0
  */
 
-import {Datasource} from "./datasource.mjs";
+import { Datasource } from "./datasource.mjs";
 import {
-    assembleMethodSymbol,
-    CustomElement,
-    registerCustomElement,
-    getSlottedElements,
+	assembleMethodSymbol,
+	CustomElement,
+	registerCustomElement,
+	getSlottedElements,
 } from "../../dom/customelement.mjs";
 import {
-    findTargetElementFromEvent,
-    fireCustomEvent,
+	findTargetElementFromEvent,
+	fireCustomEvent,
 } from "../../dom/events.mjs";
-import {clone} from "../../util/clone.mjs";
+import { clone } from "../../util/clone.mjs";
 import {
-    isString,
-    isFunction,
-    isInstance,
-    isObject,
-    isArray,
+	isString,
+	isFunction,
+	isInstance,
+	isObject,
+	isArray,
 } from "../../types/is.mjs";
 import {
-    validateArray,
-    validateInteger,
-    validateObject,
+	validateArray,
+	validateInteger,
+	validateObject,
 } from "../../types/validate.mjs";
-import {Observer} from "../../types/observer.mjs";
+import { Observer } from "../../types/observer.mjs";
 import {
-    ATTRIBUTE_DATATABLE_HEAD,
-    ATTRIBUTE_DATATABLE_GRID_TEMPLATE,
-    ATTRIBUTE_DATASOURCE_SELECTOR,
-    ATTRIBUTE_DATATABLE_ALIGN,
-    ATTRIBUTE_DATATABLE_SORTABLE,
-    ATTRIBUTE_DATATABLE_MODE,
-    ATTRIBUTE_DATATABLE_INDEX,
-    ATTRIBUTE_DATATABLE_MODE_HIDDEN,
-    ATTRIBUTE_DATATABLE_FEATURES,
-    ATTRIBUTE_DATATABLE_MODE_VISIBLE,
-    ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
-    ATTRIBUTE_DATATABLE_MODE_FIXED,
+	ATTRIBUTE_DATATABLE_HEAD,
+	ATTRIBUTE_DATATABLE_GRID_TEMPLATE,
+	ATTRIBUTE_DATASOURCE_SELECTOR,
+	ATTRIBUTE_DATATABLE_ALIGN,
+	ATTRIBUTE_DATATABLE_SORTABLE,
+	ATTRIBUTE_DATATABLE_MODE,
+	ATTRIBUTE_DATATABLE_INDEX,
+	ATTRIBUTE_DATATABLE_MODE_HIDDEN,
+	ATTRIBUTE_DATATABLE_FEATURES,
+	ATTRIBUTE_DATATABLE_MODE_VISIBLE,
+	ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
+	ATTRIBUTE_DATATABLE_MODE_FIXED,
 } from "./constants.mjs";
-import {instanceSymbol} from "../../constants.mjs";
+import { instanceSymbol } from "../../constants.mjs";
 import {
-    Header,
-    createOrderStatement,
-    DIRECTION_ASC,
-    DIRECTION_DESC,
-    DIRECTION_NONE,
+	Header,
+	createOrderStatement,
+	DIRECTION_ASC,
+	DIRECTION_DESC,
+	DIRECTION_NONE,
 } from "./datatable/header.mjs";
-import {DatatableStyleSheet} from "./stylesheet/datatable.mjs";
+import { DatatableStyleSheet } from "./stylesheet/datatable.mjs";
 import {
-    handleDataSourceChanges,
-    datasourceLinkedElementSymbol,
+	handleDataSourceChanges,
+	datasourceLinkedElementSymbol,
 } from "./util.mjs";
 import "./columnbar.mjs";
 import "./filter-button.mjs";
 import {
-    findElementWithSelectorUpwards,
-    getDocument,
-    getWindow,
+	findElementWithSelectorUpwards,
+	getDocument,
+	getWindow,
 } from "../../dom/util.mjs";
 
-import {getDocumentTranslations} from "../../i18n/translations.mjs";
+import { getDocumentTranslations } from "../../i18n/translations.mjs";
 import "../state/state.mjs";
 import "../host/collapse.mjs";
-import {generateUniqueConfigKey} from "../host/util.mjs";
+import { generateUniqueConfigKey } from "../host/util.mjs";
 
 import "./datasource/dom.mjs";
 import "./datasource/rest.mjs";
 
 import "../form/context-help.mjs";
-import {getLocaleOfDocument} from "../../dom/locale.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { getLocaleOfDocument } from "../../dom/locale.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 
-export {DataTable};
+export { DataTable };
 
 /**
  * @private
@@ -136,6 +136,7 @@ const resizeObserverSymbol = Symbol("resizeObserver");
  * @example /examples/components/datatable/datasource Use a datasource
  * @example /examples/components/datatable/pagination Use pagination
  * @example /examples/components/datatable/filter Filer the data
+ * @example /examples/components/datatable/order-by Order the data
  * @example /examples/components/datatable/select-rows Select rows
  *
  * @copyright schukai GmbH
@@ -150,421 +151,412 @@ const resizeObserverSymbol = Symbol("resizeObserver");
  * @fires monster-datatable-selection-changed
  **/
 class DataTable extends CustomElement {
-    /**
-     * This method is called by the `instanceof` operator.
-     * @return {symbol}
-     */
-    static get [instanceSymbol]() {
-        return Symbol.for("@schukai/monster/components/datatable@@instance");
-    }
-
-    /**
-     * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
-     * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
-     *
-     * The individual configuration values can be found in the table.
-     *
-     * @property {Object} templates Template definitions
-     * @property {string} templates.main Main template
-     * @property {Object} datasource Datasource configuration
-     * @property {string} datasource.selector Selector for the datasource
-     * @property {Object} mapping Mapping configuration
-     * @property {string} mapping.data Data mapping
-     * @property {Array} data Data
-     * @property {Array} headers Headers
-     * @property {Object} responsive Responsive configuration
-     * @property {number} responsive.breakpoint Breakpoint for responsive mode
-     * @property {Object} labels Labels
-     * @property {string} labels.theListContainsNoEntries Label for empty state
-     * @property {Object} classes Classes
-     * @property {string} classes.container Container class
-     * @property {Object} features Features
-     * @property {boolean} features.settings Settings feature
-     * @property {boolean} features.footer Footer feature
-     * @property {boolean} features.autoInit Auto init feature (init datasource automatically)
-     * @property {boolean} features.doubleClickCopyToClipboard Double click copy to clipboard feature
-     * @property {boolean} features.copyAll Copy all feature
-     * @property {boolean} features.help Help feature
-     * @property {Object} templateMapping Template mapping
-     * @property {string} templateMapping.row-key Row key
-     * @property {string} templateMapping.filter-id Filter id
-     **/
-    get defaults() {
-        return Object.assign(
-            {},
-            super.defaults,
-            {
-                templates: {
-                    main: getTemplate(),
-                    emptyState: getEmptyTemplate(),
-                },
-
-                datasource: {
-                    selector: null,
-                },
-
-                mapping: {
-                    data: "dataset",
-                },
-
-                data: [],
-                headers: [],
-
-                responsive: {
-                    breakpoint: 900,
-                },
-
-                labels: getTranslations(),
-
-                classes: {
-                    control: "monster-theme-control-container-1",
-                    container: "",
-                    row: "monster-theme-control-row-1",
-                },
-
-                features: {
-                    settings: true,
-                    footer: true,
-                    autoInit: true,
-                    doubleClickCopyToClipboard: true,
-                    copyAll: true,
-                    help: true,
-                },
-
-                copy: {
-                    delimiter: ";",
-                    quoteOpen: '"',
-                    quoteClose: '"',
-                    rowBreak: "\n",
-                },
-
-                templateMapping: {
-                    "row-key": null,
-                    "filter-id": null,
-                },
-            },
-            initOptionsFromArguments.call(this),
-        );
-    }
-
-    /**
-     *
-     * @param {string} selector
-     * @return {NodeList}
-     */
-    getGridElements(selector) {
-        return this[gridElementSymbol].querySelectorAll(selector);
-    }
-
-    /**
-     *
-     * @return {string}
-     */
-    static getTag() {
-        return "monster-datatable";
-    }
-
-    /**
-     * @return {void}
-     */
-    disconnectedCallback() {
-        super.disconnectedCallback();
-        if (this?.[resizeObserverSymbol] instanceof ResizeObserver) {
-            this[resizeObserverSymbol].disconnect();
-        }
-    }
-
-    /**
-     * @return {void}
-     */
-    connectedCallback() {
-        const self = this;
-        super.connectedCallback();
-
-        this[resizeObserverSymbol] = new ResizeObserver((entries) => {
-            updateGrid.call(self);
-        });
-
-        this[resizeObserverSymbol].observe(this.parentNode);
-    }
-
-    /**
-     * Get the row number of the selected rows as an array
-     *
-     * @returns {number[]}
-     */
-    getSelectedRows() {
-        const rows = this.getGridElements(`[data-monster-role="select-row"]`);
-        const selectedRows = [];
-        rows.forEach((row) => {
-            if (row.checked) {
-                const key = row.parentNode.getAttribute(
-                    "data-monster-insert-reference",
-                );
-                const index = key.split("-").pop();
-                selectedRows.push(parseInt(index, 10));
-            }
-        });
-
-        return selectedRows;
-    }
-
-    /**
-     * @return void
-     */
-    [assembleMethodSymbol]() {
-        const rawKey = this.getOption("templateMapping.row-key");
-
-        if (rawKey === null) {
-            if (this.id !== null && this.id !== "") {
-                const rawKey = this.getOption("templateMapping.row-key");
-                if (rawKey === null) {
-                    this.setOption("templateMapping.row-key", this.id + "-row");
-                }
-            } else {
-                this.setOption("templateMapping.row-key", "row");
-            }
-        }
-
-        if (this.id !== null && this.id !== "") {
-            this.setOption("templateMapping.filter-id", "" + this.id + "-filter");
-        } else {
-            this.setOption("templateMapping.filter-id", "filter");
-        }
-
-        super[assembleMethodSymbol]();
-
-        initControlReferences.call(this);
-        initEventHandler.call(this);
-
-        getHostConfig
-            .call(this, getColumnVisibilityConfigKey)
-            .then((config) => {
-                const headerOrderMap = new Map();
-
-                getHostConfig
-                    .call(this, getStoredOrderConfigKey)
-                    .then((orderConfig) => {
-                        if (isArray(orderConfig) || orderConfig.length > 0) {
-                            for (let i = 0; i < orderConfig.length; i++) {
-                                const item = orderConfig[i];
-                                const parts = item.split(" ");
-                                const field = parts[0];
-                                const direction = parts[1] || DIRECTION_ASC;
-                                headerOrderMap.set(field, direction);
-                            }
-                        }
-                    })
-                    .then(() => {
-                        try {
-                            initGridAndStructs.call(this, config, headerOrderMap);
-                        } catch (error) {
-                            addErrorAttribute(
-                                this,
-                                error
-                            );
-                        }
-
-                        updateColumnBar.call(this);
-                    })
-                    .catch((error) => {
-                        addErrorAttribute(
-                            this, error,
-                        );
-                    });
-            })
-            .catch((error) => {
-                addErrorAttribute(
-                    this,
-                    error
-                );
-            })
-            .finally(() => {
-
-                const selector = this.getOption("datasource.selector");
-
-                if (isString(selector)) {
-                    const element = findElementWithSelectorUpwards(this, selector);
-                    if (element === null) {
-                        throw new Error("the selector must match exactly one element");
-                    }
-
-                    if (!isInstance(element, Datasource)) {
-                        throw new TypeError("the element must be a datasource");
-                    }
-
-                    this[datasourceLinkedElementSymbol] = element;
-
-                    getWindow().requestAnimationFrame(() => {
-
-                        handleDataSourceChanges.call(this);
-                        if (element && "datasource" in element) {
-                            element.datasource.attachObserver(
-                                new Observer(handleDataSourceChanges.bind(this)),
-                            );
-                        }
-                    });
-                }
-            });
-
-    }
-
-    /**
-     * @return {CSSStyleSheet[]}
-     */
-    static getCSSStyleSheet() {
-        return [DatatableStyleSheet];
-    }
-
-    /**
-     * Copy a row from the datatable
-     *
-     * @param {number|string} fromIndex
-     * @param {number|string} toIndex
-     * @return {DataTable}
-     * @fires monster-datatable-row-copied
-     */
-    copyRow(fromIndex, toIndex) {
-        const datasource = this[datasourceLinkedElementSymbol];
-        if (!datasource) {
-            return this;
-        }
-        let d = datasource.data;
-        let c = clone(d);
-
-        let rows = c;
-        const mapping = this.getOption("mapping.data");
-
-        if (mapping) {
-            rows = c?.[mapping];
-        }
-
-        if (rows === undefined || rows === null) {
-            rows = [];
-        }
-
-        if (toIndex === undefined) {
-            toIndex = rows.length;
-        }
-
-        if (isString(fromIndex)) {
-            fromIndex = parseInt(fromIndex);
-        }
-        if (isString(toIndex)) {
-            toIndex = parseInt(toIndex);
-        }
-
-        if (toIndex < 0 || toIndex > rows.length) {
-            throw new RangeError("index out of bounds");
-        }
-
-        validateArray(rows);
-        validateInteger(fromIndex);
-        validateInteger(toIndex);
-
-        if (fromIndex < 0 || fromIndex >= rows.length) {
-            throw new RangeError("index out of bounds");
-        }
-
-        rows.splice(toIndex, 0, clone(rows[fromIndex]));
-        datasource.data = c;
-
-        fireCustomEvent(this, "monster-datatable-row-copied", {
-            index: toIndex,
-        });
-
-        return this;
-    }
-
-    /**
-     * Remove a row from the datatable
-     *
-     * @param {number|string} index
-     * @return {DataTable}
-     * @fires monster-datatable-row-removed
-     */
-    removeRow(index) {
-        const datasource = this[datasourceLinkedElementSymbol];
-        if (!datasource) {
-            return this;
-        }
-        let d = datasource.data;
-        let c = clone(d);
-
-        let rows = c;
-        const mapping = this.getOption("mapping.data");
-
-        if (mapping) {
-            rows = c?.[mapping];
-        }
-
-        if (rows === undefined || rows === null) {
-            rows = [];
-        }
-
-        if (isString(index)) {
-            index = parseInt(index);
-        }
-
-        validateArray(rows);
-        validateInteger(index);
-
-        if (index < 0 || index >= rows.length) {
-            throw new RangeError("index out of bounds");
-        }
-        if (mapping) {
-            rows = c?.[mapping];
-        }
-
-        rows.splice(index, 1);
-        datasource.data = c;
-
-        fireCustomEvent(this, "monster-datatable-row-removed", {
-            index: index,
-        });
-
-        return this;
-    }
-
-    /**
-     * Add a row to the datatable
-     *
-     * @param {Object} data
-     * @return {DataTable}
-     *
-     * @fires monster-datatable-row-added
-     **/
-    addRow(data) {
-        const datasource = this[datasourceLinkedElementSymbol];
-        if (!datasource) {
-            return this;
-        }
-        let d = datasource.data;
-        let c = clone(d);
-
-        let rows = c;
-
-        const mapping = this.getOption("mapping.data");
-        if (mapping) {
-            rows = c?.[mapping];
-        }
-
-        if (rows === undefined || rows === null) {
-            rows = [];
-        }
-
-        validateArray(rows);
-        validateObject(data);
-
-        rows.push(data);
-        datasource.data = c;
-
-        fireCustomEvent(this, "monster-datatable-row-added", {
-            index: rows.length - 1,
-        });
-
-        return this;
-    }
+	/**
+	 * This method is called by the `instanceof` operator.
+	 * @return {symbol}
+	 */
+	static get [instanceSymbol]() {
+		return Symbol.for("@schukai/monster/components/datatable@@instance");
+	}
+
+	/**
+	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
+	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
+	 *
+	 * The individual configuration values can be found in the table.
+	 *
+	 * @property {Object} templates Template definitions
+	 * @property {string} templates.main Main template
+	 * @property {Object} datasource Datasource configuration
+	 * @property {string} datasource.selector Selector for the datasource
+	 * @property {string} datasource.orderDelimiter Order delimiter
+	 * @property {Object} mapping Mapping configuration
+	 * @property {string} mapping.data Data mapping
+	 * @property {Array} data Data
+	 * @property {Array} headers Headers
+	 * @property {Object} responsive Responsive configuration
+	 * @property {number} responsive.breakpoint Breakpoint for responsive mode
+	 * @property {Object} labels Labels
+	 * @property {string} labels.theListContainsNoEntries Label for empty state
+	 * @property {Object} classes Classes
+	 * @property {string} classes.container Container class
+	 * @property {Object} features Features
+	 * @property {boolean} features.settings Settings feature
+	 * @property {boolean} features.footer Footer feature
+	 * @property {boolean} features.autoInit Auto init feature (init datasource automatically)
+	 * @property {boolean} features.doubleClickCopyToClipboard Double click copy to clipboard feature
+	 * @property {boolean} features.copyAll Copy all feature
+	 * @property {boolean} features.help Help feature
+	 * @property {Object} templateMapping Template mapping
+	 * @property {string} templateMapping.row-key Row key
+	 * @property {string} templateMapping.filter-id Filter id
+	 **/
+	get defaults() {
+		return Object.assign(
+			{},
+			super.defaults,
+			{
+				templates: {
+					main: getTemplate(),
+					emptyState: getEmptyTemplate(),
+				},
+
+				datasource: {
+					selector: null,
+					orderDelimiter: ",", // look at  initOptionsFromArguments()
+				},
+
+				mapping: {
+					data: "dataset",
+				},
+
+				data: [],
+				headers: [],
+
+				responsive: {
+					breakpoint: 900,
+				},
+
+				labels: getTranslations(),
+
+				classes: {
+					control: "monster-theme-control-container-1",
+					container: "",
+					row: "monster-theme-control-row-1",
+				},
+
+				features: {
+					settings: true,
+					footer: true,
+					autoInit: true,
+					doubleClickCopyToClipboard: true,
+					copyAll: true,
+					help: true,
+				},
+
+				copy: {
+					delimiter: ";",
+					quoteOpen: '"',
+					quoteClose: '"',
+					rowBreak: "\n",
+				},
+
+				templateMapping: {
+					"row-key": null,
+					"filter-id": null,
+				},
+			},
+			initOptionsFromArguments.call(this),
+		);
+	}
+
+	/**
+	 *
+	 * @param {string} selector
+	 * @return {NodeList}
+	 */
+	getGridElements(selector) {
+		return this[gridElementSymbol].querySelectorAll(selector);
+	}
+
+	/**
+	 *
+	 * @return {string}
+	 */
+	static getTag() {
+		return "monster-datatable";
+	}
+
+	/**
+	 * @return {void}
+	 */
+	disconnectedCallback() {
+		super.disconnectedCallback();
+		if (this?.[resizeObserverSymbol] instanceof ResizeObserver) {
+			this[resizeObserverSymbol].disconnect();
+		}
+	}
+
+	/**
+	 * @return {void}
+	 */
+	connectedCallback() {
+		const self = this;
+		super.connectedCallback();
+
+		this[resizeObserverSymbol] = new ResizeObserver((entries) => {
+			updateGrid.call(self);
+		});
+
+		this[resizeObserverSymbol].observe(this.parentNode);
+	}
+
+	/**
+	 * Get the row number of the selected rows as an array
+	 *
+	 * @returns {number[]}
+	 */
+	getSelectedRows() {
+		const rows = this.getGridElements(`[data-monster-role="select-row"]`);
+		const selectedRows = [];
+		rows.forEach((row) => {
+			if (row.checked) {
+				const key = row.parentNode.getAttribute(
+					"data-monster-insert-reference",
+				);
+				const index = key.split("-").pop();
+				selectedRows.push(parseInt(index, 10));
+			}
+		});
+
+		return selectedRows;
+	}
+
+	/**
+	 * @return void
+	 */
+	[assembleMethodSymbol]() {
+		const rawKey = this.getOption("templateMapping.row-key");
+
+		if (rawKey === null) {
+			if (this.id !== null && this.id !== "") {
+				const rawKey = this.getOption("templateMapping.row-key");
+				if (rawKey === null) {
+					this.setOption("templateMapping.row-key", this.id + "-row");
+				}
+			} else {
+				this.setOption("templateMapping.row-key", "row");
+			}
+		}
+
+		if (this.id !== null && this.id !== "") {
+			this.setOption("templateMapping.filter-id", "" + this.id + "-filter");
+		} else {
+			this.setOption("templateMapping.filter-id", "filter");
+		}
+
+		super[assembleMethodSymbol]();
+
+		initControlReferences.call(this);
+		initEventHandler.call(this);
+
+		getHostConfig
+			.call(this, getColumnVisibilityConfigKey)
+			.then((config) => {
+				const headerOrderMap = new Map();
+
+				getHostConfig
+					.call(this, getStoredOrderConfigKey)
+					.then((orderConfig) => {
+						if (isArray(orderConfig) || orderConfig.length > 0) {
+							for (let i = 0; i < orderConfig.length; i++) {
+								const item = orderConfig[i];
+								const parts = item.split(" ");
+								const field = parts[0];
+								const direction = parts[1] || DIRECTION_ASC;
+								headerOrderMap.set(field, direction);
+							}
+						}
+					})
+					.then(() => {
+						try {
+							initGridAndStructs.call(this, config, headerOrderMap);
+						} catch (error) {
+							addErrorAttribute(this, error);
+						}
+
+						updateColumnBar.call(this);
+					})
+					.catch((error) => {
+						addErrorAttribute(this, error);
+					});
+			})
+			.catch((error) => {
+				addErrorAttribute(this, error);
+			})
+			.finally(() => {
+				const selector = this.getOption("datasource.selector");
+
+				if (isString(selector)) {
+					const element = findElementWithSelectorUpwards(this, selector);
+					if (element === null) {
+						throw new Error("the selector must match exactly one element");
+					}
+
+					if (!isInstance(element, Datasource)) {
+						throw new TypeError("the element must be a datasource");
+					}
+
+					this[datasourceLinkedElementSymbol] = element;
+
+					getWindow().requestAnimationFrame(() => {
+						handleDataSourceChanges.call(this);
+						if (element && "datasource" in element) {
+							element.datasource.attachObserver(
+								new Observer(handleDataSourceChanges.bind(this)),
+							);
+						}
+					});
+				}
+			});
+	}
+
+	/**
+	 * @return {CSSStyleSheet[]}
+	 */
+	static getCSSStyleSheet() {
+		return [DatatableStyleSheet];
+	}
+
+	/**
+	 * Copy a row from the datatable
+	 *
+	 * @param {number|string} fromIndex
+	 * @param {number|string} toIndex
+	 * @return {DataTable}
+	 * @fires monster-datatable-row-copied
+	 */
+	copyRow(fromIndex, toIndex) {
+		const datasource = this[datasourceLinkedElementSymbol];
+		if (!datasource) {
+			return this;
+		}
+		let d = datasource.data;
+		let c = clone(d);
+
+		let rows = c;
+		const mapping = this.getOption("mapping.data");
+
+		if (mapping) {
+			rows = c?.[mapping];
+		}
+
+		if (rows === undefined || rows === null) {
+			rows = [];
+		}
+
+		if (toIndex === undefined) {
+			toIndex = rows.length;
+		}
+
+		if (isString(fromIndex)) {
+			fromIndex = parseInt(fromIndex);
+		}
+		if (isString(toIndex)) {
+			toIndex = parseInt(toIndex);
+		}
+
+		if (toIndex < 0 || toIndex > rows.length) {
+			throw new RangeError("index out of bounds");
+		}
+
+		validateArray(rows);
+		validateInteger(fromIndex);
+		validateInteger(toIndex);
+
+		if (fromIndex < 0 || fromIndex >= rows.length) {
+			throw new RangeError("index out of bounds");
+		}
+
+		rows.splice(toIndex, 0, clone(rows[fromIndex]));
+		datasource.data = c;
+
+		fireCustomEvent(this, "monster-datatable-row-copied", {
+			index: toIndex,
+		});
+
+		return this;
+	}
+
+	/**
+	 * Remove a row from the datatable
+	 *
+	 * @param {number|string} index
+	 * @return {DataTable}
+	 * @fires monster-datatable-row-removed
+	 */
+	removeRow(index) {
+		const datasource = this[datasourceLinkedElementSymbol];
+		if (!datasource) {
+			return this;
+		}
+		let d = datasource.data;
+		let c = clone(d);
+
+		let rows = c;
+		const mapping = this.getOption("mapping.data");
+
+		if (mapping) {
+			rows = c?.[mapping];
+		}
+
+		if (rows === undefined || rows === null) {
+			rows = [];
+		}
+
+		if (isString(index)) {
+			index = parseInt(index);
+		}
+
+		validateArray(rows);
+		validateInteger(index);
+
+		if (index < 0 || index >= rows.length) {
+			throw new RangeError("index out of bounds");
+		}
+		if (mapping) {
+			rows = c?.[mapping];
+		}
+
+		rows.splice(index, 1);
+		datasource.data = c;
+
+		fireCustomEvent(this, "monster-datatable-row-removed", {
+			index: index,
+		});
+
+		return this;
+	}
+
+	/**
+	 * Add a row to the datatable
+	 *
+	 * @param {Object} data
+	 * @return {DataTable}
+	 *
+	 * @fires monster-datatable-row-added
+	 **/
+	addRow(data) {
+		const datasource = this[datasourceLinkedElementSymbol];
+		if (!datasource) {
+			return this;
+		}
+		let d = datasource.data;
+		let c = clone(d);
+
+		let rows = c;
+
+		const mapping = this.getOption("mapping.data");
+		if (mapping) {
+			rows = c?.[mapping];
+		}
+
+		if (rows === undefined || rows === null) {
+			rows = [];
+		}
+
+		validateArray(rows);
+		validateObject(data);
+
+		rows.push(data);
+		datasource.data = c;
+
+		fireCustomEvent(this, "monster-datatable-row-added", {
+			index: rows.length - 1,
+		});
+
+		return this;
+	}
 }
 
 /**
@@ -572,7 +564,7 @@ class DataTable extends CustomElement {
  * @return {string}
  */
 function getColumnVisibilityConfigKey() {
-    return generateUniqueConfigKey("datatable", this?.id, "columns-visibility");
+	return generateUniqueConfigKey("datatable", this?.id, "columns-visibility");
 }
 
 /**
@@ -580,7 +572,7 @@ function getColumnVisibilityConfigKey() {
  * @return {string}
  */
 function getFilterConfigKey() {
-    return generateUniqueConfigKey("datatable", this?.id, "filter");
+	return generateUniqueConfigKey("datatable", this?.id, "filter");
 }
 
 /**
@@ -588,537 +580,530 @@ function getFilterConfigKey() {
  * @return {Promise}
  */
 function getHostConfig(callback) {
-    const host = findElementWithSelectorUpwards(this, "monster-host");
-
-    if (!host) {
-        addErrorAttribute(this, "no host found");
-        return Promise.resolve({});
-    }
-
-    if (!this.id) {
-        addErrorAttribute(
-            this,
-            "no id found; id is required for config",
-        );
-        return Promise.resolve({});
-    }
-
-    if (!host || !isFunction(host?.getConfig)) {
-        throw new TypeError("the host must be a monster-host");
-    }
-
-    const configKey = callback.call(this);
-    return host.hasConfig(configKey).then((hasConfig) => {
-        if (hasConfig) {
-            return host.getConfig(configKey);
-        } else {
-            return {};
-        }
-    });
+	const host = findElementWithSelectorUpwards(this, "monster-host");
+
+	if (!host) {
+		addErrorAttribute(this, "no host found");
+		return Promise.resolve({});
+	}
+
+	if (!this.id) {
+		addErrorAttribute(this, "no id found; id is required for config");
+		return Promise.resolve({});
+	}
+
+	if (!host || !isFunction(host?.getConfig)) {
+		throw new TypeError("the host must be a monster-host");
+	}
+
+	const configKey = callback.call(this);
+	return host.hasConfig(configKey).then((hasConfig) => {
+		if (hasConfig) {
+			return host.getConfig(configKey);
+		} else {
+			return {};
+		}
+	});
 }
 
 /**
  * @private
  */
 function updateColumnBar() {
-    if (!this[columnBarElementSymbol]) {
-        return;
-    }
-
-    const columns = [];
-    for (const header of this.getOption("headers")) {
-        const mode = header.getInternal("mode");
-
-        if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
-            continue;
-        }
-
-        columns.push({
-            visible: mode !== ATTRIBUTE_DATATABLE_MODE_HIDDEN,
-            name: header.label,
-            index: header.index,
-        });
-    }
-
-    this[columnBarElementSymbol].setOption("columns", columns);
+	if (!this[columnBarElementSymbol]) {
+		return;
+	}
+
+	const columns = [];
+	for (const header of this.getOption("headers")) {
+		const mode = header.getInternal("mode");
+
+		if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
+			continue;
+		}
+
+		columns.push({
+			visible: mode !== ATTRIBUTE_DATATABLE_MODE_HIDDEN,
+			name: header.label,
+			index: header.index,
+		});
+	}
+
+	this[columnBarElementSymbol].setOption("columns", columns);
 }
 
 /**
  * @private
  */
 function updateHeaderFromColumnBar() {
-    if (!this[columnBarElementSymbol]) {
-        return;
-    }
+	if (!this[columnBarElementSymbol]) {
+		return;
+	}
 
-    const options = this[columnBarElementSymbol].getOption("columns");
-    if (!isArray(options)) return;
+	const options = this[columnBarElementSymbol].getOption("columns");
+	if (!isArray(options)) return;
 
-    const invisibleMap = {};
+	const invisibleMap = {};
 
-    for (let i = 0; i < options.length; i++) {
-        const option = options[i];
-        invisibleMap[option.index] = option.visible;
-    }
+	for (let i = 0; i < options.length; i++) {
+		const option = options[i];
+		invisibleMap[option.index] = option.visible;
+	}
 
-    for (const header of this.getOption("headers")) {
-        const mode = header.getInternal("mode");
+	for (const header of this.getOption("headers")) {
+		const mode = header.getInternal("mode");
 
-        if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
-            continue;
-        }
+		if (mode === ATTRIBUTE_DATATABLE_MODE_FIXED) {
+			continue;
+		}
 
-        if (invisibleMap[header.index] === false) {
-            header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_HIDDEN);
-        } else {
-            header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_VISIBLE);
-        }
-    }
+		if (invisibleMap[header.index] === false) {
+			header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_HIDDEN);
+		} else {
+			header.setInternal("mode", ATTRIBUTE_DATATABLE_MODE_VISIBLE);
+		}
+	}
 }
 
 /**
  * @private
  */
 function updateConfigColumnBar() {
-    if (!this[columnBarElementSymbol]) {
-        return;
-    }
-
-    const options = this[columnBarElementSymbol].getOption("columns");
-    if (!isArray(options)) return;
-
-    const map = {};
-    for (let i = 0; i < options.length; i++) {
-        const option = options[i];
-        map[option.name] = option.visible;
-    }
-
-    const host = findElementWithSelectorUpwards(this, "monster-host");
-    if (!(host && this.id)) {
-        return;
-    }
-    const configKey = getColumnVisibilityConfigKey.call(this);
-
-    try {
-        host.setConfig(configKey, map);
-    } catch (error) {
-        addErrorAttribute(this, error);
-    }
+	if (!this[columnBarElementSymbol]) {
+		return;
+	}
+
+	const options = this[columnBarElementSymbol].getOption("columns");
+	if (!isArray(options)) return;
+
+	const map = {};
+	for (let i = 0; i < options.length; i++) {
+		const option = options[i];
+		map[option.name] = option.visible;
+	}
+
+	const host = findElementWithSelectorUpwards(this, "monster-host");
+	if (!(host && this.id)) {
+		return;
+	}
+	const configKey = getColumnVisibilityConfigKey.call(this);
+
+	try {
+		host.setConfig(configKey, map);
+	} catch (error) {
+		addErrorAttribute(this, error);
+	}
 }
 
 /**
  * @private
  */
 function initEventHandler() {
-    const self = this;
-
-    const quoteOpenChar = this.getOption("copy.quoteOpen");
-    const quoteCloseChar = this.getOption("copy.quoteClose");
-    const delimiterChar = this.getOption("copy.delimiter");
-    const rowBreak = this.getOption("copy.rowBreak");
-
-    self[columnBarElementSymbol].attachObserver(
-        new Observer((e) => {
-            updateHeaderFromColumnBar.call(self);
-            updateGrid.call(self);
-            updateConfigColumnBar.call(self);
-        }),
-    );
-
-    self[gridHeadersElementSymbol].addEventListener("click", function (event) {
-        let element = null;
-        const datasource = self[datasourceLinkedElementSymbol];
-        if (!datasource) {
-            return;
-        }
-
-        element = findTargetElementFromEvent(event, ATTRIBUTE_DATATABLE_SORTABLE);
-        if (element) {
-            const index = element.parentNode.getAttribute(ATTRIBUTE_DATATABLE_INDEX);
-            const headers = self.getOption("headers");
-
-            event.preventDefault();
-
-            headers[index].changeDirection();
-
-            queueMicrotask(function () {
-                /** hotfix, normally this should be done via the updater, no idea why this is not possible. */
-                element.setAttribute(
-                    ATTRIBUTE_DATATABLE_SORTABLE,
-                    `${headers[index].field} ${headers[index].direction}`,
-                );
-
-                storeOrderStatement.call(self, true);
-            });
-        }
-    });
-
-    const eventHandlerDoubleClickCopyToClipboard = (event) => {
-        const element = findTargetElementFromEvent(event, "data-monster-head");
-        if (element) {
-            let text = "";
-
-            if (event.shiftKey) {
-                const index = element.getAttribute("data-monster-insert-reference");
-                if (index) {
-                    const cols = self.getGridElements(
-                        `[data-monster-insert-reference="${index}"]`,
-                    );
-
-                    const colTexts = [];
-                    for (let i = 0; i < cols.length; i++) {
-                        const col = cols[i];
-
-                        if (
-                            col.querySelector("monster-button-bar") ||
-                            col.querySelector("monster-button")
-                        ) {
-                            continue;
-                        }
-
-                        if (col.textContent) {
-                            colTexts.push(
-                                quoteOpenChar + col.textContent.trim() + quoteCloseChar,
-                            );
-                        }
-                    }
-
-                    text = colTexts.join(delimiterChar);
-                }
-            } else {
-                if (
-                    element.querySelector("monster-button-bar") ||
-                    element.querySelector("monster-button")
-                ) {
-                    return;
-                }
-
-                text = element.textContent.trim();
-            }
-
-            if (getWindow().navigator.clipboard && text) {
-                getWindow()
-                    .navigator.clipboard.writeText(text)
-                    .then(
-                        () => {
-                        },
-                        (err) => {
-                        },
-                    );
-            }
-        }
-    };
-
-    if (self.getOption("features.doubleClickCopyToClipboard")) {
-        self[gridElementSymbol].addEventListener(
-            "dblclick",
-            eventHandlerDoubleClickCopyToClipboard,
-        );
-    }
-
-    if (self.getOption("features.copyAll") && this[copyAllElementSymbol]) {
-        this[copyAllElementSymbol].addEventListener("click", (event) => {
-            event.preventDefault();
-
-            const table = [];
-            let currentRow = [];
-            let currentIndex = null;
-
-            const cols = self.getGridElements(`[data-monster-insert-reference]`);
-            const rowIndexes = new Map();
-            cols.forEach((col) => {
-                const index = col.getAttribute("data-monster-insert-reference");
-                rowIndexes.set(index, true);
-            });
-
-            rowIndexes.forEach((value, key) => {
-                const cols = self.getGridElements(
-                    `[data-monster-insert-reference="${key}"]`,
-                );
-
-                for (let i = 0; i < cols.length; i++) {
-                    const col = cols[i];
-
-                    if (
-                        col.querySelector("monster-button-bar") ||
-                        col.querySelector("monster-button")
-                    ) {
-                        continue;
-                    }
-
-                    if (col.textContent) {
-                        currentRow.push(
-                            quoteOpenChar + col.textContent.trim() + quoteCloseChar,
-                        );
-                    }
-                }
-
-                if (currentRow.length > 0) {
-                    table.push(currentRow);
-                }
-                currentRow = [];
-            });
-
-            if (table.length > 0) {
-                const text = table.map((row) => row.join(delimiterChar)).join(rowBreak);
-                if (getWindow().navigator.clipboard && text) {
-                    getWindow()
-                        .navigator.clipboard.writeText(text)
-                        .then(
-                            () => {
-                            },
-                            (err) => {
-                            },
-                        );
-                }
-            }
-        });
-    }
-
-    const selectRowCallback = (event) => {
-        const element = findTargetElementFromEvent(
-            event,
-            "data-monster-role",
-            "select-row",
-        );
-
-        if (!element) {
-            return;
-        }
-        
-        const key = element.parentNode.getAttribute(
-            "data-monster-insert-reference",
-        );
-        const row = self.getGridElements(
-            `[data-monster-insert-reference="${key}"]`,
-        );
-
-        const index = key.split("-").pop();
-
-        if (element.checked) {
-            row.forEach((col) => {
-                col.classList.add("selected");
-            });
-
-            fireCustomEvent(self, "monster-datatable-row-selected", {
-                index: index,
-            });
-        } else {
-            row.forEach((col) => {
-                col.classList.remove("selected");
-            });
-
-            fireCustomEvent(self, "monster-datatable-row-deselected", {
-                index: index,
-            });
-        }
-
-        fireCustomEvent(this, "monster-datatable-selection-changed", {});
-
-        const rows = self.getGridElements(`[data-monster-role="select-row"]`);
-        const allSelected = Array.from(rows).every((row) => row.checked);
-
-        const selectAll = this[gridHeadersElementSymbol].querySelector(
-            `[data-monster-role="select-all"]`,
-        );
-
-        if (selectAll) {
-            selectAll.checked = allSelected;
-        }
-
-    };
-
-    this[gridElementSymbol].addEventListener("click", selectRowCallback);
-    this[gridElementSymbol].addEventListener("touch", selectRowCallback);
-
-    const selectAllCallback = (event) => {
-        const element = findTargetElementFromEvent(
-            event,
-            "data-monster-role",
-            "select-all",
-        );
-        if (element) {
-            const mode = element.checked;
-
-            const rows = this.getGridElements(`[data-monster-role="select-row"]`);
-            rows.forEach((row) => {
-                row.checked = mode;
-            });
-
-            if (mode) {
-                fireCustomEvent(this, "monster-datatable-all-rows-selected", {});
-            } else {
-                fireCustomEvent(this, "monster-datatable-all-rows-deselected", {});
-            }
-
-            fireCustomEvent(this, "monster-datatable-selection-changed", {});
-        }
-    };
-
-    this[gridHeadersElementSymbol].addEventListener("click", selectAllCallback);
-    this[gridHeadersElementSymbol].addEventListener("touch", selectAllCallback);
+	const self = this;
+
+	const quoteOpenChar = this.getOption("copy.quoteOpen");
+	const quoteCloseChar = this.getOption("copy.quoteClose");
+	const delimiterChar = this.getOption("copy.delimiter");
+	const rowBreak = this.getOption("copy.rowBreak");
+
+	self[columnBarElementSymbol].attachObserver(
+		new Observer((e) => {
+			updateHeaderFromColumnBar.call(self);
+			updateGrid.call(self);
+			updateConfigColumnBar.call(self);
+		}),
+	);
+
+	self[gridHeadersElementSymbol].addEventListener("click", function (event) {
+		let element = null;
+		const datasource = self[datasourceLinkedElementSymbol];
+		if (!datasource) {
+			return;
+		}
+
+		element = findTargetElementFromEvent(event, ATTRIBUTE_DATATABLE_SORTABLE);
+		if (element instanceof HTMLElement) {
+			const index = element.parentNode?.getAttribute(ATTRIBUTE_DATATABLE_INDEX);
+			const headers = self.getOption("headers");
+
+			event.preventDefault();
+
+			headers[index].changeDirection();
+
+			queueMicrotask(function () {
+				/** hotfix, normally this should be done via the updater, no idea why this is not possible. */
+				element.setAttribute(
+					ATTRIBUTE_DATATABLE_SORTABLE,
+					`${headers[index].field} ${headers[index].direction}`,
+				);
+
+				storeOrderStatement.call(self, true);
+			});
+		}
+	});
+
+	const eventHandlerDoubleClickCopyToClipboard = (event) => {
+		const element = findTargetElementFromEvent(event, "data-monster-head");
+		if (element) {
+			let text = "";
+
+			if (event.shiftKey) {
+				const index = element.getAttribute("data-monster-insert-reference");
+				if (index) {
+					const cols = self.getGridElements(
+						`[data-monster-insert-reference="${index}"]`,
+					);
+
+					const colTexts = [];
+					for (let i = 0; i < cols.length; i++) {
+						const col = cols[i];
+
+						if (
+							col.querySelector("monster-button-bar") ||
+							col.querySelector("monster-button")
+						) {
+							continue;
+						}
+
+						if (col.textContent) {
+							colTexts.push(
+								quoteOpenChar + col.textContent.trim() + quoteCloseChar,
+							);
+						}
+					}
+
+					text = colTexts.join(delimiterChar);
+				}
+			} else {
+				if (
+					element.querySelector("monster-button-bar") ||
+					element.querySelector("monster-button")
+				) {
+					return;
+				}
+
+				text = element.textContent.trim();
+			}
+
+			if (getWindow().navigator.clipboard && text) {
+				getWindow()
+					.navigator.clipboard.writeText(text)
+					.then(
+						() => {},
+						(err) => {},
+					);
+			}
+		}
+	};
+
+	if (self.getOption("features.doubleClickCopyToClipboard")) {
+		self[gridElementSymbol].addEventListener(
+			"dblclick",
+			eventHandlerDoubleClickCopyToClipboard,
+		);
+	}
+
+	if (self.getOption("features.copyAll") && this[copyAllElementSymbol]) {
+		this[copyAllElementSymbol].addEventListener("click", (event) => {
+			event.preventDefault();
+
+			const table = [];
+			let currentRow = [];
+			let currentIndex = null;
+
+			const cols = self.getGridElements(`[data-monster-insert-reference]`);
+			const rowIndexes = new Map();
+			cols.forEach((col) => {
+				const index = col.getAttribute("data-monster-insert-reference");
+				rowIndexes.set(index, true);
+			});
+
+			rowIndexes.forEach((value, key) => {
+				const cols = self.getGridElements(
+					`[data-monster-insert-reference="${key}"]`,
+				);
+
+				for (let i = 0; i < cols.length; i++) {
+					const col = cols[i];
+
+					if (
+						col.querySelector("monster-button-bar") ||
+						col.querySelector("monster-button")
+					) {
+						continue;
+					}
+
+					if (col.textContent) {
+						currentRow.push(
+							quoteOpenChar + col.textContent.trim() + quoteCloseChar,
+						);
+					}
+				}
+
+				if (currentRow.length > 0) {
+					table.push(currentRow);
+				}
+				currentRow = [];
+			});
+
+			if (table.length > 0) {
+				const text = table.map((row) => row.join(delimiterChar)).join(rowBreak);
+				if (getWindow().navigator.clipboard && text) {
+					getWindow()
+						.navigator.clipboard.writeText(text)
+						.then(
+							() => {},
+							(err) => {},
+						);
+				}
+			}
+		});
+	}
+
+	const selectRowCallback = (event) => {
+		const element = findTargetElementFromEvent(
+			event,
+			"data-monster-role",
+			"select-row",
+		);
+
+		if (!element) {
+			return;
+		}
+
+		const key = element.parentNode.getAttribute(
+			"data-monster-insert-reference",
+		);
+		const row = self.getGridElements(
+			`[data-monster-insert-reference="${key}"]`,
+		);
+
+		const index = key.split("-").pop();
+
+		if (element.checked) {
+			row.forEach((col) => {
+				col.classList.add("selected");
+			});
+
+			fireCustomEvent(self, "monster-datatable-row-selected", {
+				index: index,
+			});
+		} else {
+			row.forEach((col) => {
+				col.classList.remove("selected");
+			});
+
+			fireCustomEvent(self, "monster-datatable-row-deselected", {
+				index: index,
+			});
+		}
+
+		fireCustomEvent(this, "monster-datatable-selection-changed", {});
+
+		const rows = self.getGridElements(`[data-monster-role="select-row"]`);
+		const allSelected = Array.from(rows).every((row) => row.checked);
+
+		const selectAll = this[gridHeadersElementSymbol].querySelector(
+			`[data-monster-role="select-all"]`,
+		);
+
+		if (selectAll) {
+			selectAll.checked = allSelected;
+		}
+	};
+
+	this[gridElementSymbol].addEventListener("click", selectRowCallback);
+	this[gridElementSymbol].addEventListener("touch", selectRowCallback);
+
+	const selectAllCallback = (event) => {
+		const element = findTargetElementFromEvent(
+			event,
+			"data-monster-role",
+			"select-all",
+		);
+		if (element) {
+			const mode = element.checked;
+
+			const rows = this.getGridElements(`[data-monster-role="select-row"]`);
+			rows.forEach((row) => {
+				row.checked = mode;
+			});
+
+			if (mode) {
+				fireCustomEvent(this, "monster-datatable-all-rows-selected", {});
+			} else {
+				fireCustomEvent(this, "monster-datatable-all-rows-deselected", {});
+			}
+
+			fireCustomEvent(this, "monster-datatable-selection-changed", {});
+		}
+	};
+
+	this[gridHeadersElementSymbol].addEventListener("click", selectAllCallback);
+	this[gridHeadersElementSymbol].addEventListener("touch", selectAllCallback);
 }
 
 /**
  * @private
  */
 function initGridAndStructs(hostConfig, headerOrderMap) {
-    const rowID = this.getOption("templateMapping.row-key");
-
-    if (!this[gridElementSymbol]) {
-        addErrorAttribute(this, "no grid element found");
-        return;
-    }
-
-    let template;
-    getSlottedElements.call(this).forEach((e) => {
-        if (e instanceof HTMLTemplateElement && e.id === rowID) {
-            template = e;
-        }
-    });
-
-    if (!template) {
-        addErrorAttribute(
-            this,
-            "no template found, please add a template",
-        );
-        return;
-    }
-
-    const rowCount = template.content.children.length;
-
-    const headers = [];
-
-    for (let i = 0; i < rowCount; i++) {
-        let hClass = "";
-        const row = template.content.children[i];
-
-        let mode = "";
-        if (row.hasAttribute(ATTRIBUTE_DATATABLE_MODE)) {
-            mode = row.getAttribute(ATTRIBUTE_DATATABLE_MODE);
-        }
-
-        let grid = row.getAttribute(ATTRIBUTE_DATATABLE_GRID_TEMPLATE);
-        if (!grid || grid === "" || grid === "auto") {
-            grid = "minmax(0, 1fr)";
-        }
-
-        let label = "";
-        let labelKey = "";
-
-        if (row.hasAttribute(ATTRIBUTE_DATATABLE_HEAD)) {
-            label = row.getAttribute(ATTRIBUTE_DATATABLE_HEAD);
-            labelKey = label;
-
-            try {
-                if (label.startsWith("i18n:")) {
-                    label = label.substring(5, label.length);
-                    label = getDocumentTranslations().getText(label, label);
-                }
-            } catch (e) {
-                label = "i18n error " + label;
-            }
-        }
-
-        if (!label) {
-            label = i + 1 + "";
-            mode = ATTRIBUTE_DATATABLE_MODE_FIXED;
-            labelKey = label;
-        }
-
-        if (isObject(hostConfig) && hostConfig.hasOwnProperty(label)) {
-            if (hostConfig[label] === false) {
-                mode = ATTRIBUTE_DATATABLE_MODE_HIDDEN;
-            } else {
-                mode = ATTRIBUTE_DATATABLE_MODE_VISIBLE;
-            }
-        }
-
-        let align = "";
-        if (row.hasAttribute(ATTRIBUTE_DATATABLE_ALIGN)) {
-            align = row.getAttribute(ATTRIBUTE_DATATABLE_ALIGN);
-        }
-
-        switch (align) {
-            case "center":
-                hClass = "flex-center";
-                break;
-            case "end":
-                hClass = "flex-end";
-                break;
-            case "start":
-                hClass = "flex-start";
-                break;
-            default:
-                hClass = "flex-start";
-        }
-
-        let field = "";
-        let direction = DIRECTION_NONE;
-        if (row.hasAttribute(ATTRIBUTE_DATATABLE_SORTABLE)) {
-            field = row.getAttribute(ATTRIBUTE_DATATABLE_SORTABLE).trim();
-            const parts = field.split(" ").map((item) => item.trim());
-            field = parts[0];
-
-            if (headerOrderMap.has(field)) {
-                direction = headerOrderMap.get(field);
-            } else if (
-                parts.length === 2 &&
-                [DIRECTION_ASC, DIRECTION_DESC].indexOf(parts[1]) !== -1
-            ) {
-                direction = parts[1];
-            }
-        }
-
-        if (mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
-            hClass += " hidden";
-        }
-
-        const features = [];
-
-        if (row.hasAttribute(ATTRIBUTE_DATATABLE_FEATURES)) {
-            const fl = row
-                .getAttribute(ATTRIBUTE_DATATABLE_FEATURES)
-                .split(" ");
-
-            fl.forEach((feature) => {
-                features.push(feature.trim());
-
-                if (feature === "select") {
-                    label = "<input type='checkbox' data-monster-role='select-all' />";
-
-                    while (row.firstChild) {
-                        row.removeChild(row.firstChild);
-                    }
-
-                    const checkbox = document.createElement("input");
-                    checkbox.type = "checkbox";
-                    checkbox.setAttribute("data-monster-role", "select-row");
-                    row.appendChild(checkbox);
-                }
-            });
-        }
-
-        const header = new Header();
-        header.setInternals({
-            field: field,
-            label: label,
-            classes: hClass,
-            index: i,
-            mode: mode,
-            grid: grid,
-            labelKey: labelKey,
-            direction: direction,
-            features: features,
-        });
-
-        headers.push(header);
-    }
-
-    this.setOption("headers", headers);
-    queueMicrotask(() => {
-        storeOrderStatement.call(this, this.getOption("features.autoInit"));
-    });
+	const rowID = this.getOption("templateMapping.row-key");
+
+	if (!this[gridElementSymbol]) {
+		addErrorAttribute(this, "no grid element found");
+		return;
+	}
+
+	let template;
+	getSlottedElements.call(this).forEach((e) => {
+		if (e instanceof HTMLTemplateElement && e.id === rowID) {
+			template = e;
+		}
+	});
+
+	if (!template) {
+		addErrorAttribute(this, "no template found, please add a template");
+		return;
+	}
+
+	const rowCount = template.content.children.length;
+
+	const headers = [];
+
+	for (let i = 0; i < rowCount; i++) {
+		let hClass = "";
+		const row = template.content.children[i];
+
+		let mode = "";
+		if (row.hasAttribute(ATTRIBUTE_DATATABLE_MODE)) {
+			mode = row.getAttribute(ATTRIBUTE_DATATABLE_MODE);
+		}
+
+		let grid = row.getAttribute(ATTRIBUTE_DATATABLE_GRID_TEMPLATE);
+		if (!grid || grid === "" || grid === "auto") {
+			grid = "minmax(0, 1fr)";
+		}
+
+		let label = "";
+		let labelKey = "";
+
+		if (row.hasAttribute(ATTRIBUTE_DATATABLE_HEAD)) {
+			label = row.getAttribute(ATTRIBUTE_DATATABLE_HEAD);
+			labelKey = label;
+
+			try {
+				if (label.startsWith("i18n:")) {
+					label = label.substring(5, label.length);
+					label = getDocumentTranslations().getText(label, label);
+				}
+			} catch (e) {
+				label = "i18n error " + label;
+			}
+		}
+
+		if (!label) {
+			label = i + 1 + "";
+			mode = ATTRIBUTE_DATATABLE_MODE_FIXED;
+			labelKey = label;
+		}
+
+		if (isObject(hostConfig) && hostConfig.hasOwnProperty(label)) {
+			if (hostConfig[label] === false) {
+				mode = ATTRIBUTE_DATATABLE_MODE_HIDDEN;
+			} else {
+				mode = ATTRIBUTE_DATATABLE_MODE_VISIBLE;
+			}
+		}
+
+		let align = "";
+		if (row.hasAttribute(ATTRIBUTE_DATATABLE_ALIGN)) {
+			align = row.getAttribute(ATTRIBUTE_DATATABLE_ALIGN);
+		}
+
+		switch (align) {
+			case "center":
+				hClass = "flex-center";
+				break;
+			case "end":
+				hClass = "flex-end";
+				break;
+			case "start":
+				hClass = "flex-start";
+				break;
+			default:
+				hClass = "flex-start";
+		}
+
+		let field = "";
+		let direction = DIRECTION_NONE;
+		if (row.hasAttribute(ATTRIBUTE_DATATABLE_SORTABLE)) {
+			field = row.getAttribute(ATTRIBUTE_DATATABLE_SORTABLE).trim();
+			const parts = field.split(" ").map((item) => item.trim());
+			field = parts[0];
+
+			if (headerOrderMap.has(field)) {
+				direction = headerOrderMap.get(field);
+			} else if (
+				parts.length === 2 &&
+				[DIRECTION_ASC, DIRECTION_DESC].indexOf(parts[1]) !== -1
+			) {
+				direction = parts[1];
+			}
+		}
+
+		if (mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
+			hClass += " hidden";
+		}
+
+		const features = [];
+
+		if (row.hasAttribute(ATTRIBUTE_DATATABLE_FEATURES)) {
+			const fl = row.getAttribute(ATTRIBUTE_DATATABLE_FEATURES).split(" ");
+
+			fl.forEach((feature) => {
+				features.push(feature.trim());
+
+				if (feature === "select") {
+					label = "<input type='checkbox' data-monster-role='select-all' />";
+
+					while (row.firstChild) {
+						row.removeChild(row.firstChild);
+					}
+
+					const checkbox = document.createElement("input");
+					checkbox.type = "checkbox";
+					checkbox.setAttribute("data-monster-role", "select-row");
+					row.appendChild(checkbox);
+				}
+			});
+		}
+
+		let orderTemplate = "${field} ${direction}";
+		if (row.hasAttribute("data-monster-order-template")) {
+			orderTemplate = row.getAttribute("data-monster-order-template");
+		}
+
+		const header = new Header();
+		header.setInternals({
+			field: field,
+			label: label,
+			classes: hClass,
+			index: i,
+			mode: mode,
+			grid: grid,
+			labelKey: labelKey,
+			direction: direction,
+			features: features,
+			orderTemplate: orderTemplate
+		});
+
+		headers.push(header);
+	}
+
+	this.setOption("headers", headers);
+	queueMicrotask(() => {
+		storeOrderStatement.call(this, this.getOption("features.autoInit"));
+	});
 }
 
 /**
@@ -1126,101 +1111,101 @@ function initGridAndStructs(hostConfig, headerOrderMap) {
  * @returns {object}
  */
 function getTranslations() {
-    const locale = getLocaleOfDocument();
-    switch (locale.language) {
-        case "de":
-            return {
-                theListContainsNoEntries: "Die Liste enthält keine Einträge",
-                copyAll: "Alles kopieren",
-                helpText:
-                    "<p>Sie können die Werte aus einzelnen Zeilen<br>" +
-                    "in die Zwischenablage kopieren, indem Sie auf die entsprechende Spalte doppelklicken.</p>" +
-                    "<p>Um eine ganze Zeile zu kopieren, halten Sie die Umschalttaste gedrückt, während Sie klicken.<br>" +
-                    "Wenn Sie alle Zeilen kopieren möchten, können Sie die Schaltfläche <strong>Alles kopieren</strong> verwenden.</p>",
-            };
-        case "fr":
-            return {
-                theListContainsNoEntries: "La liste ne contient aucune entrée",
-                copyAll: "Copier tout",
-                helpText:
-                    "<p>Vous pouvez copier les valeurs des rangées individuelles<br>" +
-                    "dans le presse-papiers en double-cliquant sur la colonne concernée.</p>" +
-                    "<p>Pour copier une rangée entière, maintenez la touche Maj enfoncée tout en cliquant.<br>" +
-                    "Si vous souhaitez copier toutes les rangées, vous pouvez utiliser le bouton <strong>Copier tout</strong>.</p>",
-            };
-        case "sp":
-            return {
-                theListContainsNoEntries: "La lista no contiene entradas",
-                copyAll: "Copiar todo",
-                helpText:
-                    "<p>Puedes copiar los valores de filas individuales<br>" +
-                    "al portapapeles haciendo doble clic en la columna correspondiente.</p>" +
-                    "<p>Para copiar una fila entera, mantén presionada la tecla Shift mientras haces clic.<br>" +
-                    "Si quieres copiar todas las filas, puedes usar el botón <strong>Copiar todo</strong>.</p>",
-            };
-        case "it":
-            return {
-                theListContainsNoEntries: "L'elenco non contiene voci",
-                copyAll: "Copia tutto",
-                helpText:
-                    "<p>Puoi copiare i valori dalle singole righe<br>" +
-                    "negli appunti facendo doppio clic sulla colonna relativa.</p>" +
-                    "<p>Per copiare un'intera riga, tieni premuto il tasto Shift mentre clicchi.<br>" +
-                    "Se vuoi copiare tutte le righe, puoi usare il pulsante <strong>Copia tutto</strong>.</p>",
-            };
-        case "pl":
-            return {
-                theListContainsNoEntries: "Lista nie zawiera wpisów",
-                copyAll: "Kopiuj wszystko",
-                helpText:
-                    "<p>Możesz skopiować wartości z poszczególnych wierszy<br>" +
-                    "do schowka, klikając dwukrotnie na odpowiednią kolumnę.</p>" +
-                    "<p>Aby skopiować cały wiersz, przytrzymaj klawisz Shift podczas klikania.<br>" +
-                    "Jeśli chcesz skopiować wszystkie wiersze, możesz użyć przycisku <strong>Kopiuj wszystko</strong>.</p>",
-            };
-        case "no":
-            return {
-                theListContainsNoEntries: "Listen inneholder ingen oppføringer",
-                copyAll: "Kopier alt",
-                helpText:
-                    "<p>Du kan kopiere verdier fra enkeltrader<br>" +
-                    "til utklippstavlen ved å dobbeltklikke på den relevante kolonnen.</p>" +
-                    "<p>For å kopiere en hel rad, hold nede Skift-tasten mens du klikker.<br>" +
-                    "Hvis du vil kopiere alle radene, kan du bruke knappen <strong>Kopier alt</strong>.</p>",
-            };
-        case "dk":
-            return {
-                theListContainsNoEntries: "Listen indeholder ingen poster",
-                copyAll: "Kopiér alt",
-                helpText:
-                    "<p>Du kan kopiere værdier fra enkelte rækker<br>" +
-                    "til udklipsholderen ved at dobbeltklikke på den relevante kolonne.</p>" +
-                    "<p>For at kopiere en hel række, hold Shift-tasten nede, mens du klikker.<br>" +
-                    "Hvis du vil kopiere alle rækker, kan du bruge knappen <strong>Kopiér alt</strong>.</p>",
-            };
-        case "sw":
-            return {
-                theListContainsNoEntries: "Listan innehåller inga poster",
-                copyAll: "Kopiera allt",
-                helpText:
-                    "<p>Du kan kopiera värden från enskilda rader<br>" +
-                    "till urklipp genom att dubbelklicka på den relevanta kolumnen.</p>" +
-                    "<p>För att kopiera en hel rad, håll ned Shift-tangenten medan du klickar.<br>" +
-                    "Om du vill kopiera alla rader kan du använda knappen <strong>Kopiera allt</strong>.</p>",
-            };
-
-        case "en":
-        default:
-            return {
-                theListContainsNoEntries: "The list contains no entries",
-                copyAll: "Copy all",
-                helpText:
-                    "<p>You can copy the values from individual rows<br>" +
-                    "to the clipboard by double-clicking on the relevant column.</p>" +
-                    "<p>To copy an entire row, hold down the Shift key while clicking.<br>" +
-                    "If you want to copy all rows, you can use the <strong>Copy All</strong> button.</p>",
-            };
-    }
+	const locale = getLocaleOfDocument();
+	switch (locale.language) {
+		case "de":
+			return {
+				theListContainsNoEntries: "Die Liste enthält keine Einträge",
+				copyAll: "Alles kopieren",
+				helpText:
+					"<p>Sie können die Werte aus einzelnen Zeilen<br>" +
+					"in die Zwischenablage kopieren, indem Sie auf die entsprechende Spalte doppelklicken.</p>" +
+					"<p>Um eine ganze Zeile zu kopieren, halten Sie die Umschalttaste gedrückt, während Sie klicken.<br>" +
+					"Wenn Sie alle Zeilen kopieren möchten, können Sie die Schaltfläche <strong>Alles kopieren</strong> verwenden.</p>",
+			};
+		case "fr":
+			return {
+				theListContainsNoEntries: "La liste ne contient aucune entrée",
+				copyAll: "Copier tout",
+				helpText:
+					"<p>Vous pouvez copier les valeurs des rangées individuelles<br>" +
+					"dans le presse-papiers en double-cliquant sur la colonne concernée.</p>" +
+					"<p>Pour copier une rangée entière, maintenez la touche Maj enfoncée tout en cliquant.<br>" +
+					"Si vous souhaitez copier toutes les rangées, vous pouvez utiliser le bouton <strong>Copier tout</strong>.</p>",
+			};
+		case "sp":
+			return {
+				theListContainsNoEntries: "La lista no contiene entradas",
+				copyAll: "Copiar todo",
+				helpText:
+					"<p>Puedes copiar los valores de filas individuales<br>" +
+					"al portapapeles haciendo doble clic en la columna correspondiente.</p>" +
+					"<p>Para copiar una fila entera, mantén presionada la tecla Shift mientras haces clic.<br>" +
+					"Si quieres copiar todas las filas, puedes usar el botón <strong>Copiar todo</strong>.</p>",
+			};
+		case "it":
+			return {
+				theListContainsNoEntries: "L'elenco non contiene voci",
+				copyAll: "Copia tutto",
+				helpText:
+					"<p>Puoi copiare i valori dalle singole righe<br>" +
+					"negli appunti facendo doppio clic sulla colonna relativa.</p>" +
+					"<p>Per copiare un'intera riga, tieni premuto il tasto Shift mentre clicchi.<br>" +
+					"Se vuoi copiare tutte le righe, puoi usare il pulsante <strong>Copia tutto</strong>.</p>",
+			};
+		case "pl":
+			return {
+				theListContainsNoEntries: "Lista nie zawiera wpisów",
+				copyAll: "Kopiuj wszystko",
+				helpText:
+					"<p>Możesz skopiować wartości z poszczególnych wierszy<br>" +
+					"do schowka, klikając dwukrotnie na odpowiednią kolumnę.</p>" +
+					"<p>Aby skopiować cały wiersz, przytrzymaj klawisz Shift podczas klikania.<br>" +
+					"Jeśli chcesz skopiować wszystkie wiersze, możesz użyć przycisku <strong>Kopiuj wszystko</strong>.</p>",
+			};
+		case "no":
+			return {
+				theListContainsNoEntries: "Listen inneholder ingen oppføringer",
+				copyAll: "Kopier alt",
+				helpText:
+					"<p>Du kan kopiere verdier fra enkeltrader<br>" +
+					"til utklippstavlen ved å dobbeltklikke på den relevante kolonnen.</p>" +
+					"<p>For å kopiere en hel rad, hold nede Skift-tasten mens du klikker.<br>" +
+					"Hvis du vil kopiere alle radene, kan du bruke knappen <strong>Kopier alt</strong>.</p>",
+			};
+		case "dk":
+			return {
+				theListContainsNoEntries: "Listen indeholder ingen poster",
+				copyAll: "Kopiér alt",
+				helpText:
+					"<p>Du kan kopiere værdier fra enkelte rækker<br>" +
+					"til udklipsholderen ved at dobbeltklikke på den relevante kolonne.</p>" +
+					"<p>For at kopiere en hel række, hold Shift-tasten nede, mens du klikker.<br>" +
+					"Hvis du vil kopiere alle rækker, kan du bruge knappen <strong>Kopiér alt</strong>.</p>",
+			};
+		case "sw":
+			return {
+				theListContainsNoEntries: "Listan innehåller inga poster",
+				copyAll: "Kopiera allt",
+				helpText:
+					"<p>Du kan kopiera värden från enskilda rader<br>" +
+					"till urklipp genom att dubbelklicka på den relevanta kolumnen.</p>" +
+					"<p>För att kopiera en hel rad, håll ned Shift-tangenten medan du klickar.<br>" +
+					"Om du vill kopiera alla rader kan du använda knappen <strong>Kopiera allt</strong>.</p>",
+			};
+
+		case "en":
+		default:
+			return {
+				theListContainsNoEntries: "The list contains no entries",
+				copyAll: "Copy all",
+				helpText:
+					"<p>You can copy the values from individual rows<br>" +
+					"to the clipboard by double-clicking on the relevant column.</p>" +
+					"<p>To copy an entire row, hold down the Shift key while clicking.<br>" +
+					"If you want to copy all rows, you can use the <strong>Copy All</strong> button.</p>",
+			};
+	}
 }
 
 /**
@@ -1228,79 +1213,80 @@ function getTranslations() {
  * @return {string}
  */
 export function getStoredOrderConfigKey() {
-    return generateUniqueConfigKey("datatable", this?.id, "stored-order");
+	return generateUniqueConfigKey("datatable", this?.id, "stored-order");
 }
 
 /**
  * @private
  */
 function storeOrderStatement(doFetch) {
-    const headers = this.getOption("headers");
-    const statement = createOrderStatement(headers);
-    setDataSource.call(this, {orderBy: statement}, doFetch);
+	const headers = this.getOption("headers");
+	const delimiter = this.getOption("datasource.orderDelimiter");
+	const statement = createOrderStatement(headers, delimiter);
+	setDataSource.call(this, { order: statement }, doFetch);
 
-    const host = findElementWithSelectorUpwards(this, "monster-host");
-    if (!(host && this.id)) {
-        return;
-    }
+	const host = findElementWithSelectorUpwards(this, "monster-host");
+	if (!(host && this.id)) {
+		return;
+	}
 
-    const configKey = getStoredOrderConfigKey.call(this);
+	const configKey = getStoredOrderConfigKey.call(this);
 
-    // statement explode with , and remove all empty
-    const list = statement.split(",").filter((item) => item.trim() !== "");
-    if (list.length === 0) {
-        return;
-    }
+	// statement explode with , and remove all empty
+	const list = statement.split(",").filter((item) => item.trim() !== "");
+	if (list.length === 0) {
+		return;
+	}
 
-    host.setConfig(configKey, list);
+	host.setConfig(configKey, list);
 }
 
 /**
  * @private
  */
 function updateGrid() {
-    if (!this[gridElementSymbol]) {
-        throw new Error("no grid element is defined");
-    }
-
-    let gridTemplateColumns = "";
-
-    const headers = this.getOption("headers");
-
-    let styles = "";
-
-    for (let i = 0; i < headers.length; i++) {
-        const header = headers[i];
-
-        if (header.mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
-            styles += `[data-monster-role=datatable]>[data-monster-head="${header.labelKey}"] { display: none; }\n`;
-            styles += `[data-monster-role=datatable-headers]>[data-monster-index="${header.index}"] { display: none; }\n`;
-        } else {
-            gridTemplateColumns += `${header.grid} `;
-        }
-    }
-
-    const sheet = new CSSStyleSheet();
-    if (styles !== "") sheet.replaceSync(styles);
-    this.shadowRoot.adoptedStyleSheets = [...DataTable.getCSSStyleSheet(), sheet];
-
-    const bodyWidth = this.parentNode.clientWidth;
-
-    const breakpoint = this.getOption("responsive.breakpoint");
-    this[dataControlElementSymbol].classList.toggle(
-        "small",
-        bodyWidth <= breakpoint,
-    );
-
-    if (bodyWidth > breakpoint) {
-        this[gridElementSymbol].style.gridTemplateColumns =
-            `${gridTemplateColumns}`;
-        this[gridHeadersElementSymbol].style.gridTemplateColumns =
-            `${gridTemplateColumns}`;
-    } else {
-        this[gridElementSymbol].style.gridTemplateColumns = "auto";
-        this[gridHeadersElementSymbol].style.gridTemplateColumns = "auto";
-    }
+	if (!this[gridElementSymbol]) {
+		throw new Error("no grid element is defined");
+	}
+
+	let gridTemplateColumns = "";
+
+	const headers = this.getOption("headers");
+
+	let styles = "";
+
+	for (let i = 0; i < headers.length; i++) {
+		const header = headers[i];
+
+		if (header.mode === ATTRIBUTE_DATATABLE_MODE_HIDDEN) {
+			styles += `[data-monster-role=datatable]>[data-monster-head="${header.labelKey}"] { display: none; }\n`;
+			styles += `[data-monster-role=datatable-headers]>[data-monster-index="${header.index}"] { display: none; }\n`;
+		} else {
+			gridTemplateColumns += `${header.grid} `;
+		}
+	}
+
+	const sheet = new CSSStyleSheet();
+	if (styles !== "") sheet.replaceSync(styles);
+	this.shadowRoot.adoptedStyleSheets = [...DataTable.getCSSStyleSheet(), sheet];
+
+	const bodyWidth = this.parentNode.clientWidth;
+
+	const breakpoint = this.getOption("responsive.breakpoint");
+	this[dataControlElementSymbol].classList.toggle(
+		"small",
+		bodyWidth <= breakpoint,
+	);
+
+	if (bodyWidth > breakpoint) {
+		this[gridElementSymbol].style.gridTemplateColumns =
+			`${gridTemplateColumns}`;
+		this[gridHeadersElementSymbol].style.gridTemplateColumns =
+			`${gridTemplateColumns}`;
+	} else {
+		this[gridElementSymbol].style.gridTemplateColumns = "auto";
+		this[gridHeadersElementSymbol].style.gridTemplateColumns = "auto";
+	}
 }
 
 /**
@@ -1308,20 +1294,20 @@ function updateGrid() {
  * @param {Header[]} headers
  * @param {bool} doFetch
  */
-function setDataSource({orderBy}, doFetch) {
-    const datasource = this[datasourceLinkedElementSymbol];
+function setDataSource({ order }, doFetch) {
+	const datasource = this[datasourceLinkedElementSymbol];
 
-    if (!datasource) {
-        return;
-    }
+	if (!datasource) {
+		return;
+	}
 
-    if (isFunction(datasource?.setParameters)) {
-        datasource.setParameters({orderBy});
-    }
+	if (isFunction(datasource?.setParameters)) {
+		datasource.setParameters({ order });
+	}
 
-    if (doFetch !== false && isFunction(datasource?.fetch)) {
-        datasource.fetch();
-    }
+	if (doFetch !== false && isFunction(datasource?.fetch)) {
+		datasource.fetch();
+	}
 }
 
 /**
@@ -1329,30 +1315,30 @@ function setDataSource({orderBy}, doFetch) {
  * @return {DataTable}
  */
 function initControlReferences() {
-    if (!this.shadowRoot) {
-        throw new Error("no shadow-root is defined");
-    }
+	if (!this.shadowRoot) {
+		throw new Error("no shadow-root is defined");
+	}
 
-    this[dataControlElementSymbol] = this.shadowRoot.querySelector(
-        "[data-monster-role=control]",
-    );
+	this[dataControlElementSymbol] = this.shadowRoot.querySelector(
+		"[data-monster-role=control]",
+	);
 
-    this[gridElementSymbol] = this.shadowRoot.querySelector(
-        "[data-monster-role=datatable]",
-    );
+	this[gridElementSymbol] = this.shadowRoot.querySelector(
+		"[data-monster-role=datatable]",
+	);
 
-    this[gridHeadersElementSymbol] = this.shadowRoot.querySelector(
-        "[data-monster-role=datatable-headers]",
-    );
+	this[gridHeadersElementSymbol] = this.shadowRoot.querySelector(
+		"[data-monster-role=datatable-headers]",
+	);
 
-    this[columnBarElementSymbol] =
-        this.shadowRoot.querySelector("monster-column-bar");
+	this[columnBarElementSymbol] =
+		this.shadowRoot.querySelector("monster-column-bar");
 
-    this[copyAllElementSymbol] = this.shadowRoot.querySelector(
-        "[data-monster-role=copy-all]",
-    );
+	this[copyAllElementSymbol] = this.shadowRoot.querySelector(
+		"[data-monster-role=copy-all]",
+	);
 
-    return this;
+	return this;
 }
 
 /**
@@ -1362,22 +1348,25 @@ function initControlReferences() {
  * @throws {Error} the datasource could not be initialized
  */
 function initOptionsFromArguments() {
-    const options = {};
-    const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);
+	const options = {};
+	const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);
 
     if (selector) {
         options.datasource = {selector: selector};
     }
 
-    const breakpoint = this.getAttribute(
-        ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
-    );
-    if (breakpoint) {
-        options.responsive = {};
-        options.responsive.breakpoint = parseInt(breakpoint);
-    }
+	options.datasource.orderDelimiter = "," // workaround for the missing orderDelimiter
+
+	const breakpoint = this.getAttribute(
+		ATTRIBUTE_DATATABLE_RESPONSIVE_BREAKPOINT,
+	);
+
+	if (breakpoint) {
+		options.responsive = {};
+		options.responsive.breakpoint = parseInt(breakpoint);
+	}
 
-    return options;
+	return options;
 }
 
 /**
@@ -1385,7 +1374,7 @@ function initOptionsFromArguments() {
  * @return {string}
  */
 function getEmptyTemplate() {
-    return `<monster-state data-monster-role="empty-without-action">
+	return `<monster-state data-monster-role="empty-without-action">
     <div part="visual">
         <svg width="4rem" height="4rem" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
             <path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/>
@@ -1403,8 +1392,8 @@ function getEmptyTemplate() {
  * @return {string}
  */
 function getTemplate() {
-    // language=HTML
-    return `
+	// language=HTML
+	return `
         <div data-monster-role="control" part="control" data-monster-attributes="class path:classes.control">
             <template id="headers-row">
                 <div data-monster-attributes="class path:headers-row.classes,
diff --git a/source/components/datatable/datatable/header.mjs b/source/components/datatable/datatable/header.mjs
index d024151e261162b374ea8bb53633ef6775039be4..554242d023b614e8d0748b165ae597b66fa071f2 100644
--- a/source/components/datatable/datatable/header.mjs
+++ b/source/components/datatable/datatable/header.mjs
@@ -21,6 +21,7 @@ import {
 	validateIterable,
 	validateInstance,
 } from "../../../types/validate.mjs";
+import {Formatter} from "../../../text/formatter.mjs";
 
 export {
 	Header,
@@ -104,6 +105,7 @@ class Header extends Base {
 			mode: undefined,
 			grid: undefined,
 			features: undefined,
+			orderTemplate: undefined,
 		};
 	}
 
@@ -238,10 +240,11 @@ class Header extends Base {
 
 /**
  * @private
- * @param {Array<Header>} headers
+ * @param {Header[]} headers
+ * @param {string} delimiter
  * @return {string}
  */
-function createOrderStatement(headers) {
+function createOrderStatement(headers, delimiter = ",") {
 	validateIterable(headers);
 
 	const oderStatement = [];
@@ -254,7 +257,7 @@ function createOrderStatement(headers) {
 		}
 		oderStatement.push(order);
 	}
-	return oderStatement.join(",");
+	return oderStatement.join(delimiter);
 }
 
 /**
@@ -274,7 +277,13 @@ function updateStruct() {
 	}
 
 	if (direction) {
-		order = `${field} ${direction}`.trim();
+		const tmpl = this.getInternal("orderTemplate");
+		const formatter = new Formatter({
+			"direction": direction,
+			"label": label,
+			"field": field,
+		});
+		order = formatter.format(tmpl);
 	}
 
 	this.setInternal("order", order);
diff --git a/source/components/form/action-button.mjs b/source/components/form/action-button.mjs
index e5b567a728a56984c6828393b4c09dc1add110a3..022fc57b4f6dba19f72e8540a694abbc2343732d 100644
--- a/source/components/form/action-button.mjs
+++ b/source/components/form/action-button.mjs
@@ -31,7 +31,7 @@ import {
 	ATTRIBUTE_ERRORMESSAGE,
 	ATTRIBUTE_ROLE,
 } from "../../dom/constants.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 
 export { ActionButton };
 
diff --git a/source/components/form/api-button.mjs b/source/components/form/api-button.mjs
index 8b7183e1292f35cdb7ae031cfe651d6e872c1805..2c1513d75e2d46af1c032771466df62bba236ec9 100644
--- a/source/components/form/api-button.mjs
+++ b/source/components/form/api-button.mjs
@@ -35,7 +35,7 @@ import { ApiButtonStyleSheet } from "./stylesheet/api-button.mjs";
 import { isObject, isFunction } from "../../types/is.mjs";
 import { getGlobal } from "../../types/global.mjs";
 import { Formatter } from "../../text/formatter.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 export { ApiButton };
 
 /**
diff --git a/source/components/form/button-bar.mjs b/source/components/form/button-bar.mjs
index 76db511bd406b74bed6828813a80adddc5b7cbf5..1ff6671bd0e44fdaaa360a54573d38af20df8548 100644
--- a/source/components/form/button-bar.mjs
+++ b/source/components/form/button-bar.mjs
@@ -41,7 +41,7 @@ import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs";
 import { ButtonBarStyleSheet } from "./stylesheet/button-bar.mjs";
 import { positionPopper } from "./util/floating-ui.mjs";
 import { convertToPixels } from "../../dom/dimension.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 export { ButtonBar };
 
 /**
diff --git a/source/components/form/button.mjs b/source/components/form/button.mjs
index f99c16c0299de90b7bc26856d7cc615ed72ed5b9..0df4d5946dbc14a043b729fb7b5114ac00b82950 100644
--- a/source/components/form/button.mjs
+++ b/source/components/form/button.mjs
@@ -30,7 +30,7 @@ import { ATTRIBUTE_BUTTON_CLASS } from "./constants.mjs";
 import { ButtonStyleSheet } from "./stylesheet/button.mjs";
 import { RippleStyleSheet } from "../stylesheet/ripple.mjs";
 import { fireCustomEvent } from "../../dom/events.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 
 export { Button };
 
diff --git a/source/components/form/context-error.mjs b/source/components/form/context-error.mjs
index 1885544e54eea7b71d64061a0a7d40faf6f4fca3..0b35504599d498d3fa17bce801e99ee4e19ad0ae 100644
--- a/source/components/form/context-error.mjs
+++ b/source/components/form/context-error.mjs
@@ -24,7 +24,7 @@ import {
 	ATTRIBUTE_ERRORMESSAGE,
 	ATTRIBUTE_ROLE,
 } from "../../dom/constants.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 
 export { ContextError };
 
diff --git a/source/components/form/field-set.mjs b/source/components/form/field-set.mjs
index b689981e27ba653ab253d3fbfa906e00cba358eb..11a2496603d4a738ac415e2fc977063cca82590a 100644
--- a/source/components/form/field-set.mjs
+++ b/source/components/form/field-set.mjs
@@ -29,7 +29,7 @@ import { FieldSetStyleSheet } from "./stylesheet/field-set.mjs";
 import "../layout/collapse.mjs";
 import "./toggle-switch.mjs";
 import { getLocaleOfDocument } from "../../dom/locale.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 
 export { FieldSet };
 
diff --git a/source/components/form/popper-button.mjs b/source/components/form/popper-button.mjs
index a6c1b25e7b85614832e08644f1418c1d08c585de..1b46cf718f997013ab94b42acf1dabe9f6d5cd59 100644
--- a/source/components/form/popper-button.mjs
+++ b/source/components/form/popper-button.mjs
@@ -12,27 +12,27 @@
  * SPDX-License-Identifier: AGPL-3.0
  */
 
-import {instanceSymbol} from "../../constants.mjs";
-import {addAttributeToken} from "../../dom/attributes.mjs";
+import { instanceSymbol } from "../../constants.mjs";
+import { addAttributeToken } from "../../dom/attributes.mjs";
 import {
-    ATTRIBUTE_ERRORMESSAGE,
-    ATTRIBUTE_ROLE,
+	ATTRIBUTE_ERRORMESSAGE,
+	ATTRIBUTE_ROLE,
 } from "../../dom/constants.mjs";
 import {
-    assembleMethodSymbol,
-    registerCustomElement,
+	assembleMethodSymbol,
+	registerCustomElement,
 } from "../../dom/customelement.mjs";
-import {getDocument} from "../../dom/util.mjs";
-import {isFunction} from "../../types/is.mjs";
-import {DeadMansSwitch} from "../../util/deadmansswitch.mjs";
-import {Popper} from "./popper.mjs";
-import {STYLE_DISPLAY_MODE_BLOCK} from "./constants.mjs";
-import {PopperButtonStyleSheet} from "./stylesheet/popper-button.mjs";
-import {positionPopper} from "./util/floating-ui.mjs";
+import { getDocument } from "../../dom/util.mjs";
+import { isFunction } from "../../types/is.mjs";
+import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
+import { Popper } from "./popper.mjs";
+import { STYLE_DISPLAY_MODE_BLOCK } from "./constants.mjs";
+import { PopperButtonStyleSheet } from "./stylesheet/popper-button.mjs";
+import { positionPopper } from "./util/floating-ui.mjs";
 import "./button.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 
-export {PopperButton};
+export { PopperButton };
 
 /**
  * @private
@@ -141,205 +141,205 @@ const arrowElementSymbol = Symbol("arrowElement");
  * @fires monster-changed
  */
 class PopperButton extends Popper {
-    /**
-     * This method is called by the `instanceof` operator.
-     * @return {symbol}
-     * @since 2.1.0
-     */
-    static get [instanceSymbol]() {
-        return Symbol.for(
-            "@schukai/monster/components/form/popper-button@@instance",
-        );
-    }
-
-    /**
-     * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
-     * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
-     *
-     * The individual configuration values can be found in the table.
-     *
-     * @property {Object} templates - The templates for the control.
-     * @property {string} templates.main - The main template.
-     * @property {object} labels - The labels for the control.
-     * @property {string} labels.button - The label for the button.
-     * @property {string} content - The content of the popper.
-     * @property {object} popper - The popper options.
-     * @extends {Button.defaults}
-     */
-    get defaults() {
-        return Object.assign({}, super.defaults, {
-            templates: {
-                main: getTemplate(),
-            },
-            actions: {
-                click: (e) => {
-                    this.toggleDialog();
-                },
-            },
-            classes: {
-                button: "monster-button",
-            },
-            labels: {
-                button: '<slot name="button"></slot>',
-            },
-            mode: "click",
-            value: null,
-        });
-    }
-
-    /**
-     *
-     * @return {Monster.Components.Form.PopperButton}
-     */
-    [assembleMethodSymbol]() {
-        super[assembleMethodSymbol]();
-        initControlReferences.call(this);
-        initEventHandler.call(this);
-
-        return this;
-    }
-
-    /**
-     * @return {string}
-     */
-    static getTag() {
-        return "monster-popper-button";
-    }
-
-    /**
-     * @return {CSSStyleSheet[]}
-     */
-    static getCSSStyleSheet() {
-        const styles = super.getCSSStyleSheet();
-        styles.push(PopperButtonStyleSheet);
-        return styles;
-    }
-
-    /**
-     * @return {void}
-     */
-    connectedCallback() {
-        super.connectedCallback();
-
-        const document = getDocument();
-
-        for (const [, type] of Object.entries(["click", "touch"])) {
-            // close on outside ui-events
-            document.addEventListener(type, this[closeEventHandler]);
-        }
-
-        updatePopper.call(this);
-        attachResizeObserver.call(this);
-    }
-
-    /**
-     * @return {void}
-     */
-    disconnectedCallback() {
-        super.disconnectedCallback();
-
-        // close on outside ui-events
-        for (const [, type] of Object.entries(["click", "touch"])) {
-            document.removeEventListener(type, this[closeEventHandler]);
-        }
-
-        disconnectResizeObserver.call(this);
-    }
-
-    /**
-     * The Button.click() method simulates a click on the internal button element.
-     *
-     * @since 3.27.0
-     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
-     */
-    click() {
-        if (this.getOption("disabled") === true) {
-            return;
-        }
-
-        if (
-            this[buttonElementSymbol] &&
-            isFunction(this[buttonElementSymbol].click)
-        ) {
-            this[buttonElementSymbol].click();
-        }
-    }
-
-    /**
-     * The Button.focus() method sets focus on the internal button element.
-     *
-     * @since 3.27.0
-     * @param {Object} options
-     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
-     */
-    focus(options) {
-        if (this.getOption("disabled") === true) {
-            return;
-        }
-
-        if (
-            this[buttonElementSymbol] &&
-            isFunction(this[buttonElementSymbol].focus)
-        ) {
-            this[buttonElementSymbol].focus(options);
-        }
-    }
-
-    /**
-     * The Button.blur() method removes focus from the internal button element.
-     */
-    blur() {
-        if (
-            this[buttonElementSymbol] &&
-            isFunction(this[buttonElementSymbol].blur)
-        ) {
-            this[buttonElementSymbol].blur();
-        }
-    }
-
-    /**
-     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
-     * @return {boolean}
-     */
-    static get formAssociated() {
-        return true;
-    }
-
-    /**
-     * The current selection of the Select
-     *
-     * ```
-     * e = document.querySelector('monster-select');
-     * console.log(e.value)
-     * // ↦ 1
-     * // ↦ ['1','2']
-     * ```
-     *
-     * @property {string|array}
-     */
-    get value() {
-        return this.getOption("value");
-    }
-
-    /**
-     * Set selection
-     *
-     * ```
-     * e = document.querySelector('monster-select');
-     * e.value=1
-     * ```
-     *
-     * @property {string|array} value
-     * @throws {Error} unsupported type
-     */
-    set value(value) {
-        this.setOption("value", value);
-        try {
-            this?.setFormValue(this.value);
-        } catch (e) {
-            addErrorAttribute(this, e);
-        }
-    }
+	/**
+	 * This method is called by the `instanceof` operator.
+	 * @return {symbol}
+	 * @since 2.1.0
+	 */
+	static get [instanceSymbol]() {
+		return Symbol.for(
+			"@schukai/monster/components/form/popper-button@@instance",
+		);
+	}
+
+	/**
+	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
+	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
+	 *
+	 * The individual configuration values can be found in the table.
+	 *
+	 * @property {Object} templates - The templates for the control.
+	 * @property {string} templates.main - The main template.
+	 * @property {object} labels - The labels for the control.
+	 * @property {string} labels.button - The label for the button.
+	 * @property {string} content - The content of the popper.
+	 * @property {object} popper - The popper options.
+	 * @extends {Button.defaults}
+	 */
+	get defaults() {
+		return Object.assign({}, super.defaults, {
+			templates: {
+				main: getTemplate(),
+			},
+			actions: {
+				click: (e) => {
+					this.toggleDialog();
+				},
+			},
+			classes: {
+				button: "monster-button",
+			},
+			labels: {
+				button: '<slot name="button"></slot>',
+			},
+			mode: "click",
+			value: null,
+		});
+	}
+
+	/**
+	 *
+	 * @return {Monster.Components.Form.PopperButton}
+	 */
+	[assembleMethodSymbol]() {
+		super[assembleMethodSymbol]();
+		initControlReferences.call(this);
+		initEventHandler.call(this);
+
+		return this;
+	}
+
+	/**
+	 * @return {string}
+	 */
+	static getTag() {
+		return "monster-popper-button";
+	}
+
+	/**
+	 * @return {CSSStyleSheet[]}
+	 */
+	static getCSSStyleSheet() {
+		const styles = super.getCSSStyleSheet();
+		styles.push(PopperButtonStyleSheet);
+		return styles;
+	}
+
+	/**
+	 * @return {void}
+	 */
+	connectedCallback() {
+		super.connectedCallback();
+
+		const document = getDocument();
+
+		for (const [, type] of Object.entries(["click", "touch"])) {
+			// close on outside ui-events
+			document.addEventListener(type, this[closeEventHandler]);
+		}
+
+		updatePopper.call(this);
+		attachResizeObserver.call(this);
+	}
+
+	/**
+	 * @return {void}
+	 */
+	disconnectedCallback() {
+		super.disconnectedCallback();
+
+		// close on outside ui-events
+		for (const [, type] of Object.entries(["click", "touch"])) {
+			document.removeEventListener(type, this[closeEventHandler]);
+		}
+
+		disconnectResizeObserver.call(this);
+	}
+
+	/**
+	 * The Button.click() method simulates a click on the internal button element.
+	 *
+	 * @since 3.27.0
+	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
+	 */
+	click() {
+		if (this.getOption("disabled") === true) {
+			return;
+		}
+
+		if (
+			this[buttonElementSymbol] &&
+			isFunction(this[buttonElementSymbol].click)
+		) {
+			this[buttonElementSymbol].click();
+		}
+	}
+
+	/**
+	 * The Button.focus() method sets focus on the internal button element.
+	 *
+	 * @since 3.27.0
+	 * @param {Object} options
+	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
+	 */
+	focus(options) {
+		if (this.getOption("disabled") === true) {
+			return;
+		}
+
+		if (
+			this[buttonElementSymbol] &&
+			isFunction(this[buttonElementSymbol].focus)
+		) {
+			this[buttonElementSymbol].focus(options);
+		}
+	}
+
+	/**
+	 * The Button.blur() method removes focus from the internal button element.
+	 */
+	blur() {
+		if (
+			this[buttonElementSymbol] &&
+			isFunction(this[buttonElementSymbol].blur)
+		) {
+			this[buttonElementSymbol].blur();
+		}
+	}
+
+	/**
+	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
+	 * @return {boolean}
+	 */
+	static get formAssociated() {
+		return true;
+	}
+
+	/**
+	 * The current selection of the Select
+	 *
+	 * ```
+	 * e = document.querySelector('monster-select');
+	 * console.log(e.value)
+	 * // ↦ 1
+	 * // ↦ ['1','2']
+	 * ```
+	 *
+	 * @property {string|array}
+	 */
+	get value() {
+		return this.getOption("value");
+	}
+
+	/**
+	 * Set selection
+	 *
+	 * ```
+	 * e = document.querySelector('monster-select');
+	 * e.value=1
+	 * ```
+	 *
+	 * @property {string|array} value
+	 * @throws {Error} unsupported type
+	 */
+	set value(value) {
+		this.setOption("value", value);
+		try {
+			this?.setFormValue(this.value);
+		} catch (e) {
+			addErrorAttribute(this, e);
+		}
+	}
 }
 
 /**
@@ -347,67 +347,67 @@ class PopperButton extends Popper {
  * @return {initEventHandler}
  */
 function initEventHandler() {
-    this[closeEventHandler] = (event) => {
-        const path = event.composedPath();
-
-        for (const [, element] of Object.entries(path)) {
-            if (element === this) {
-                return;
-            }
-        }
-        this.hideDialog();
-    };
-
-    return this;
+	this[closeEventHandler] = (event) => {
+		const path = event.composedPath();
+
+		for (const [, element] of Object.entries(path)) {
+			if (element === this) {
+				return;
+			}
+		}
+		this.hideDialog();
+	};
+
+	return this;
 }
 
 /**
  * @private
  */
 function attachResizeObserver() {
-    // against flickering
-    this[resizeObserverSymbol] = new ResizeObserver(() => {
-        if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
-            try {
-                this[timerCallbackSymbol].touch();
-                return;
-            } catch (e) {
-                delete this[timerCallbackSymbol];
-            }
-        }
-
-        this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
-            updatePopper.call(this);
-        });
-    });
-
-    this[resizeObserverSymbol].observe(this.parentElement);
+	// against flickering
+	this[resizeObserverSymbol] = new ResizeObserver(() => {
+		if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
+			try {
+				this[timerCallbackSymbol].touch();
+				return;
+			} catch (e) {
+				delete this[timerCallbackSymbol];
+			}
+		}
+
+		this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
+			updatePopper.call(this);
+		});
+	});
+
+	this[resizeObserverSymbol].observe(this.parentElement);
 }
 
 function disconnectResizeObserver() {
-    if (this[resizeObserverSymbol] instanceof ResizeObserver) {
-        this[resizeObserverSymbol].disconnect();
-    }
+	if (this[resizeObserverSymbol] instanceof ResizeObserver) {
+		this[resizeObserverSymbol].disconnect();
+	}
 }
 
 /**
  * @private
  */
 function updatePopper() {
-    if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) {
-        return;
-    }
-
-    if (this.getOption("disabled", false) === true) {
-        return;
-    }
-
-    positionPopper.call(
-        this,
-        this[controlElementSymbol],
-        this[popperElementSymbol],
-        this.getOption("popper", {}),
-    );
+	if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) {
+		return;
+	}
+
+	if (this.getOption("disabled", false) === true) {
+		return;
+	}
+
+	positionPopper.call(
+		this,
+		this[controlElementSymbol],
+		this[popperElementSymbol],
+		this.getOption("popper", {}),
+	);
 }
 
 /**
@@ -415,21 +415,21 @@ function updatePopper() {
  * @return {Select}
  */
 function initControlReferences() {
-    this[controlElementSymbol] = this.shadowRoot.querySelector(
-        `[${ATTRIBUTE_ROLE}=control]`,
-    );
+	this[controlElementSymbol] = this.shadowRoot.querySelector(
+		`[${ATTRIBUTE_ROLE}=control]`,
+	);
 
-    this[buttonElementSymbol] = this.shadowRoot.querySelector(
-        `[${ATTRIBUTE_ROLE}=button]`,
-    );
+	this[buttonElementSymbol] = this.shadowRoot.querySelector(
+		`[${ATTRIBUTE_ROLE}=button]`,
+	);
 
-    this[popperElementSymbol] = this.shadowRoot.querySelector(
-        `[${ATTRIBUTE_ROLE}=popper]`,
-    );
+	this[popperElementSymbol] = this.shadowRoot.querySelector(
+		`[${ATTRIBUTE_ROLE}=popper]`,
+	);
 
-    this[arrowElementSymbol] = this.shadowRoot.querySelector(
-        `[${ATTRIBUTE_ROLE}=arrow]`,
-    );
+	this[arrowElementSymbol] = this.shadowRoot.querySelector(
+		`[${ATTRIBUTE_ROLE}=arrow]`,
+	);
 }
 
 /**
@@ -437,8 +437,8 @@ function initControlReferences() {
  * @return {string}
  */
 function getTemplate() {
-    // language=HTML
-    return `
+	// language=HTML
+	return `
         <div data-monster-role="control" part="control">
             <button data-monster-attributes="disabled path:disabled | if:true, class path:classes.button"
                     data-monster-role="button"
diff --git a/source/components/form/reload.mjs b/source/components/form/reload.mjs
index 2120f17382b436297e094260202a49daf912ac4d..639384c7be6754d3a7f0bb5b434e26ea64cb863c 100644
--- a/source/components/form/reload.mjs
+++ b/source/components/form/reload.mjs
@@ -28,7 +28,7 @@ import {
 import { isString } from "../../types/is.mjs";
 import { ATTRIBUTE_FORM_RELOAD, ATTRIBUTE_FORM_URL } from "./constants.mjs";
 import { loadAndAssignContent } from "./util/fetch.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 
 export { Reload };
 
diff --git a/source/components/form/toggle-switch.mjs b/source/components/form/toggle-switch.mjs
index 8cd47552b26d2b9c54fdca917b5885d48b367300..df80f449d970cfb85855da632d6db497de760408 100644
--- a/source/components/form/toggle-switch.mjs
+++ b/source/components/form/toggle-switch.mjs
@@ -31,7 +31,7 @@ import {
 } from "../../dom/constants.mjs";
 import { getWindow } from "../../dom/util.mjs";
 import { fireEvent } from "../../dom/events.mjs";
-import {addErrorAttribute} from "../../dom/error.mjs";
+import { addErrorAttribute } from "../../dom/error.mjs";
 
 export { ToggleSwitch };
 
diff --git a/source/dom/customelement.mjs b/source/dom/customelement.mjs
index 6ed82c75349409eaf878394534715b3980b5b0a9..6852f1dd0e06d7a11e5863dab54ce75a910a5081 100644
--- a/source/dom/customelement.mjs
+++ b/source/dom/customelement.mjs
@@ -12,59 +12,56 @@
  * SPDX-License-Identifier: AGPL-3.0
  */
 
-import {findElementWithIdUpwards, getDocument, getWindow} from "./util.mjs";
-import {internalSymbol} from "../constants.mjs";
-import {extend} from "../data/extend.mjs";
-import {Pathfinder} from "../data/pathfinder.mjs";
-import {Formatter} from "../text/formatter.mjs";
-
-import {parseDataURL} from "../types/dataurl.mjs";
-import {getGlobalObject} from "../types/global.mjs";
+import { findElementWithIdUpwards, getDocument, getWindow } from "./util.mjs";
+import { internalSymbol } from "../constants.mjs";
+import { extend } from "../data/extend.mjs";
+import { Pathfinder } from "../data/pathfinder.mjs";
+import { Formatter } from "../text/formatter.mjs";
+
+import { parseDataURL } from "../types/dataurl.mjs";
+import { getGlobalObject } from "../types/global.mjs";
 import {
-    isArray,
-    isFunction,
-    isIterable,
-    isObject,
-    isString,
+	isArray,
+	isFunction,
+	isIterable,
+	isObject,
+	isString,
 } from "../types/is.mjs";
-import {Observer} from "../types/observer.mjs";
-import {ProxyObserver} from "../types/proxyobserver.mjs";
+import { Observer } from "../types/observer.mjs";
+import { ProxyObserver } from "../types/proxyobserver.mjs";
 import {
-    validateFunction,
-    validateInstance,
-    validateObject,
+	validateFunction,
+	validateInstance,
+	validateObject,
 } from "../types/validate.mjs";
-import {clone} from "../util/clone.mjs";
+import { clone } from "../util/clone.mjs";
+import { getLinkedObjects, hasObjectLink } from "./attributes.mjs";
 import {
-    getLinkedObjects,
-    hasObjectLink,
-} from "./attributes.mjs";
-import {
-    ATTRIBUTE_DISABLED,
-    ATTRIBUTE_OPTIONS,
-    ATTRIBUTE_INIT_CALLBACK,
-    ATTRIBUTE_OPTIONS_SELECTOR,
-    ATTRIBUTE_SCRIPT_HOST,
-    customElementUpdaterLinkSymbol,
-    initControlCallbackName,
+	ATTRIBUTE_DISABLED,
+	ATTRIBUTE_OPTIONS,
+	ATTRIBUTE_INIT_CALLBACK,
+	ATTRIBUTE_OPTIONS_SELECTOR,
+	ATTRIBUTE_SCRIPT_HOST,
+	customElementUpdaterLinkSymbol,
+	initControlCallbackName,
 } from "./constants.mjs";
-import {findDocumentTemplate, Template} from "./template.mjs";
-import {addObjectWithUpdaterToElement} from "./updater.mjs";
-import {instanceSymbol} from "../constants.mjs";
-import {getDocumentTranslations} from "../i18n/translations.mjs";
-import {getSlottedElements} from "./slotted.mjs";
-import {initOptionsFromAttributes} from "./util/init-options-from-attributes.mjs";
-import {setOptionFromAttribute} from "./util/set-option-from-attribute.mjs";
-import {addErrorAttribute} from "./error.mjs";
+import { findDocumentTemplate, Template } from "./template.mjs";
+import { addObjectWithUpdaterToElement } from "./updater.mjs";
+import { instanceSymbol } from "../constants.mjs";
+import { getDocumentTranslations } from "../i18n/translations.mjs";
+import { getSlottedElements } from "./slotted.mjs";
+import { initOptionsFromAttributes } from "./util/init-options-from-attributes.mjs";
+import { setOptionFromAttribute } from "./util/set-option-from-attribute.mjs";
+import { addErrorAttribute } from "./error.mjs";
 
 export {
-    CustomElement,
-    initMethodSymbol,
-    assembleMethodSymbol,
-    attributeObserverSymbol,
-    registerCustomElement,
-    getSlottedElements,
-    updaterTransformerMethodsSymbol,
+	CustomElement,
+	initMethodSymbol,
+	assembleMethodSymbol,
+	attributeObserverSymbol,
+	registerCustomElement,
+	getSlottedElements,
+	updaterTransformerMethodsSymbol,
 };
 
 /**
@@ -76,14 +73,14 @@ const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol");
  * @type {symbol}
  */
 const assembleMethodSymbol = Symbol.for(
-    "@schukai/monster/dom/@@assembleMethodSymbol",
+	"@schukai/monster/dom/@@assembleMethodSymbol",
 );
 
 /**
  * @type {symbol}
  */
 const updaterTransformerMethodsSymbol = Symbol.for(
-    "@schukai/monster/dom/@@updaterTransformerMethodsSymbol",
+	"@schukai/monster/dom/@@updaterTransformerMethodsSymbol",
 );
 
 /**
@@ -91,7 +88,7 @@ const updaterTransformerMethodsSymbol = Symbol.for(
  * @type {symbol}
  */
 const attributeObserverSymbol = Symbol.for(
-    "@schukai/monster/dom/@@attributeObserver",
+	"@schukai/monster/dom/@@attributeObserver",
 );
 
 /**
@@ -99,7 +96,7 @@ const attributeObserverSymbol = Symbol.for(
  * @type {symbol}
  */
 const attributeMutationObserverSymbol = Symbol(
-    "@schukai/monster/dom/@@mutationObserver",
+	"@schukai/monster/dom/@@mutationObserver",
 );
 
 /**
@@ -188,558 +185,555 @@ const scriptHostElementSymbol = Symbol("scriptHostElement");
  * @summary A base class for HTML5 custom controls.
  */
 class CustomElement extends HTMLElement {
-    /**
-     * A new object is created. First, the `initOptions` method is called. Here the
-     * options can be defined in derived classes. Subsequently, the shadowRoot is initialized.
-     *
-     * IMPORTANT: CustomControls instances are not created via the constructor, but either via a tag in the HTML or via <code>document.createElement()</code>.
-     *
-     * @throws {Error} the option attribute does not contain a valid JSON definition.
-     */
-    constructor() {
-        super();
-
-        this[attributeObserverSymbol] = {};
-        this[internalSymbol] = new ProxyObserver({
-            options: initOptionsFromAttributes(this, extend({}, this.defaults)),
-        });
-        this[initMethodSymbol]();
-        initOptionObserver.call(this);
-        this[scriptHostElementSymbol] = [];
-    }
-
-    /**
-     * This method is called by the `instanceof` operator.
-     *
-     * @return {symbol}
-     * @since 2.1.0
-     */
-    static get [instanceSymbol]() {
-        return Symbol.for("@schukai/monster/dom/custom-element@@instance");
-    }
-
-    /**
-     * This method determines which attributes are to be
-     * monitored by `attributeChangedCallback()`. Unfortunately, this method is static.
-     * Therefore, the `observedAttributes` property cannot be changed during runtime.
-     *
-     * @return {string[]}
-     * @since 1.15.0
-     */
-    static get observedAttributes() {
-        return [];
-    }
-
-    /**
-     *
-     * @param attribute
-     * @param callback
-     * @return {CustomElement}
-     */
-    addAttributeObserver(attribute, callback) {
-        validateFunction(callback);
-        this[attributeObserverSymbol][attribute] = callback;
-        return this;
-    }
-
-    /**
-     *
-     * @param attribute
-     * @return {CustomElement}
-     */
-    removeAttributeObserver(attribute) {
-        delete this[attributeObserverSymbol][attribute];
-        return this;
-    }
-
-    /**
-     * The `defaults` property defines the default values for a control. If you want to override these,
-     * you can use various methods, which are described in the documentation available at
-     * {@link https://monsterjs.orgendocconfigurate-a-monster-control}.
-     *
-     * The individual configuration values are listed below:
-     *
-     * More information about the shadowRoot can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow),
-     * in the [HTML Standard](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) or in the [WHATWG Wiki](https://wiki.whatwg.org/wiki/Custom_Elements).
-     *
-     * More information about the template element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).
-     *
-     * More information about the slot element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot).
-     *
-     * @property {boolean} disabled=false Specifies whether the control is disabled. When present, it makes the element non-mutable, non-focusable, and non-submittable with the form.
-     * @property {string} shadowMode=open Specifies the mode of the shadow root. When set to `open`, elements in the shadow root are accessible from JavaScript outside the root, while setting it to `closed` denies access to the root's nodes from JavaScript outside it.
-     * @property {Boolean} delegatesFocus=true Specifies the behavior of the control with respect to focusability. When set to `true`, it mitigates custom element issues around focusability. When a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling.
-     * @property {Object} templates Specifies the templates used by the control.
-     * @property {string} templates.main=undefined Specifies the main template used by the control.
-     * @property {Object} templateMapping Specifies the mapping of templates.
-     * @property {Object} templateFormatter Specifies the formatter for the templates.
-     * @property {Object} templateFormatter.marker Specifies the marker for the templates.
-     * @property {Function} templateFormatter.marker.open=null Specifies the opening marker for the templates.
-     * @property {Function} templateFormatter.marker.close=null Specifies the closing marker for the templates.
-     * @property {Boolean} eventProcessing=false Specifies whether the control processes events.
-     * @since 1.8.0
-     */
-    get defaults() {
-        return {
-            disabled: false,
-            shadowMode: "open",
-            delegatesFocus: true,
-            templates: {
-                main: undefined,
-            },
-            templateMapping: {},
-            templateFormatter: {
-                marker: {
-                    open: null,
-                    close: null,
-                },
-            },
-
-            eventProcessing: false,
-        };
-    }
-
-    /**
-     * This method updates the labels of the element.
-     * The labels are defined in the option object.
-     * The key of the label is used to retrieve the translation from the document.
-     * If the translation is different from the label, the label is updated.
-     *
-     * Before you can use this method, you must have loaded the translations.
-     *
-     * @return {CustomElement}
-     * @throws {Error}  Cannot find an element with translations. Add a translation object to the document.
-     */
-    updateI18n() {
-        let translations;
-
-        try {
-            translations = getDocumentTranslations();
-        } catch (e) {
-            addErrorAttribute(this, e);
-            return this;
-        }
-
-        if (!translations) {
-            return this;
-        }
-
-        const labels = this.getOption("labels");
-        if (!(isObject(labels) || isIterable(labels))) {
-            return this;
-        }
-
-        for (const key in labels) {
-            const def = labels[key];
-
-            if (isString(def)) {
-                const text = translations.getText(key, def);
-                if (text !== def) {
-                    this.setOption(`labels.${key}`, text);
-                }
-                continue;
-            } else if (isObject(def)) {
-                for (const k in def) {
-                    const d = def[k];
-
-                    const text = translations.getPluralRuleText(key, k, d);
-                    if (!isString(text)) {
-                        throw new Error("Invalid labels definition");
-                    }
-                    if (text !== d) {
-                        this.setOption(`labels.${key}.${k}`, text);
-                    }
-                }
-                continue;
-            }
-
-            throw new Error("Invalid labels definition");
-        }
-        return this;
-    }
-
-    /**
-     * The `getTag()` method returns the tag name associated with the custom element. This method should be overwritten
-     * by the derived class.
-     *
-     * Note that there is no check on the name of the tag in this class. It is the responsibility of
-     * the developer to assign an appropriate tag name. If the name is not valid, the
-     * `registerCustomElement()` method will issue an error.
-     *
-     * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
-     * @throws {Error} This method must be overridden by the derived class.
-     * @return {string} The tag name associated with the custom element.
-     * @since 1.7.0
-     */
-    static getTag() {
-        throw new Error(
-            "The method `getTag()` must be overridden by the derived class.",
-        );
-    }
-
-    /**
-     * The `getCSSStyleSheet()` method returns a `CSSStyleSheet` object that defines the styles for the custom element.
-     * If the environment does not support the `CSSStyleSheet` constructor, then an object can be built using the provided detour.
-     *
-     * If `undefined` is returned, then the shadow root does not receive a stylesheet.
-     *
-     * Example usage:
-     *
-     * ```js
-     * class MyElement extends CustomElement {
-     *   static getCSSStyleSheet() {
-     *       const sheet = new CSSStyleSheet();
-     *       sheet.replaceSync("p { color: red; }");
-     *       return sheet;
-     *   }
-     * }
-     * ```
-     *
-     * If the environment does not support the `CSSStyleSheet` constructor,
-     * you can use the following workaround to create the stylesheet:
-     *
-     * ```js
-     * const doc = document.implementation.createHTMLDocument('title');
-     * let style = doc.createElement("style");
-     * style.innerHTML = "p { color: red; }";
-     * style.appendChild(document.createTextNode(""));
-     * doc.head.appendChild(style);
-     * return doc.styleSheets[0];
-     * ```
-     *
-     * @return {CSSStyleSheet|CSSStyleSheet[]|string|undefined} A `CSSStyleSheet` object or an array of such objects that define the styles for the custom element, or `undefined` if no stylesheet should be applied.
-     */
-    static getCSSStyleSheet() {
-        return undefined;
-    }
-
-    /**
-     * attach a new observer
-     *
-     * @param {Observer} observer
-     * @return {CustomElement}
-     */
-    attachObserver(observer) {
-        this[internalSymbol].attachObserver(observer);
-        return this;
-    }
-
-    /**
-     * detach a observer
-     *
-     * @param {Observer} observer
-     * @return {CustomElement}
-     */
-    detachObserver(observer) {
-        this[internalSymbol].detachObserver(observer);
-        return this;
-    }
-
-    /**
-     * @param {Observer} observer
-     * @return {ProxyObserver}
-     */
-    containsObserver(observer) {
-        return this[internalSymbol].containsObserver(observer);
-    }
-
-    /**
-     * nested options can be specified by path `a.b.c`
-     *
-     * @param {string} path
-     * @param {*} defaultValue
-     * @return {*}
-     * @since 1.10.0
-     */
-    getOption(path, defaultValue = undefined) {
-        let value;
-
-        try {
-            value = new Pathfinder(
-                this[internalSymbol].getRealSubject()["options"],
-            ).getVia(path);
-        } catch (e) {
-        }
-
-        if (value === undefined) return defaultValue;
-        return value;
-    }
-
-    /**
-     * Set option and inform elements
-     *
-     * @param {string} path
-     * @param {*} value
-     * @return {CustomElement}
-     * @since 1.14.0
-     */
-    setOption(path, value) {
-        new Pathfinder(this[internalSymbol].getSubject()["options"]).setVia(
-            path,
-            value,
-        );
-        return this;
-    }
-
-    /**
-     * @since 1.15.0
-     * @param {string|object} options
-     * @return {CustomElement}
-     */
-    setOptions(options) {
-        if (isString(options)) {
-            options = parseOptionsJSON.call(this, options);
-        }
-        // 2024-01-21: remove this.defaults, otherwise it will overwrite
-        // the current settings that have already been made.
-        // https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/136
-        extend(this[internalSymbol].getSubject()["options"], options);
-
-        return this;
-    }
-
-    /**
-     * Is called once via the constructor
-     *
-     * @return {CustomElement}
-     * @since 1.8.0
-     */
-    [initMethodSymbol]() {
-        return this;
-    }
-
-    /**
-     * This method is called once when the object is equipped with update for the dynamic change of the dom.
-     * The functions returned here can be used as pipe functions in the template.
-     *
-     * In the example, the function `my-transformer` is defined. In the template, you can use it as follows:
-     *
-     * ```html
-     * <my-element
-     *   data-monster-option-transformer="path:my-value | call:my-transformer">
-     * </my-element>
-     * ```
-     *
-     * The function `my-transformer` is called with the value of `my-value` as a parameter.
-     *
-     * ```js
-     * class MyElement extends CustomElement {
-     * [updaterTransformerMethodsSymbol]() {
-     *    return {
-     *       "my-transformer": (value) => {
-     *           switch (typeof Wert) {
-     *           case "string":
-     *               return value + "!";
-     *           case "Zahl":
-     *               return value + 1;
-     *           default:
-     *               return value;
-     *           }
-     *    }
-     *    };
-     *  };
-     *  }
-     * ```
-     *
-     * @return {object}
-     * @since 2.43.0
-     */
-    [updaterTransformerMethodsSymbol]() {
-        return {};
-    }
-
-    /**
-     * This method is called once when the object is included in the DOM for the first time. It performs the following actions:
-     *
-     * <ol>
-     * <li>Extracts the options from the attributes and the script tag of the element and sets them.</li>
-     * <li>Initializes the shadow root and its CSS stylesheet (if specified).</li>
-     * <li>Initializes the HTML content of the element.</li>
-     * <li>Initializes the custom elements inside the shadow root and the slotted elements.</li>
-     * <li>Attaches a mutation observer to observe changes to the attributes of the element.</li>
-     *
-     * @return {CustomElement} - The updated custom element.
-     * @since 1.8.0
-     */
-    [assembleMethodSymbol]() {
-        let elements;
-        let nodeList;
-
-        // Extract options from attributes and set them
-        const AttributeOptions = getOptionsFromAttributes.call(this);
-        if (
-            isObject(AttributeOptions) &&
-            Object.keys(AttributeOptions).length > 0
-        ) {
-            this.setOptions(AttributeOptions);
-        }
-
-        // Extract options from script tag and set them
-        const ScriptOptions = getOptionsFromScriptTag.call(this);
-        if (isObject(ScriptOptions) && Object.keys(ScriptOptions).length > 0) {
-            this.setOptions(ScriptOptions);
-        }
-
-        // Initialize the shadow root and its CSS stylesheet
-        if (this.getOption("shadowMode", false) !== false) {
-            try {
-                initShadowRoot.call(this);
-                elements = this.shadowRoot.childNodes;
-            } catch (e) {
-                addErrorAttribute(this, e);
-            }
-
-            try {
-                initCSSStylesheet.call(this);
-            } catch (e) {
-                addErrorAttribute(this, e);
-            }
-        }
-
-        // If the elements are not found inside the shadow root, initialize the HTML content of the element
-        if (!(elements instanceof NodeList)) {
-            initHtmlContent.call(this);
-            elements = this.childNodes;
-        }
-
-        // Initialize the custom elements inside the shadow root and the slotted elements
-        initFromCallbackHost.call(this);
-        try {
-            nodeList = new Set([...elements, ...getSlottedElements.call(this)]);
-        } catch (e) {
-            nodeList = elements;
-        }
-
-        try {
-            this[updateCloneDataSymbol] = clone(
-                this[internalSymbol].getRealSubject()["options"],
-            );
-        } catch (e) {
-            addErrorAttribute(this, e);
-        }
-
-        const cfg = {};
-        if (this.getOption("eventProcessing") === true) {
-            cfg.eventProcessing = true;
-        }
-
-        addObjectWithUpdaterToElement.call(
-            this,
-            nodeList,
-            customElementUpdaterLinkSymbol,
-            this[updateCloneDataSymbol],
-            cfg,
-        );
-
-        // Attach a mutation observer to observe changes to the attributes of the element
-        attachAttributeChangeMutationObserver.call(this);
-
-        return this;
-    }
-
-    /**
-     * You know what you are doing? This function is only for advanced users.
-     * The result is a clone of the internal data.
-     *
-     * @return {*}
-     */
-    getInternalUpdateCloneData() {
-        return clone(this[updateCloneDataSymbol]);
-    }
-
-    /**
-     * This method is called every time the element is inserted into the DOM. It checks if the custom element
-     * has already been initialized and if not, calls the assembleMethod to initialize it.
-     *
-     * @return {void}
-     * @since 1.7.0
-     * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/connectedCallback
-     */
-    connectedCallback() {
-        // Check if the object has already been initialized
-        if (!hasObjectLink(this, customElementUpdaterLinkSymbol)) {
-            // If not, call the assembleMethod to initialize the object
-            this[assembleMethodSymbol]();
-        }
-    }
-
-    /**
-     * Called every time the element is removed from the DOM. Useful for running clean up code.
-     *
-     * @return {void}
-     * @since 1.7.0
-     */
-    disconnectedCallback() {
-    }
-
-    /**
-     * The custom element has been moved into a new document (e.g. someone called document.adoptNode(el)).
-     *
-     * @return {void}
-     * @since 1.7.0
-     */
-    adoptedCallback() {
-    }
-
-    /**
-     * Called when an observed attribute has been added, removed, updated, or replaced. Also called for initial
-     * values when an element is created by the parser, or upgraded. Note: only attributes listed in the observedAttributes
-     * property will receive this callback.
-     *
-     * @param {string} attrName
-     * @param {string} oldVal
-     * @param {string} newVal
-     * @return {void}
-     * @since 1.15.0
-     */
-    attributeChangedCallback(attrName, oldVal, newVal) {
-        if (attrName.startsWith("data-monster-option-")) {
-            setOptionFromAttribute(
-                this,
-                attrName,
-                this[internalSymbol].getSubject()["options"],
-            );
-        }
-
-        const callback = this[attributeObserverSymbol]?.[attrName];
-        if (isFunction(callback)) {
-            try {
-                callback.call(this, newVal, oldVal);
-            } catch (e) {
-                addErrorAttribute(this, e);
-            }
-        }
-    }
-
-    /**
-     *
-     * @param {Node} node
-     * @return {boolean}
-     * @throws {TypeError} value is not an instance of
-     * @since 1.19.0
-     */
-    hasNode(node) {
-        if (containChildNode.call(this, validateInstance(node, Node))) {
-            return true;
-        }
-
-        if (!(this.shadowRoot instanceof ShadowRoot)) {
-            return false;
-        }
-
-        return containChildNode.call(this.shadowRoot, node);
-    }
-
-    /**
-     * Calls a callback function if it exists.
-     *
-     * @param {string} name
-     * @param {*} args
-     * @return {*}
-     */
-    callCallback(name, args) {
-        return callControlCallback.call(this, name, ...args);
-    }
+	/**
+	 * A new object is created. First, the `initOptions` method is called. Here the
+	 * options can be defined in derived classes. Subsequently, the shadowRoot is initialized.
+	 *
+	 * IMPORTANT: CustomControls instances are not created via the constructor, but either via a tag in the HTML or via <code>document.createElement()</code>.
+	 *
+	 * @throws {Error} the option attribute does not contain a valid JSON definition.
+	 */
+	constructor() {
+		super();
+
+		this[attributeObserverSymbol] = {};
+		this[internalSymbol] = new ProxyObserver({
+			options: initOptionsFromAttributes(this, extend({}, this.defaults)),
+		});
+		this[initMethodSymbol]();
+		initOptionObserver.call(this);
+		this[scriptHostElementSymbol] = [];
+	}
+
+	/**
+	 * This method is called by the `instanceof` operator.
+	 *
+	 * @return {symbol}
+	 * @since 2.1.0
+	 */
+	static get [instanceSymbol]() {
+		return Symbol.for("@schukai/monster/dom/custom-element@@instance");
+	}
+
+	/**
+	 * This method determines which attributes are to be
+	 * monitored by `attributeChangedCallback()`. Unfortunately, this method is static.
+	 * Therefore, the `observedAttributes` property cannot be changed during runtime.
+	 *
+	 * @return {string[]}
+	 * @since 1.15.0
+	 */
+	static get observedAttributes() {
+		return [];
+	}
+
+	/**
+	 *
+	 * @param attribute
+	 * @param callback
+	 * @return {CustomElement}
+	 */
+	addAttributeObserver(attribute, callback) {
+		validateFunction(callback);
+		this[attributeObserverSymbol][attribute] = callback;
+		return this;
+	}
+
+	/**
+	 *
+	 * @param attribute
+	 * @return {CustomElement}
+	 */
+	removeAttributeObserver(attribute) {
+		delete this[attributeObserverSymbol][attribute];
+		return this;
+	}
+
+	/**
+	 * The `defaults` property defines the default values for a control. If you want to override these,
+	 * you can use various methods, which are described in the documentation available at
+	 * {@link https://monsterjs.orgendocconfigurate-a-monster-control}.
+	 *
+	 * The individual configuration values are listed below:
+	 *
+	 * More information about the shadowRoot can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow),
+	 * in the [HTML Standard](https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements) or in the [WHATWG Wiki](https://wiki.whatwg.org/wiki/Custom_Elements).
+	 *
+	 * More information about the template element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).
+	 *
+	 * More information about the slot element can be found in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot).
+	 *
+	 * @property {boolean} disabled=false Specifies whether the control is disabled. When present, it makes the element non-mutable, non-focusable, and non-submittable with the form.
+	 * @property {string} shadowMode=open Specifies the mode of the shadow root. When set to `open`, elements in the shadow root are accessible from JavaScript outside the root, while setting it to `closed` denies access to the root's nodes from JavaScript outside it.
+	 * @property {Boolean} delegatesFocus=true Specifies the behavior of the control with respect to focusability. When set to `true`, it mitigates custom element issues around focusability. When a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling.
+	 * @property {Object} templates Specifies the templates used by the control.
+	 * @property {string} templates.main=undefined Specifies the main template used by the control.
+	 * @property {Object} templateMapping Specifies the mapping of templates.
+	 * @property {Object} templateFormatter Specifies the formatter for the templates.
+	 * @property {Object} templateFormatter.marker Specifies the marker for the templates.
+	 * @property {Function} templateFormatter.marker.open=null Specifies the opening marker for the templates.
+	 * @property {Function} templateFormatter.marker.close=null Specifies the closing marker for the templates.
+	 * @property {Boolean} eventProcessing=false Specifies whether the control processes events.
+	 * @since 1.8.0
+	 */
+	get defaults() {
+		return {
+			disabled: false,
+			shadowMode: "open",
+			delegatesFocus: true,
+			templates: {
+				main: undefined,
+			},
+			templateMapping: {},
+			templateFormatter: {
+				marker: {
+					open: null,
+					close: null,
+				},
+			},
+
+			eventProcessing: false,
+		};
+	}
+
+	/**
+	 * This method updates the labels of the element.
+	 * The labels are defined in the option object.
+	 * The key of the label is used to retrieve the translation from the document.
+	 * If the translation is different from the label, the label is updated.
+	 *
+	 * Before you can use this method, you must have loaded the translations.
+	 *
+	 * @return {CustomElement}
+	 * @throws {Error}  Cannot find an element with translations. Add a translation object to the document.
+	 */
+	updateI18n() {
+		let translations;
+
+		try {
+			translations = getDocumentTranslations();
+		} catch (e) {
+			addErrorAttribute(this, e);
+			return this;
+		}
+
+		if (!translations) {
+			return this;
+		}
+
+		const labels = this.getOption("labels");
+		if (!(isObject(labels) || isIterable(labels))) {
+			return this;
+		}
+
+		for (const key in labels) {
+			const def = labels[key];
+
+			if (isString(def)) {
+				const text = translations.getText(key, def);
+				if (text !== def) {
+					this.setOption(`labels.${key}`, text);
+				}
+				continue;
+			} else if (isObject(def)) {
+				for (const k in def) {
+					const d = def[k];
+
+					const text = translations.getPluralRuleText(key, k, d);
+					if (!isString(text)) {
+						throw new Error("Invalid labels definition");
+					}
+					if (text !== d) {
+						this.setOption(`labels.${key}.${k}`, text);
+					}
+				}
+				continue;
+			}
+
+			throw new Error("Invalid labels definition");
+		}
+		return this;
+	}
+
+	/**
+	 * The `getTag()` method returns the tag name associated with the custom element. This method should be overwritten
+	 * by the derived class.
+	 *
+	 * Note that there is no check on the name of the tag in this class. It is the responsibility of
+	 * the developer to assign an appropriate tag name. If the name is not valid, the
+	 * `registerCustomElement()` method will issue an error.
+	 *
+	 * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
+	 * @throws {Error} This method must be overridden by the derived class.
+	 * @return {string} The tag name associated with the custom element.
+	 * @since 1.7.0
+	 */
+	static getTag() {
+		throw new Error(
+			"The method `getTag()` must be overridden by the derived class.",
+		);
+	}
+
+	/**
+	 * The `getCSSStyleSheet()` method returns a `CSSStyleSheet` object that defines the styles for the custom element.
+	 * If the environment does not support the `CSSStyleSheet` constructor, then an object can be built using the provided detour.
+	 *
+	 * If `undefined` is returned, then the shadow root does not receive a stylesheet.
+	 *
+	 * Example usage:
+	 *
+	 * ```js
+	 * class MyElement extends CustomElement {
+	 *   static getCSSStyleSheet() {
+	 *       const sheet = new CSSStyleSheet();
+	 *       sheet.replaceSync("p { color: red; }");
+	 *       return sheet;
+	 *   }
+	 * }
+	 * ```
+	 *
+	 * If the environment does not support the `CSSStyleSheet` constructor,
+	 * you can use the following workaround to create the stylesheet:
+	 *
+	 * ```js
+	 * const doc = document.implementation.createHTMLDocument('title');
+	 * let style = doc.createElement("style");
+	 * style.innerHTML = "p { color: red; }";
+	 * style.appendChild(document.createTextNode(""));
+	 * doc.head.appendChild(style);
+	 * return doc.styleSheets[0];
+	 * ```
+	 *
+	 * @return {CSSStyleSheet|CSSStyleSheet[]|string|undefined} A `CSSStyleSheet` object or an array of such objects that define the styles for the custom element, or `undefined` if no stylesheet should be applied.
+	 */
+	static getCSSStyleSheet() {
+		return undefined;
+	}
+
+	/**
+	 * attach a new observer
+	 *
+	 * @param {Observer} observer
+	 * @return {CustomElement}
+	 */
+	attachObserver(observer) {
+		this[internalSymbol].attachObserver(observer);
+		return this;
+	}
+
+	/**
+	 * detach a observer
+	 *
+	 * @param {Observer} observer
+	 * @return {CustomElement}
+	 */
+	detachObserver(observer) {
+		this[internalSymbol].detachObserver(observer);
+		return this;
+	}
+
+	/**
+	 * @param {Observer} observer
+	 * @return {ProxyObserver}
+	 */
+	containsObserver(observer) {
+		return this[internalSymbol].containsObserver(observer);
+	}
+
+	/**
+	 * nested options can be specified by path `a.b.c`
+	 *
+	 * @param {string} path
+	 * @param {*} defaultValue
+	 * @return {*}
+	 * @since 1.10.0
+	 */
+	getOption(path, defaultValue = undefined) {
+		let value;
+
+		try {
+			value = new Pathfinder(
+				this[internalSymbol].getRealSubject()["options"],
+			).getVia(path);
+		} catch (e) {}
+
+		if (value === undefined) return defaultValue;
+		return value;
+	}
+
+	/**
+	 * Set option and inform elements
+	 *
+	 * @param {string} path
+	 * @param {*} value
+	 * @return {CustomElement}
+	 * @since 1.14.0
+	 */
+	setOption(path, value) {
+		new Pathfinder(this[internalSymbol].getSubject()["options"]).setVia(
+			path,
+			value,
+		);
+		return this;
+	}
+
+	/**
+	 * @since 1.15.0
+	 * @param {string|object} options
+	 * @return {CustomElement}
+	 */
+	setOptions(options) {
+		if (isString(options)) {
+			options = parseOptionsJSON.call(this, options);
+		}
+		// 2024-01-21: remove this.defaults, otherwise it will overwrite
+		// the current settings that have already been made.
+		// https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/136
+		extend(this[internalSymbol].getSubject()["options"], options);
+
+		return this;
+	}
+
+	/**
+	 * Is called once via the constructor
+	 *
+	 * @return {CustomElement}
+	 * @since 1.8.0
+	 */
+	[initMethodSymbol]() {
+		return this;
+	}
+
+	/**
+	 * This method is called once when the object is equipped with update for the dynamic change of the dom.
+	 * The functions returned here can be used as pipe functions in the template.
+	 *
+	 * In the example, the function `my-transformer` is defined. In the template, you can use it as follows:
+	 *
+	 * ```html
+	 * <my-element
+	 *   data-monster-option-transformer="path:my-value | call:my-transformer">
+	 * </my-element>
+	 * ```
+	 *
+	 * The function `my-transformer` is called with the value of `my-value` as a parameter.
+	 *
+	 * ```js
+	 * class MyElement extends CustomElement {
+	 * [updaterTransformerMethodsSymbol]() {
+	 *    return {
+	 *       "my-transformer": (value) => {
+	 *           switch (typeof Wert) {
+	 *           case "string":
+	 *               return value + "!";
+	 *           case "Zahl":
+	 *               return value + 1;
+	 *           default:
+	 *               return value;
+	 *           }
+	 *    }
+	 *    };
+	 *  };
+	 *  }
+	 * ```
+	 *
+	 * @return {object}
+	 * @since 2.43.0
+	 */
+	[updaterTransformerMethodsSymbol]() {
+		return {};
+	}
+
+	/**
+	 * This method is called once when the object is included in the DOM for the first time. It performs the following actions:
+	 *
+	 * <ol>
+	 * <li>Extracts the options from the attributes and the script tag of the element and sets them.</li>
+	 * <li>Initializes the shadow root and its CSS stylesheet (if specified).</li>
+	 * <li>Initializes the HTML content of the element.</li>
+	 * <li>Initializes the custom elements inside the shadow root and the slotted elements.</li>
+	 * <li>Attaches a mutation observer to observe changes to the attributes of the element.</li>
+	 *
+	 * @return {CustomElement} - The updated custom element.
+	 * @since 1.8.0
+	 */
+	[assembleMethodSymbol]() {
+		let elements;
+		let nodeList;
+
+		// Extract options from attributes and set them
+		const AttributeOptions = getOptionsFromAttributes.call(this);
+		if (
+			isObject(AttributeOptions) &&
+			Object.keys(AttributeOptions).length > 0
+		) {
+			this.setOptions(AttributeOptions);
+		}
+
+		// Extract options from script tag and set them
+		const ScriptOptions = getOptionsFromScriptTag.call(this);
+		if (isObject(ScriptOptions) && Object.keys(ScriptOptions).length > 0) {
+			this.setOptions(ScriptOptions);
+		}
+
+		// Initialize the shadow root and its CSS stylesheet
+		if (this.getOption("shadowMode", false) !== false) {
+			try {
+				initShadowRoot.call(this);
+				elements = this.shadowRoot.childNodes;
+			} catch (e) {
+				addErrorAttribute(this, e);
+			}
+
+			try {
+				initCSSStylesheet.call(this);
+			} catch (e) {
+				addErrorAttribute(this, e);
+			}
+		}
+
+		// If the elements are not found inside the shadow root, initialize the HTML content of the element
+		if (!(elements instanceof NodeList)) {
+			initHtmlContent.call(this);
+			elements = this.childNodes;
+		}
+
+		// Initialize the custom elements inside the shadow root and the slotted elements
+		initFromCallbackHost.call(this);
+		try {
+			nodeList = new Set([...elements, ...getSlottedElements.call(this)]);
+		} catch (e) {
+			nodeList = elements;
+		}
+
+		try {
+			this[updateCloneDataSymbol] = clone(
+				this[internalSymbol].getRealSubject()["options"],
+			);
+		} catch (e) {
+			addErrorAttribute(this, e);
+		}
+
+		const cfg = {};
+		if (this.getOption("eventProcessing") === true) {
+			cfg.eventProcessing = true;
+		}
+
+		addObjectWithUpdaterToElement.call(
+			this,
+			nodeList,
+			customElementUpdaterLinkSymbol,
+			this[updateCloneDataSymbol],
+			cfg,
+		);
+
+		// Attach a mutation observer to observe changes to the attributes of the element
+		attachAttributeChangeMutationObserver.call(this);
+
+		return this;
+	}
+
+	/**
+	 * You know what you are doing? This function is only for advanced users.
+	 * The result is a clone of the internal data.
+	 *
+	 * @return {*}
+	 */
+	getInternalUpdateCloneData() {
+		return clone(this[updateCloneDataSymbol]);
+	}
+
+	/**
+	 * This method is called every time the element is inserted into the DOM. It checks if the custom element
+	 * has already been initialized and if not, calls the assembleMethod to initialize it.
+	 *
+	 * @return {void}
+	 * @since 1.7.0
+	 * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/connectedCallback
+	 */
+	connectedCallback() {
+		// Check if the object has already been initialized
+		if (!hasObjectLink(this, customElementUpdaterLinkSymbol)) {
+			// If not, call the assembleMethod to initialize the object
+			this[assembleMethodSymbol]();
+		}
+	}
+
+	/**
+	 * Called every time the element is removed from the DOM. Useful for running clean up code.
+	 *
+	 * @return {void}
+	 * @since 1.7.0
+	 */
+	disconnectedCallback() {}
+
+	/**
+	 * The custom element has been moved into a new document (e.g. someone called document.adoptNode(el)).
+	 *
+	 * @return {void}
+	 * @since 1.7.0
+	 */
+	adoptedCallback() {}
+
+	/**
+	 * Called when an observed attribute has been added, removed, updated, or replaced. Also called for initial
+	 * values when an element is created by the parser, or upgraded. Note: only attributes listed in the observedAttributes
+	 * property will receive this callback.
+	 *
+	 * @param {string} attrName
+	 * @param {string} oldVal
+	 * @param {string} newVal
+	 * @return {void}
+	 * @since 1.15.0
+	 */
+	attributeChangedCallback(attrName, oldVal, newVal) {
+		if (attrName.startsWith("data-monster-option-")) {
+			setOptionFromAttribute(
+				this,
+				attrName,
+				this[internalSymbol].getSubject()["options"],
+			);
+		}
+
+		const callback = this[attributeObserverSymbol]?.[attrName];
+		if (isFunction(callback)) {
+			try {
+				callback.call(this, newVal, oldVal);
+			} catch (e) {
+				addErrorAttribute(this, e);
+			}
+		}
+	}
+
+	/**
+	 *
+	 * @param {Node} node
+	 * @return {boolean}
+	 * @throws {TypeError} value is not an instance of
+	 * @since 1.19.0
+	 */
+	hasNode(node) {
+		if (containChildNode.call(this, validateInstance(node, Node))) {
+			return true;
+		}
+
+		if (!(this.shadowRoot instanceof ShadowRoot)) {
+			return false;
+		}
+
+		return containChildNode.call(this.shadowRoot, node);
+	}
+
+	/**
+	 * Calls a callback function if it exists.
+	 *
+	 * @param {string} name
+	 * @param {*} args
+	 * @return {*}
+	 */
+	callCallback(name, args) {
+		return callControlCallback.call(this, name, ...args);
+	}
 }
 
 /**
@@ -748,49 +742,46 @@ class CustomElement extends HTMLElement {
  * @return {any}
  */
 function callControlCallback(callBackFunctionName, ...args) {
-    if (!isString(callBackFunctionName) || callBackFunctionName === "") {
-        return;
-    }
-
-    if (callBackFunctionName in this) {
-        return this[callBackFunctionName](this, ...args);
-    }
-
-    if (!this.hasAttribute(ATTRIBUTE_SCRIPT_HOST)) {
-        return;
-    }
-
-    if (this[scriptHostElementSymbol].length === 0) {
-        const targetId = this.getAttribute(ATTRIBUTE_SCRIPT_HOST);
-        if (!targetId) {
-            return;
-        }
-
-        const list = targetId.split(",");
-        for (const id of list) {
-            const host = findElementWithIdUpwards(this, targetId);
-            if (!(host instanceof HTMLElement)) {
-                continue;
-            }
-
-            this[scriptHostElementSymbol].push(host);
-        }
-    }
-
-    for (const host of this[scriptHostElementSymbol]) {
-        if (callBackFunctionName in host) {
-            try {
-                return host[callBackFunctionName](this, ...args);
-            } catch (e) {
-                addErrorAttribute(this, e);
-            }
-        }
-    }
-
-    addErrorAttribute(
-        this,
-        `callback ${callBackFunctionName} not found`,
-    );
+	if (!isString(callBackFunctionName) || callBackFunctionName === "") {
+		return;
+	}
+
+	if (callBackFunctionName in this) {
+		return this[callBackFunctionName](this, ...args);
+	}
+
+	if (!this.hasAttribute(ATTRIBUTE_SCRIPT_HOST)) {
+		return;
+	}
+
+	if (this[scriptHostElementSymbol].length === 0) {
+		const targetId = this.getAttribute(ATTRIBUTE_SCRIPT_HOST);
+		if (!targetId) {
+			return;
+		}
+
+		const list = targetId.split(",");
+		for (const id of list) {
+			const host = findElementWithIdUpwards(this, targetId);
+			if (!(host instanceof HTMLElement)) {
+				continue;
+			}
+
+			this[scriptHostElementSymbol].push(host);
+		}
+	}
+
+	for (const host of this[scriptHostElementSymbol]) {
+		if (callBackFunctionName in host) {
+			try {
+				return host[callBackFunctionName](this, ...args);
+			} catch (e) {
+				addErrorAttribute(this, e);
+			}
+		}
+	}
+
+	addErrorAttribute(this, `callback ${callBackFunctionName} not found`);
 }
 
 /**
@@ -807,16 +798,16 @@ function callControlCallback(callBackFunctionName, ...args) {
  * @since 1.8.0
  */
 function initFromCallbackHost() {
-    // Set the default callback function name
-    let callBackFunctionName = initControlCallbackName;
+	// Set the default callback function name
+	let callBackFunctionName = initControlCallbackName;
 
-    // If the `data-monster-option-callback` attribute is set, use its value as the callback function name
-    if (this.hasAttribute(ATTRIBUTE_INIT_CALLBACK)) {
-        callBackFunctionName = this.getAttribute(ATTRIBUTE_INIT_CALLBACK);
-    }
+	// If the `data-monster-option-callback` attribute is set, use its value as the callback function name
+	if (this.hasAttribute(ATTRIBUTE_INIT_CALLBACK)) {
+		callBackFunctionName = this.getAttribute(ATTRIBUTE_INIT_CALLBACK);
+	}
 
-    // Call the callback function with the element as a parameter if it exists
-    callControlCallback.call(this, callBackFunctionName);
+	// Call the callback function with the element as a parameter if it exists
+	callControlCallback.call(this, callBackFunctionName);
 }
 
 /**
@@ -826,34 +817,34 @@ function initFromCallbackHost() {
  * @this CustomElement
  */
 function attachAttributeChangeMutationObserver() {
-    const self = this;
-
-    if (typeof self[attributeMutationObserverSymbol] !== "undefined") {
-        return;
-    }
-
-    self[attributeMutationObserverSymbol] = new MutationObserver(
-        function (mutations, observer) {
-            for (const mutation of mutations) {
-                if (mutation.type === "attributes") {
-                    self.attributeChangedCallback(
-                        mutation.attributeName,
-                        mutation.oldValue,
-                        mutation.target.getAttribute(mutation.attributeName),
-                    );
-                }
-            }
-        },
-    );
-
-    try {
-        self[attributeMutationObserverSymbol].observe(self, {
-            attributes: true,
-            attributeOldValue: true,
-        });
-    } catch (e) {
-        addErrorAttribute(self, e);
-    }
+	const self = this;
+
+	if (typeof self[attributeMutationObserverSymbol] !== "undefined") {
+		return;
+	}
+
+	self[attributeMutationObserverSymbol] = new MutationObserver(
+		function (mutations, observer) {
+			for (const mutation of mutations) {
+				if (mutation.type === "attributes") {
+					self.attributeChangedCallback(
+						mutation.attributeName,
+						mutation.oldValue,
+						mutation.target.getAttribute(mutation.attributeName),
+					);
+				}
+			}
+		},
+	);
+
+	try {
+		self[attributeMutationObserverSymbol].observe(self, {
+			attributes: true,
+			attributeOldValue: true,
+		});
+	} catch (e) {
+		addErrorAttribute(self, e);
+	}
 }
 
 /**
@@ -863,19 +854,19 @@ function attachAttributeChangeMutationObserver() {
  * @return {boolean}
  */
 function containChildNode(node) {
-    if (this.contains(node)) {
-        return true;
-    }
+	if (this.contains(node)) {
+		return true;
+	}
 
-    for (const [, e] of Object.entries(this.childNodes)) {
-        if (e.contains(node)) {
-            return true;
-        }
+	for (const [, e] of Object.entries(this.childNodes)) {
+		if (e.contains(node)) {
+			return true;
+		}
 
-        containChildNode.call(e, node);
-    }
+		containChildNode.call(e, node);
+	}
 
-    return false;
+	return false;
 }
 
 /**
@@ -885,89 +876,89 @@ function containChildNode(node) {
  * @this CustomElement
  */
 function initOptionObserver() {
-    const self = this;
-
-    let lastDisabledValue = undefined;
-    self.attachObserver(
-        new Observer(function () {
-            const flag = self.getOption("disabled");
-
-            if (flag === lastDisabledValue) {
-                return;
-            }
-
-            lastDisabledValue = flag;
-
-            if (!(self.shadowRoot instanceof ShadowRoot)) {
-                return;
-            }
-
-            const query =
-                "button, command, fieldset, keygen, optgroup, option, select, textarea, input, [data-monster-objectlink]";
-            const elements = self.shadowRoot.querySelectorAll(query);
-
-            let nodeList;
-            try {
-                nodeList = new Set([
-                    ...elements,
-                    ...getSlottedElements.call(self, query),
-                ]);
-            } catch (e) {
-                nodeList = elements;
-            }
-
-            for (const element of [...nodeList]) {
-                if (flag === true) {
-                    element.setAttribute(ATTRIBUTE_DISABLED, "");
-                } else {
-                    element.removeAttribute(ATTRIBUTE_DISABLED);
-                }
-            }
-        }),
-    );
-
-    self.attachObserver(
-        new Observer(function () {
-            // not initialised
-            if (!hasObjectLink(self, customElementUpdaterLinkSymbol)) {
-                return;
-            }
-            // inform every element
-            const updaters = getLinkedObjects(self, customElementUpdaterLinkSymbol);
-
-            for (const list of updaters) {
-                for (const updater of list) {
-                    const d = clone(self[internalSymbol].getRealSubject()["options"]);
-                    Object.assign(updater.getSubject(), d);
-                }
-            }
-        }),
-    );
-
-    // disabled
-    self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => {
-        if (self.hasAttribute(ATTRIBUTE_DISABLED)) {
-            self.setOption(ATTRIBUTE_DISABLED, true);
-        } else {
-            self.setOption(ATTRIBUTE_DISABLED, undefined);
-        }
-    };
-
-    // data-monster-options
-    self[attributeObserverSymbol][ATTRIBUTE_OPTIONS] = () => {
-        const options = getOptionsFromAttributes.call(self);
-        if (isObject(options) && Object.keys(options).length > 0) {
-            self.setOptions(options);
-        }
-    };
-
-    // data-monster-options-selector
-    self[attributeObserverSymbol][ATTRIBUTE_OPTIONS_SELECTOR] = () => {
-        const options = getOptionsFromScriptTag.call(self);
-        if (isObject(options) && Object.keys(options).length > 0) {
-            self.setOptions(options);
-        }
-    };
+	const self = this;
+
+	let lastDisabledValue = undefined;
+	self.attachObserver(
+		new Observer(function () {
+			const flag = self.getOption("disabled");
+
+			if (flag === lastDisabledValue) {
+				return;
+			}
+
+			lastDisabledValue = flag;
+
+			if (!(self.shadowRoot instanceof ShadowRoot)) {
+				return;
+			}
+
+			const query =
+				"button, command, fieldset, keygen, optgroup, option, select, textarea, input, [data-monster-objectlink]";
+			const elements = self.shadowRoot.querySelectorAll(query);
+
+			let nodeList;
+			try {
+				nodeList = new Set([
+					...elements,
+					...getSlottedElements.call(self, query),
+				]);
+			} catch (e) {
+				nodeList = elements;
+			}
+
+			for (const element of [...nodeList]) {
+				if (flag === true) {
+					element.setAttribute(ATTRIBUTE_DISABLED, "");
+				} else {
+					element.removeAttribute(ATTRIBUTE_DISABLED);
+				}
+			}
+		}),
+	);
+
+	self.attachObserver(
+		new Observer(function () {
+			// not initialised
+			if (!hasObjectLink(self, customElementUpdaterLinkSymbol)) {
+				return;
+			}
+			// inform every element
+			const updaters = getLinkedObjects(self, customElementUpdaterLinkSymbol);
+
+			for (const list of updaters) {
+				for (const updater of list) {
+					const d = clone(self[internalSymbol].getRealSubject()["options"]);
+					Object.assign(updater.getSubject(), d);
+				}
+			}
+		}),
+	);
+
+	// disabled
+	self[attributeObserverSymbol][ATTRIBUTE_DISABLED] = () => {
+		if (self.hasAttribute(ATTRIBUTE_DISABLED)) {
+			self.setOption(ATTRIBUTE_DISABLED, true);
+		} else {
+			self.setOption(ATTRIBUTE_DISABLED, undefined);
+		}
+	};
+
+	// data-monster-options
+	self[attributeObserverSymbol][ATTRIBUTE_OPTIONS] = () => {
+		const options = getOptionsFromAttributes.call(self);
+		if (isObject(options) && Object.keys(options).length > 0) {
+			self.setOptions(options);
+		}
+	};
+
+	// data-monster-options-selector
+	self[attributeObserverSymbol][ATTRIBUTE_OPTIONS_SELECTOR] = () => {
+		const options = getOptionsFromScriptTag.call(self);
+		if (isObject(options) && Object.keys(options).length > 0) {
+			self.setOptions(options);
+		}
+	};
 }
 
 /**
@@ -976,35 +967,35 @@ function initOptionObserver() {
  * @throws {TypeError} value is not a object
  */
 function getOptionsFromScriptTag() {
-    if (!this.hasAttribute(ATTRIBUTE_OPTIONS_SELECTOR)) {
-        return {};
-    }
-
-    const node = document.querySelector(
-        this.getAttribute(ATTRIBUTE_OPTIONS_SELECTOR),
-    );
-    if (!(node instanceof HTMLScriptElement)) {
-        addErrorAttribute(
-            this,
-            `the selector ${ATTRIBUTE_OPTIONS_SELECTOR} for options was specified (${this.getAttribute(
-                ATTRIBUTE_OPTIONS_SELECTOR,
-            )}) but not found.`,
-        );
-        return {};
-    }
-
-    let obj = {};
-
-    try {
-        obj = parseOptionsJSON.call(this, node.textContent.trim());
-    } catch (e) {
-        addErrorAttribute(
-            this,
-            `when analyzing the configuration from the script tag there was an error. ${e}`,
-        );
-    }
-
-    return obj;
+	if (!this.hasAttribute(ATTRIBUTE_OPTIONS_SELECTOR)) {
+		return {};
+	}
+
+	const node = document.querySelector(
+		this.getAttribute(ATTRIBUTE_OPTIONS_SELECTOR),
+	);
+	if (!(node instanceof HTMLScriptElement)) {
+		addErrorAttribute(
+			this,
+			`the selector ${ATTRIBUTE_OPTIONS_SELECTOR} for options was specified (${this.getAttribute(
+				ATTRIBUTE_OPTIONS_SELECTOR,
+			)}) but not found.`,
+		);
+		return {};
+	}
+
+	let obj = {};
+
+	try {
+		obj = parseOptionsJSON.call(this, node.textContent.trim());
+	} catch (e) {
+		addErrorAttribute(
+			this,
+			`when analyzing the configuration from the script tag there was an error. ${e}`,
+		);
+	}
+
+	return obj;
 }
 
 /**
@@ -1012,20 +1003,20 @@ function getOptionsFromScriptTag() {
  * @return {object}
  */
 function getOptionsFromAttributes() {
-    if (this.hasAttribute(ATTRIBUTE_OPTIONS)) {
-        try {
-            return parseOptionsJSON.call(this, this.getAttribute(ATTRIBUTE_OPTIONS));
-        } catch (e) {
-            addErrorAttribute(
-                this,
-                `the options attribute ${ATTRIBUTE_OPTIONS} does not contain a valid json definition (actual: ${this.getAttribute(
-                    ATTRIBUTE_OPTIONS,
-                )}).${e}`,
-            );
-        }
-    }
-
-    return {};
+	if (this.hasAttribute(ATTRIBUTE_OPTIONS)) {
+		try {
+			return parseOptionsJSON.call(this, this.getAttribute(ATTRIBUTE_OPTIONS));
+		} catch (e) {
+			addErrorAttribute(
+				this,
+				`the options attribute ${ATTRIBUTE_OPTIONS} does not contain a valid json definition (actual: ${this.getAttribute(
+					ATTRIBUTE_OPTIONS,
+				)}).${e}`,
+			);
+		}
+	}
+
+	return {};
 }
 
 /**
@@ -1037,26 +1028,25 @@ function getOptionsFromAttributes() {
  * @throws {error} Throws an error if the JSON data is not valid.
  */
 function parseOptionsJSON(data) {
-    let obj = {};
-
-    if (!isString(data)) {
-        return obj;
-    }
-
-    // the configuration can be specified as a data url.
-    try {
-        const dataUrl = parseDataURL(data);
-        data = dataUrl.content;
-    } catch (e) {
-    }
-
-    try {
-        obj = JSON.parse(data);
-    } catch (e) {
-        throw e;
-    }
-
-    return validateObject(obj);
+	let obj = {};
+
+	if (!isString(data)) {
+		return obj;
+	}
+
+	// the configuration can be specified as a data url.
+	try {
+		const dataUrl = parseDataURL(data);
+		data = dataUrl.content;
+	} catch (e) {}
+
+	try {
+		obj = JSON.parse(data);
+	} catch (e) {
+		throw e;
+	}
+
+	return validateObject(obj);
 }
 
 /**
@@ -1064,21 +1054,21 @@ function parseOptionsJSON(data) {
  * @return {initHtmlContent}
  */
 function initHtmlContent() {
-    try {
-        const template = findDocumentTemplate(this.constructor.getTag());
-        this.appendChild(template.createDocumentFragment());
-    } catch (e) {
-        let html = this.getOption("templates.main", "");
-        if (isString(html) && html.length > 0) {
-            const mapping = this.getOption("templateMapping", {});
-            if (isObject(mapping)) {
-                html = new Formatter(mapping, {}).format(html);
-            }
-            this.innerHTML = html;
-        }
-    }
-
-    return this;
+	try {
+		const template = findDocumentTemplate(this.constructor.getTag());
+		this.appendChild(template.createDocumentFragment());
+	} catch (e) {
+		let html = this.getOption("templates.main", "");
+		if (isString(html) && html.length > 0) {
+			const mapping = this.getOption("templateMapping", {});
+			if (isObject(mapping)) {
+				html = new Formatter(mapping, {}).format(html);
+			}
+			this.innerHTML = html;
+		}
+	}
+
+	return this;
 }
 
 /**
@@ -1090,49 +1080,49 @@ function initHtmlContent() {
  * @throws {TypeError} value is not an instance of
  */
 function initCSSStylesheet() {
-    if (!(this.shadowRoot instanceof ShadowRoot)) {
-        return this;
-    }
-
-    const styleSheet = this.constructor.getCSSStyleSheet();
-
-    if (styleSheet instanceof CSSStyleSheet) {
-        if (styleSheet.cssRules.length > 0) {
-            this.shadowRoot.adoptedStyleSheets = [styleSheet];
-        }
-    } else if (isArray(styleSheet)) {
-        const assign = [];
-        for (const s of styleSheet) {
-            if (isString(s)) {
-                const trimedStyleSheet = s.trim();
-                if (trimedStyleSheet !== "") {
-                    const style = document.createElement("style");
-                    style.innerHTML = trimedStyleSheet;
-                    this.shadowRoot.prepend(style);
-                }
-                continue;
-            }
-
-            validateInstance(s, CSSStyleSheet);
-
-            if (s.cssRules.length > 0) {
-                assign.push(s);
-            }
-        }
-
-        if (assign.length > 0) {
-            this.shadowRoot.adoptedStyleSheets = assign;
-        }
-    } else if (isString(styleSheet)) {
-        const trimedStyleSheet = styleSheet.trim();
-        if (trimedStyleSheet !== "") {
-            const style = document.createElement("style");
-            style.innerHTML = styleSheet;
-            this.shadowRoot.prepend(style);
-        }
-    }
-
-    return this;
+	if (!(this.shadowRoot instanceof ShadowRoot)) {
+		return this;
+	}
+
+	const styleSheet = this.constructor.getCSSStyleSheet();
+
+	if (styleSheet instanceof CSSStyleSheet) {
+		if (styleSheet.cssRules.length > 0) {
+			this.shadowRoot.adoptedStyleSheets = [styleSheet];
+		}
+	} else if (isArray(styleSheet)) {
+		const assign = [];
+		for (const s of styleSheet) {
+			if (isString(s)) {
+				const trimedStyleSheet = s.trim();
+				if (trimedStyleSheet !== "") {
+					const style = document.createElement("style");
+					style.innerHTML = trimedStyleSheet;
+					this.shadowRoot.prepend(style);
+				}
+				continue;
+			}
+
+			validateInstance(s, CSSStyleSheet);
+
+			if (s.cssRules.length > 0) {
+				assign.push(s);
+			}
+		}
+
+		if (assign.length > 0) {
+			this.shadowRoot.adoptedStyleSheets = assign;
+		}
+	} else if (isString(styleSheet)) {
+		const trimedStyleSheet = styleSheet.trim();
+		if (trimedStyleSheet !== "") {
+			const style = document.createElement("style");
+			style.innerHTML = styleSheet;
+			this.shadowRoot.prepend(style);
+		}
+	}
+
+	return this;
 }
 
 /**
@@ -1144,42 +1134,42 @@ function initCSSStylesheet() {
  * @since 1.8.0
  */
 function initShadowRoot() {
-    let template;
-    let html;
-
-    try {
-        template = findDocumentTemplate(this.constructor.getTag());
-    } catch (e) {
-        html = this.getOption("templates.main", "");
-        if (!isString(html) || html === undefined || html === "") {
-            throw new Error("html is not set.");
-        }
-    }
-
-    this.attachShadow({
-        mode: this.getOption("shadowMode", "open"),
-        delegatesFocus: this.getOption("delegatesFocus", true),
-    });
-
-    if (template instanceof Template) {
-        this.shadowRoot.appendChild(template.createDocumentFragment());
-        return this;
-    }
-
-    const mapping = this.getOption("templateMapping", {});
-    if (isObject(mapping)) {
-        const formatter = new Formatter(mapping);
-        if (this.getOption("templateFormatter.marker.open") !== null) {
-            formatter.setMarker(
-                this.getOption("templateFormatter.marker.open"),
-                this.getOption("templateFormatter.marker.close"),
-            );
-        }
-        html = formatter.format(html);
-    }
-
-    this.shadowRoot.innerHTML = html;
-    return this;
+	let template;
+	let html;
+
+	try {
+		template = findDocumentTemplate(this.constructor.getTag());
+	} catch (e) {
+		html = this.getOption("templates.main", "");
+		if (!isString(html) || html === undefined || html === "") {
+			throw new Error("html is not set.");
+		}
+	}
+
+	this.attachShadow({
+		mode: this.getOption("shadowMode", "open"),
+		delegatesFocus: this.getOption("delegatesFocus", true),
+	});
+
+	if (template instanceof Template) {
+		this.shadowRoot.appendChild(template.createDocumentFragment());
+		return this;
+	}
+
+	const mapping = this.getOption("templateMapping", {});
+	if (isObject(mapping)) {
+		const formatter = new Formatter(mapping);
+		if (this.getOption("templateFormatter.marker.open") !== null) {
+			formatter.setMarker(
+				this.getOption("templateFormatter.marker.open"),
+				this.getOption("templateFormatter.marker.close"),
+			);
+		}
+		html = formatter.format(html);
+	}
+
+	this.shadowRoot.innerHTML = html;
+	return this;
 }
 
 /**
@@ -1193,20 +1183,20 @@ function initShadowRoot() {
  * @throws {DOMException} Failed to execute 'define' on 'CustomElementRegistry': is not a valid custom element name
  */
 function registerCustomElement(element) {
-    validateFunction(element);
-    const customElements = getGlobalObject("customElements");
-    if (customElements === undefined) {
-        throw new Error("customElements is not supported.");
-    }
-
-    const tag = element?.getTag();
-    if (!isString(tag) || tag === "") {
-        throw new Error("tag is not set.");
-    }
-
-    if (customElements.get(tag) !== undefined) {
-        return;
-    }
-
-    customElements.define(tag, element);
+	validateFunction(element);
+	const customElements = getGlobalObject("customElements");
+	if (customElements === undefined) {
+		throw new Error("customElements is not supported.");
+	}
+
+	const tag = element?.getTag();
+	if (!isString(tag) || tag === "") {
+		throw new Error("tag is not set.");
+	}
+
+	if (customElements.get(tag) !== undefined) {
+		return;
+	}
+
+	customElements.define(tag, element);
 }
diff --git a/source/dom/error.mjs b/source/dom/error.mjs
index dd18dfd1a23c77b8b2a86ca7e2763cc0a90be2fd..157d0bafbbf5aea910b5cc6855acc92ff4d37dc7 100644
--- a/source/dom/error.mjs
+++ b/source/dom/error.mjs
@@ -12,11 +12,10 @@
  * SPDX-License-Identifier: AGPL-3.0
  */
 
-import {validateInstance, validateString} from "../types/validate.mjs";
-import {ATTRIBUTE_ERRORMESSAGE} from "./constants.mjs";
+import { validateInstance, validateString } from "../types/validate.mjs";
+import { ATTRIBUTE_ERRORMESSAGE } from "./constants.mjs";
 
-
-export {addErrorAttribute, removeErrorAttribute};
+export { addErrorAttribute, removeErrorAttribute };
 
 /**
  * This method can be used to add an error message to an element.
@@ -29,48 +28,45 @@ export {addErrorAttribute, removeErrorAttribute};
  * @return {HTMLElement}
  */
 function addErrorAttribute(element, message) {
-    validateInstance(element, HTMLElement);
-
-    if(message instanceof Error) {
-        message = message.message;
-    }
-
-    if(typeof message !== "string") {
-        if(typeof message === "object" && message !== null) {
-            if(typeof message.toString === "function") {
-                message = message.toString();
-            } else {
-                message = JSON.stringify(message);
-            }
-        } else {
-            message = String(message);
-        }
-    }
-
-    validateString(message);
-
-    if (!element.hasAttribute(ATTRIBUTE_ERRORMESSAGE)) {
-        element.setAttribute(ATTRIBUTE_ERRORMESSAGE, message);
-        return element;
-    }
-
-    const current = element.getAttribute(ATTRIBUTE_ERRORMESSAGE);
-    const list = current.split("::");
-
-    for(let i = 0; i < list.length; i++) {
-        if(list[i] === message) {
-            return element;
-        }
-    }
-
-    list.push(message);
-
-    element.setAttribute(
-        ATTRIBUTE_ERRORMESSAGE,
-        list.join("::")
-    );
-
-    return element;
+	validateInstance(element, HTMLElement);
+
+	if (message instanceof Error) {
+		message = message.message;
+	}
+
+	if (typeof message !== "string") {
+		if (typeof message === "object" && message !== null) {
+			if (typeof message.toString === "function") {
+				message = message.toString();
+			} else {
+				message = JSON.stringify(message);
+			}
+		} else {
+			message = String(message);
+		}
+	}
+
+	validateString(message);
+
+	if (!element.hasAttribute(ATTRIBUTE_ERRORMESSAGE)) {
+		element.setAttribute(ATTRIBUTE_ERRORMESSAGE, message);
+		return element;
+	}
+
+	const current = element.getAttribute(ATTRIBUTE_ERRORMESSAGE);
+	const list = current.split("::");
+
+	for (let i = 0; i < list.length; i++) {
+		if (list[i] === message) {
+			return element;
+		}
+	}
+
+	list.push(message);
+
+	element.setAttribute(ATTRIBUTE_ERRORMESSAGE, list.join("::"));
+
+	return element;
 }
 
 /**
@@ -84,23 +80,20 @@ function addErrorAttribute(element, message) {
  * @return {HTMLElement}
  */
 function removeErrorAttribute(element, message) {
-    validateInstance(element, HTMLElement);
-    validateString(message);
-
-    if (!element.hasAttribute(ATTRIBUTE_ERRORMESSAGE)) {
-        return element;
-    }
-
-    const current = element.getAttribute(ATTRIBUTE_ERRORMESSAGE);
-    const list = current.split("::");
-    const newList = list.filter(function (token) {
-        return token !== message;
-    });
-
-    element.setAttribute(
-        ATTRIBUTE_ERRORMESSAGE,
-        newList.join("::")
-    );
-
-    return element;
-}
\ No newline at end of file
+	validateInstance(element, HTMLElement);
+	validateString(message);
+
+	if (!element.hasAttribute(ATTRIBUTE_ERRORMESSAGE)) {
+		return element;
+	}
+
+	const current = element.getAttribute(ATTRIBUTE_ERRORMESSAGE);
+	const list = current.split("::");
+	const newList = list.filter(function (token) {
+		return token !== message;
+	});
+
+	element.setAttribute(ATTRIBUTE_ERRORMESSAGE, newList.join("::"));
+
+	return element;
+}
diff --git a/source/monster.mjs b/source/monster.mjs
index a287a0462c584e75644470d5dabe216770201f9f..a10c0e173faace8d0943a8de2a3892c40f3a79df 100644
--- a/source/monster.mjs
+++ b/source/monster.mjs
@@ -127,6 +127,7 @@ export * from "./constraints/isarray.mjs";
 export * from "./constraints/abstract.mjs";
 export * from "./constraints/valid.mjs";
 export * from "./dom/dimension.mjs";
+export * from "./dom/error.mjs";
 export * from "./dom/resource/link/stylesheet.mjs";
 export * from "./dom/resource/link.mjs";
 export * from "./dom/resource/script.mjs";