diff --git a/source/components/form/form.mjs b/source/components/form/form.mjs index a807bfd4c9aaf3b38e6fd18c193e7d3f8c19ab41..c75adad72ef47e3a3d84fbdd6967502d86d6e18d 100644 --- a/source/components/form/form.mjs +++ b/source/components/form/form.mjs @@ -12,576 +12,224 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { instanceSymbol } from "../../constants.mjs"; -import { internalSymbol } from "../../constants.mjs"; -import { Datasource } from "../../data/datasource.mjs"; -import { RestAPI } from "../../data/datasource/server/restapi.mjs"; -import { WebConnect } from "../../data/datasource/server/webconnect.mjs"; -import { WriteError } from "../../data/datasource/server/restapi/writeerror.mjs"; -import { LocalStorage } from "../../data/datasource/storage/localstorage.mjs"; -import { SessionStorage } from "../../data/datasource/storage/sessionstorage.mjs"; +import {instanceSymbol} from "../../constants.mjs"; +import {internalSymbol} from "../../constants.mjs"; +import {TokenList} from "../../types/tokenlist.mjs"; +import {DeadMansSwitch} from "../../util/deadmansswitch.mjs"; +import {DataSet} from "../datatable/dataset.mjs"; +//import { Datasource } from "../../data/datasource.mjs"; +import {RestAPI} from "../../data/datasource/server/restapi.mjs"; +import {WebConnect} from "../../data/datasource/server/webconnect.mjs"; +import {WriteError} from "../../data/datasource/server/restapi/writeerror.mjs"; +import {LocalStorage} from "../../data/datasource/storage/localstorage.mjs"; +import {SessionStorage} from "../../data/datasource/storage/sessionstorage.mjs"; import { - ATTRIBUTE_DISABLED, - ATTRIBUTE_ERRORMESSAGE, - ATTRIBUTE_PREFIX, - ATTRIBUTE_UPDATER_ATTRIBUTES, - ATTRIBUTE_UPDATER_INSERT, - ATTRIBUTE_UPDATER_REMOVE, - ATTRIBUTE_UPDATER_REPLACE, + ATTRIBUTE_DISABLED, + ATTRIBUTE_ERRORMESSAGE, + ATTRIBUTE_PREFIX, + ATTRIBUTE_UPDATER_ATTRIBUTES, + ATTRIBUTE_UPDATER_INSERT, + ATTRIBUTE_UPDATER_REMOVE, + ATTRIBUTE_UPDATER_REPLACE, } from "../../dom/constants.mjs"; import { - assembleMethodSymbol, - CustomElement, - registerCustomElement, - getSlottedElements, + assembleMethodSymbol, + CustomElement, + registerCustomElement, + getSlottedElements, } from "../../dom/customelement.mjs"; -import { addObjectWithUpdaterToElement } from "../../dom/updater.mjs"; -import { isFunction, isString } from "../../types/is.mjs"; -import { Observer } from "../../types/observer.mjs"; -import { ProxyObserver } from "../../types/proxyobserver.mjs"; -import { Processing } from "../../util/processing.mjs"; -import { MessageStateButton } from "./message-state-button.mjs"; +import {addObjectWithUpdaterToElement} from "../../dom/updater.mjs"; +import {findElementWithSelectorUpwards} from "../../dom/util.mjs"; +import {isFunction, isString} from "../../types/is.mjs"; +import {Observer} from "../../types/observer.mjs"; +import {ProxyObserver} from "../../types/proxyobserver.mjs"; +import {Processing} from "../../util/processing.mjs"; +import {datasourceLinkedElementSymbol, handleDataSourceChanges} from "../datatable/util.mjs"; +import {MessageStateButton} from "./message-state-button.mjs"; import { - ATTRIBUTE_FORM_DATASOURCE, - ATTRIBUTE_FORM_DATASOURCE_ARGUMENTS, + ATTRIBUTE_FORM_DATASOURCE, + ATTRIBUTE_FORM_DATASOURCE_ARGUMENTS, } from "./constants.mjs"; -import { StateButton } from "./state-button.mjs"; -import { FormStyleSheet } from "./stylesheet/form.mjs"; +import {StateButton} from "./state-button.mjs"; +import {FormStyleSheet} from "./stylesheet/form.mjs"; -export { Form }; +export {Form}; -/** - * @private - * @since 3.1.0 - * @type {string} - */ -const ATTRIBUTE_FORM_DATASOURCE_ACTION = `${ATTRIBUTE_PREFIX}datasource-action`; - -/** - * Form data is the internal representation of the form data - * - * @private - * @type {symbol} - * @since 1.7.0 - */ -const formDataSymbol = Symbol.for( - "@schukai/monster/components/form/form@@formdata", -); - -/** - * @private - * @type {symbol} - * @since 2.8.0 - */ -const formDataUpdaterSymbol = Symbol.for( - "@schukai/component-form/form@@formdata-updater-link", -); /** * @private * @type {symbol} - * @since 1.7.0 - */ -const formElementSymbol = Symbol.for( - "@schukai/component-form/form@@form-element", -); - -/** - * @private - * @type {symbol} - * @since 2.5.0 - */ -const registeredDatasourcesSymbol = Symbol.for( - "@schukai/component-form/form@@registered-datasources", -); - -/** - * @private - * @since 1.7.0 - * @type {string} - */ -const PROPERTY_VALIDATION_KEY = "__validation"; - -/** - * This CustomControl creates a form element with a variety of options. - * - * <img src="./images/form.png"> - * - * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library. - * - * You can create this control either by specifying the HTML tag `<monster-form />` directly in the HTML or using - * Javascript via the `document.createElement('monster-form');` method. - * - * ```html - * <monster-form></monster-form> - * ``` - * - * Or you can create this CustomControl directly in Javascript: - * - * ```js - * import {Form} from '@schukai/component-form/source/form.js'; - * document.createElement('monster-form'); - * ``` - * - * @startuml form.png - * skinparam monochrome true - * skinparam shadowing false - * HTMLElement <|-- CustomElement - * CustomElement <|-- Form - * @enduml - * - * @since 1.6.0 - * @copyright schukai GmbH - * @memberOf Monster.Components.Form - * @summary A configurable form control */ -class Form extends CustomElement { - /** - * @throws {Error} the options attribute does not contain a valid json definition. - * @since 1.7.0 - */ - constructor() { - super(); - this[formDataSymbol] = new ProxyObserver({}); - } - - /** - * This method is called by the `instanceof` operator. - * @returns {symbol} - * @since 2.1.0 - */ - static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/components/form/form"); - } - - /** - * 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 {Datasource} datasource data source - * @property {Object} reportValidity - * @property {string} reportValidity.selector which element should be used to report the validity - * @property {function} reportValidity.errorHandler function to handle the error - * @property {Object} classes - * @property {string} classes.button class for the form - */ - get defaults() { - return Object.assign( - {}, - super.defaults, - { - templates: { - main: getTemplate(), - }, - datasource: undefined, - reportValidity: { - selector: "input,select,textarea", - errorHandler: undefined, - }, - classes: { - form: "", - }, - }, - initOptionsFromArguments.call(this), - ); - } - - /** - * Called every time the element is inserted into the DOM. Useful for running setup code, such as - * fetching resources or rendering. Generally, you should try to delay work until this time. - * - * @return {void} - */ - connectedCallback() { - super["connectedCallback"](); - } - - /** - * The refresh method is called to update the control after a change with fresh data. - * - * Therefore, the data source is called again and the data is updated. - * - * If you have updated the data source with `setOption('datasource',datasource), you must call this method. - * - * @return {Form} - * @throws {Error} undefined datasource - */ - refresh() { - try { - this.setAttribute(ATTRIBUTE_DISABLED, ""); - const datasource = this.getOption("datasource"); - - if (!(datasource instanceof Datasource)) { - throw new Error("undefined datasource"); - } - - return datasource - .read() - .then(() => { - this[formDataSymbol].setSubject(datasource.get()); - }) - .then(() => { - new Processing(() => { - this.removeAttribute(ATTRIBUTE_DISABLED); - }).run(); - }) - .catch((e) => { - this.removeAttribute(ATTRIBUTE_DISABLED); - this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString()); - }); - } catch (e) { - this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString()); - this.removeAttribute(ATTRIBUTE_DISABLED); - throw e; - } - } - - /** - * - * @return {Monster.Components.Form.Form} - */ - [assembleMethodSymbol]() { - super[assembleMethodSymbol](); - - initControlReferences.call(this); - initDatasource.call(this); - initUpdater.call(this); - initObserver.call(this); - return this; - } - - /** - * - * @return {*} - */ - getValues() { - return this[formDataSymbol].getSubject(); - } - - /** - * - * @return {string} - */ - static getTag() { - return "monster-form"; - } - - /** - * - * @return {CSSStyleSheet} - */ - static getCSSStyleSheet() { - return [FormStyleSheet]; - } - - static [registeredDatasourcesSymbol] = new Map([ - ["restapi", RestAPI], - ["localstorage", LocalStorage], - ["sessionstorage", SessionStorage], - ["webconnect", WebConnect], - ]); - - /** - * Register a new datasource - * - * @param {string} name - * @param {Monster.Data.Datasource} datasource - */ - static registerDatasource(name, datasource) { - Form[registeredDatasourcesSymbol].set(name, datasource); - } - - /** - * Unregister a registered datasource - * - * @param {string} name - */ - static unregisterDatasource(name) { - Form[registeredDatasourcesSymbol].delete(name); - } - - /** - * Get registered data sources - * - * @return {Map} - */ - static getDatasources() { - return Form[registeredDatasourcesSymbol]; - } - - /** - * Run reportValidation on all child html form controls. - * - * @since 2.10.0 - * @returns {boolean} - */ - reportValidity() { - let valid = true; - - const selector = this.getOption("reportValidity.selector"); - const nodes = getSlottedElements.call(this, selector); - nodes.forEach((node) => { - if (typeof node.reportValidity === "function") { - if (node.reportValidity() === false) { - valid = false; - } - } - }); - - return valid; - } -} +const debounceCallbackSymbol = Symbol("timerCallback"); + +class Form extends DataSet { + + /** + * + * @returns {{shadowMode: string, templates: {main: *}, display: string, disabled: boolean, delegatesFocus: boolean, templateMapping: {}} & {templates: {main: string}, classes: {form: string}}} + */ + get defaults() { + const obj = Object.assign( + {}, + super.defaults, + { + templates: { + main: getTemplate(), + }, + + classes: { + form: "", + }, + + writeBack: { + events: ["change", "input", "keyup"] + }, + + reportValidity: { + selector: "input,select,textarea", + } + + } + ); + + obj['features']['mutationObserver'] = false; + obj['features']['writeBack'] = true; + + return obj; + + } + + + /** + * + * @return {string} + */ + static getTag() { + return "monster-form"; + } + + /** + * @return {CSSStyleSheet[]} + */ + static getCSSStyleSheet() { + return [FormStyleSheet]; + } + + /** + * + */ + [assembleMethodSymbol]() { + super[assembleMethodSymbol](); + + initControlReferences.call(this); + initEventHandler.call(this); + initDataSourceHandler.call(this); + + } + + /** + * This method is called when the component is created. + * @since 3.70.0 + * @returns {DataSet} + */ + refresh() { + this.write(); + super.refresh(); + return this; + } + + /** + * Run reportValidation on all child html form controls. + * + * @since 2.10.0 + * @returns {boolean} + */ + reportValidity() { + let valid = true; + + const selector = this.getOption("reportValidity.selector"); + const nodes = getSlottedElements.call(this, selector); + + nodes.forEach((node) => { + if (typeof node.reportValidity === "function") { + if (node.reportValidity() === false) { + valid = false; + } + } + }); + + return valid; + } -/** - * @private - */ -function initUpdater() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - const slots = this.shadowRoot.querySelectorAll("slot"); - for (const [, slot] of Object.entries(slots)) { - for (const [, node] of Object.entries(slot.assignedNodes())) { - if (!(node instanceof HTMLElement)) { - continue; - } - - const query = `[${ATTRIBUTE_UPDATER_ATTRIBUTES}],[${ATTRIBUTE_UPDATER_REPLACE}],[${ATTRIBUTE_UPDATER_REMOVE}],[${ATTRIBUTE_UPDATER_INSERT}]`; - const controls = node.querySelectorAll(query); - - const list = new Set([...controls]); - - if (node.matches(query)) { - list.add(node); - } - - if (list.size === 0) { - continue; - } - - addObjectWithUpdaterToElement.call( - node, - list, - formDataUpdaterSymbol, - this[formDataSymbol], - ); - } - } } -/** - * @private - */ -function initDatasource() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - const slots = this.shadowRoot.querySelectorAll("slot"); - for (const [, slot] of Object.entries(slots)) { - for (const [, node] of Object.entries(slot.assignedNodes())) { - if (!(node instanceof HTMLElement)) { - continue; - } - - const query = `[${ATTRIBUTE_FORM_DATASOURCE_ACTION}=write]`; - const controls = node.querySelectorAll(query); - - const list = new Set([...controls]); - - if (node.matches(query)) { - list.add(node); - } - - if (list.size === 0) { - continue; - } - - initWriteActions.call(this, list); - } - } -} +function initDataSourceHandler() { + if (!this[datasourceLinkedElementSymbol]) { + return; + } +console.log(this[datasourceLinkedElementSymbol]); + this[datasourceLinkedElementSymbol].setOption("write.responseCallback", (response) => { + console.log("response!!!", response); + }) -/** - * @private - * @param elements - */ -function initWriteActions(elements) { - elements.forEach((element) => { - if (element instanceof HTMLElement) { - element.addEventListener("click", () => { - runWriteCallback.call(this, element); - }); - - const g = element?.getOption; - if (!isFunction(g)) { - return; - } - - const s = element?.setOption; - if (!isFunction(s)) { - return; - } - - const fn = element.getOption("actions.click"); - - if (!isFunction(fn)) { - return; - } - - // disable console.log of standard click event - element.setOption("actions.click", function () { - // do nothing! - }); - } - }); -} - -function runWriteCallback(button) { - if (typeof this.reportValidity === "function") { - if (this.reportValidity() === false) { - if ( - button instanceof StateButton || - button instanceof MessageStateButton - ) { - button.setState("failed"); - } - return; - } - } - - const datasource = this.getOption("datasource"); - if (!(datasource instanceof Datasource)) { - return; - } - - if (button instanceof StateButton || button instanceof MessageStateButton) { - button.setState("activity"); - } - - //const data = form?.[formDataSymbol]?.getRealSubject(); - const writePromise = datasource - .set(this[formDataSymbol].getRealSubject()) - .write(); - if (!(writePromise instanceof Promise)) { - throw new Error("datasource.write() must return a promise"); - } - - writePromise - .then((r) => { - if ( - button instanceof StateButton || - button instanceof MessageStateButton - ) { - button.setState("successful"); - } - this[formDataSymbol].getSubject()[PROPERTY_VALIDATION_KEY] = {}; - }) - .catch((e) => { - if (e instanceof WriteError) { - this[formDataSymbol].getSubject()[PROPERTY_VALIDATION_KEY] = - e.getValidation(); - } - - if ( - button instanceof StateButton || - button instanceof MessageStateButton - ) { - button.setState("failed"); - } - - if (button instanceof MessageStateButton) { - button.setMessage(e.message); - button.showMessage(); - } - }); -} - -/** - * This attribute can be used to pass a URL to this select. - * - * ``` - * <monster-form data-monster-datasource="restapi:....."></monster-form> - * ``` - * - * @private - * @return {object} - */ -function initOptionsFromArguments() { - const options = {}; - - const datasource = this.getAttribute(ATTRIBUTE_FORM_DATASOURCE); - if (isString(datasource)) { - for (const [key, classObject] of Form.getDatasources()) { - if (datasource === key) { - let args = this.getAttribute(ATTRIBUTE_FORM_DATASOURCE_ARGUMENTS); - - try { - args = JSON.parse(args); - } catch (e) { - this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString()); - continue; - } - - try { - options["datasource"] = new classObject(args); - } catch (e) { - this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString()); - continue; - } - - break; - } - - if (options["datasource"] instanceof Datasource) { - break; - } - } - } - - return options; } /** * @private - * @this Form + * @returns {initEventHandler} */ -function initObserver() { - const self = this; - - let lastDatasource = null; - self[internalSymbol].attachObserver( - new Observer(function () { - const datasource = self.getOption("datasource"); - if (datasource !== lastDatasource) { - new Processing(100, function () { - self.refresh(); - }).run(); - } - - lastDatasource = datasource; - }), - ); +function initEventHandler() { + + if (this.getOption("features.writeBack") === true) { + const events = this.getOption("writeBack.events"); + for (const event of events) { + + this.addEventListener(event, (e) => { + + if (!this.reportValidity()) { + + this.classList.add("invalid"); + setTimeout(() => { + this.classList.remove("invalid"); + }, 1000) + + return; + } + + if (this[debounceCallbackSymbol] instanceof DeadMansSwitch) { + try { + this[debounceCallbackSymbol].touch(); + return; + } catch (e) { + if (e.message !== "has already run") { + throw e; + } + delete this[debounceCallbackSymbol]; + } + } + + this[debounceCallbackSymbol] = new DeadMansSwitch(200, () => { + setTimeout(() => { + this.write(); + }, 0); + }); + + }); + } + } + + return this; } /** * @private - * @return {Monster.Components.Form.Form} + * @return {FilterButton} */ function initControlReferences() { - if (!this.shadowRoot) { - throw new Error("no shadow-root is defined"); - } - - this[formElementSymbol] = this.shadowRoot.querySelector( - "[data-monster-role=form]", - ); - return this; + if (!this.shadowRoot) { + throw new Error("no shadow-root is defined"); + } + return this; } /** @@ -589,8 +237,8 @@ function initControlReferences() { * @return {string} */ function getTemplate() { - // language=HTML - return ` + // language=HTML + return ` <div data-monster-role="control" part="control"> <form data-monster-attributes="disabled path:disabled | if:true, class path:classes.form" data-monster-role="form"