Skip to content
Snippets Groups Projects
Verified Commit b835d2da authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: complete change of form control to a derivation of dataset #216

parent fb796880
No related branches found
No related tags found
No related merge requests found
...@@ -14,7 +14,10 @@ ...@@ -14,7 +14,10 @@
import {instanceSymbol} from "../../constants.mjs"; import {instanceSymbol} from "../../constants.mjs";
import {internalSymbol} from "../../constants.mjs"; import {internalSymbol} from "../../constants.mjs";
import { Datasource } from "../../data/datasource.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 {RestAPI} from "../../data/datasource/server/restapi.mjs";
import {WebConnect} from "../../data/datasource/server/webconnect.mjs"; import {WebConnect} from "../../data/datasource/server/webconnect.mjs";
import {WriteError} from "../../data/datasource/server/restapi/writeerror.mjs"; import {WriteError} from "../../data/datasource/server/restapi/writeerror.mjs";
...@@ -36,10 +39,12 @@ import { ...@@ -36,10 +39,12 @@ import {
getSlottedElements, getSlottedElements,
} from "../../dom/customelement.mjs"; } from "../../dom/customelement.mjs";
import {addObjectWithUpdaterToElement} from "../../dom/updater.mjs"; import {addObjectWithUpdaterToElement} from "../../dom/updater.mjs";
import {findElementWithSelectorUpwards} from "../../dom/util.mjs";
import {isFunction, isString} from "../../types/is.mjs"; import {isFunction, isString} from "../../types/is.mjs";
import {Observer} from "../../types/observer.mjs"; import {Observer} from "../../types/observer.mjs";
import {ProxyObserver} from "../../types/proxyobserver.mjs"; import {ProxyObserver} from "../../types/proxyobserver.mjs";
import {Processing} from "../../util/processing.mjs"; import {Processing} from "../../util/processing.mjs";
import {datasourceLinkedElementSymbol, handleDataSourceChanges} from "../datatable/util.mjs";
import {MessageStateButton} from "./message-state-button.mjs"; import {MessageStateButton} from "./message-state-button.mjs";
import { import {
ATTRIBUTE_FORM_DATASOURCE, ATTRIBUTE_FORM_DATASOURCE,
...@@ -50,217 +55,50 @@ import { FormStyleSheet } from "./stylesheet/form.mjs"; ...@@ -50,217 +55,50 @@ 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 * @private
* @type {symbol} * @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() { const debounceCallbackSymbol = Symbol("timerCallback");
super();
this[formDataSymbol] = new ProxyObserver({});
}
/** class Form extends DataSet {
* 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 * @returns {{shadowMode: string, templates: {main: *}, display: string, disabled: boolean, delegatesFocus: boolean, templateMapping: {}} & {templates: {main: string}, classes: {form: string}}}
* @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() { get defaults() {
return Object.assign( const obj = Object.assign(
{}, {},
super.defaults, super.defaults,
{ {
templates: { templates: {
main: getTemplate(), main: getTemplate(),
}, },
datasource: undefined,
reportValidity: {
selector: "input,select,textarea",
errorHandler: undefined,
},
classes: { classes: {
form: "", form: "",
}, },
writeBack: {
events: ["change", "input", "keyup"]
}, },
initOptionsFromArguments.call(this),
);
}
/** reportValidity: {
* Called every time the element is inserted into the DOM. Useful for running setup code, such as selector: "input,select,textarea",
* 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 obj['features']['mutationObserver'] = false;
.read() obj['features']['writeBack'] = true;
.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 obj;
*
* @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();
}
/** /**
* *
...@@ -271,46 +109,33 @@ class Form extends CustomElement { ...@@ -271,46 +109,33 @@ class Form extends CustomElement {
} }
/** /**
* * @return {CSSStyleSheet[]}
* @return {CSSStyleSheet}
*/ */
static getCSSStyleSheet() { static getCSSStyleSheet() {
return [FormStyleSheet]; 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) { [assembleMethodSymbol]() {
Form[registeredDatasourcesSymbol].set(name, datasource); super[assembleMethodSymbol]();
}
initControlReferences.call(this);
initEventHandler.call(this);
initDataSourceHandler.call(this);
/**
* Unregister a registered datasource
*
* @param {string} name
*/
static unregisterDatasource(name) {
Form[registeredDatasourcesSymbol].delete(name);
} }
/** /**
* Get registered data sources * This method is called when the component is created.
* * @since 3.70.0
* @return {Map} * @returns {DataSet}
*/ */
static getDatasources() { refresh() {
return Form[registeredDatasourcesSymbol]; this.write();
super.refresh();
return this;
} }
/** /**
...@@ -324,6 +149,7 @@ class Form extends CustomElement { ...@@ -324,6 +149,7 @@ class Form extends CustomElement {
const selector = this.getOption("reportValidity.selector"); const selector = this.getOption("reportValidity.selector");
const nodes = getSlottedElements.call(this, selector); const nodes = getSlottedElements.call(this, selector);
nodes.forEach((node) => { nodes.forEach((node) => {
if (typeof node.reportValidity === "function") { if (typeof node.reportValidity === "function") {
if (node.reportValidity() === false) { if (node.reportValidity() === false) {
...@@ -334,253 +160,75 @@ class Form extends CustomElement { ...@@ -334,253 +160,75 @@ class Form extends CustomElement {
return valid; 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) { function initDataSourceHandler() {
continue; if (!this[datasourceLinkedElementSymbol]) {
return;
} }
console.log(this[datasourceLinkedElementSymbol]);
this[datasourceLinkedElementSymbol].setOption("write.responseCallback", (response) => {
console.log("response!!!", response);
})
addObjectWithUpdaterToElement.call(
node,
list,
formDataUpdaterSymbol,
this[formDataSymbol],
);
}
}
} }
/** /**
* @private * @private
* @returns {initEventHandler}
*/ */
function initDatasource() { function initEventHandler() {
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]`; if (this.getOption("features.writeBack") === true) {
const controls = node.querySelectorAll(query); const events = this.getOption("writeBack.events");
for (const event of events) {
const list = new Set([...controls]); this.addEventListener(event, (e) => {
if (node.matches(query)) { if (!this.reportValidity()) {
list.add(node);
}
if (list.size === 0) {
continue;
}
initWriteActions.call(this, list);
}
}
}
/**
* @private
* @param elements
*/
function initWriteActions(elements) {
elements.forEach((element) => {
if (element instanceof HTMLElement) {
element.addEventListener("click", () => {
runWriteCallback.call(this, element);
});
const g = element?.getOption; this.classList.add("invalid");
if (!isFunction(g)) { setTimeout(() => {
return; this.classList.remove("invalid");
} }, 1000)
const s = element?.setOption;
if (!isFunction(s)) {
return;
}
const fn = element.getOption("actions.click");
if (!isFunction(fn)) {
return; return;
} }
// disable console.log of standard click event if (this[debounceCallbackSymbol] instanceof DeadMansSwitch) {
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 { try {
args = JSON.parse(args); this[debounceCallbackSymbol].touch();
return;
} catch (e) { } catch (e) {
this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString()); if (e.message !== "has already run") {
continue; throw e;
} }
delete this[debounceCallbackSymbol];
try {
options["datasource"] = new classObject(args);
} catch (e) {
this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString());
continue;
} }
break;
} }
if (options["datasource"] instanceof Datasource) { this[debounceCallbackSymbol] = new DeadMansSwitch(200, () => {
break; setTimeout(() => {
} this.write();
} }, 0);
} });
return options; });
} }
/**
* @private
* @this Form
*/
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; return this;
}),
);
} }
/** /**
* @private * @private
* @return {Monster.Components.Form.Form} * @return {FilterButton}
*/ */
function initControlReferences() { function initControlReferences() {
if (!this.shadowRoot) { if (!this.shadowRoot) {
throw new Error("no shadow-root is defined"); throw new Error("no shadow-root is defined");
} }
this[formElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=form]",
);
return this; return this;
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment