diff --git a/application/source/dom/customcontrol.mjs b/application/source/dom/customcontrol.mjs index 7cf06490f8fa4d2d5b6c69596de71e269b27f058..d884ef740947b0c7be40aabd6c21118c939e245c 100644 --- a/application/source/dom/customcontrol.mjs +++ b/application/source/dom/customcontrol.mjs @@ -5,11 +5,12 @@ * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html */ -import { extend } from "../data/extend.mjs"; -import { ATTRIBUTE_VALUE } from "./constants.mjs"; -import { CustomElement, attributeObserverSymbol } from "./customelement.mjs"; -import { instanceSymbol } from "../constants.mjs"; -export { CustomControl }; +import {extend} from "../data/extend.mjs"; +import {ATTRIBUTE_VALUE} from "./constants.mjs"; +import {CustomElement, attributeObserverSymbol} from "./customelement.mjs"; +import {instanceSymbol} from "../constants.mjs"; + +export {CustomControl}; /** * @private @@ -41,6 +42,7 @@ const attachedInternalSymbol = Symbol("attachedInternal"); * @see {@link https://www.npmjs.com/package/element-internals-polyfill} * @see {@link https://github.com/WICG/webcomponents} * @see {@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements} + * @see {@link https://html.spec.whatwg.org/dev/custom-elements.html#custom-element-reactions} * @license AGPLv3 * @since 1.14.0 * @copyright schukai GmbH @@ -74,7 +76,7 @@ class CustomControl extends CustomElement { * @since 2.1.0 */ static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/dom/custom-control"); + return Symbol.for("@schukai/monster/dom/custom-control@@instance"); } /** @@ -84,20 +86,18 @@ class CustomControl extends CustomElement { * @since 1.15.0 */ static get observedAttributes() { - const list = super.observedAttributes; - list.push(ATTRIBUTE_VALUE); - return list; + return super.observedAttributes; } /** + * Adding a static formAssociated property, with a true value, makes an autonomous custom element a form-associated custom element. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals} + * @see {@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example} * @since 1.14.0 * @return {boolean} */ - static get formAssociated() { - return true; - } + static formAssociated = true /** * Derived classes can override and extend this method as follows. @@ -298,6 +298,46 @@ class CustomControl extends CustomElement { reportValidity() { return getInternal.call(this)?.reportValidity(); } + + /** + * @param {string} form + */ + formAssociatedCallback(form) { + if (form) { + if(form.id) { + this.setAttribute("form", form.id); + } + } else { + this.removeAttribute("form"); + } + } + + /** + * @param {string} disabled + */ + formDisabledCallback(disabled) { + if (disabled) { + this.setAttribute("disabled", ""); + } else { + this.removeAttribute("disabled"); + } + } + + /** + * @param {string} state + * @param {string} mode + */ + formStateRestoreCallback(state, mode) { + + } + + /** + * + */ + formResetCallback() { + this.value = ""; + } + } /** @@ -313,7 +353,7 @@ function getInternal() { throw new Error("ElementInternals is not supported and a polyfill is necessary"); } - return this[attachedInternalSymbol]; + return self[attachedInternalSymbol]; } /** diff --git a/application/source/dom/customelement.mjs b/application/source/dom/customelement.mjs index 8ff4866e51da8a8d15526911671a715f357fca29..9ce57bfa812b77d25df6591799a5af41fbde428d 100644 --- a/application/source/dom/customelement.mjs +++ b/application/source/dom/customelement.mjs @@ -31,6 +31,7 @@ import {instanceSymbol} from "../constants.mjs"; import {getDocumentTranslations, Translations} 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"; export { CustomElement, @@ -220,7 +221,7 @@ class CustomElement extends HTMLElement { * @since 2.1.0 */ static get [instanceSymbol]() { - return Symbol.for("@schukai/monster/dom/custom-element"); + return Symbol.for("@schukai/monster/dom/custom-element@@instance"); } /** @@ -232,7 +233,7 @@ class CustomElement extends HTMLElement { * @since 1.15.0 */ static get observedAttributes() { - return [ATTRIBUTE_OPTIONS, ATTRIBUTE_DISABLED]; + return []; } /** @@ -309,7 +310,7 @@ class CustomElement extends HTMLElement { */ get defaults() { return { - ATTRIBUTE_DISABLED: this.getAttribute(ATTRIBUTE_DISABLED), + disabled: false, shadowMode: "open", delegatesFocus: true, templates: { @@ -605,8 +606,11 @@ class CustomElement extends HTMLElement { attributeChangedCallback(attrName, oldVal, newVal) { const self = this; - const callback = self[attributeObserverSymbol]?.[attrName]; + if (attrName.startsWith("data-monster-option-")) { + setOptionFromAttribute(self, attrName, this[internalSymbol].getSubject()["options"]) + } + const callback = self[attributeObserverSymbol]?.[attrName]; if (isFunction(callback)) { try { callback.call(self, newVal, oldVal); diff --git a/application/source/dom/util/extract-keys.mjs b/application/source/dom/util/extract-keys.mjs new file mode 100644 index 0000000000000000000000000000000000000000..d2a28d6dc8beb05f7131b856d66fe72ef9e206e2 --- /dev/null +++ b/application/source/dom/util/extract-keys.mjs @@ -0,0 +1,39 @@ +/** + * Copyright schukai GmbH and contributors 2023. All Rights Reserved. + * Node module: @schukai/monster + * This file is licensed under the AGPLv3 License. + * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html + */ + +export {extractKeys} + +/** + * Extracts the keys from the given object and returns a map with the keys and values. + * + * @private + * @param {object} obj + * @param {string} keyPrefix + * @param {string} keySeparator + * @param {string} valueSeparator + * @returns {Map<any, any>} + */ +function extractKeys(obj, keyPrefix = '', keySeparator = '-', valueSeparator = '.') { + const resultMap = new Map(); + + function helper(currentObj, currentKeyPrefix, currentValuePrefix) { + for (const key in currentObj) { + if (typeof currentObj[key] === 'object' && !Array.isArray(currentObj[key])) { + const newKeyPrefix = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase(); + const newValuePrefix = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key; + helper(currentObj[key], newKeyPrefix, newValuePrefix); + } else { + const finalKey = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase(); + const finalValue = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key; + resultMap.set(finalKey, finalValue); + } + } + } + + helper(obj, keyPrefix, keyPrefix); + return resultMap; +} \ No newline at end of file diff --git a/application/source/dom/util/init-options-from-attributes.mjs b/application/source/dom/util/init-options-from-attributes.mjs index 85664dee76277157bf71551935960d35a3ffb7b1..6f4c9d6419c36a36ec8618a9fc118c275d6c3e67 100644 --- a/application/source/dom/util/init-options-from-attributes.mjs +++ b/application/source/dom/util/init-options-from-attributes.mjs @@ -8,6 +8,7 @@ import {Pathfinder} from '../../data/pathfinder.mjs'; import {isFunction} from '../../types/is.mjs'; import {attributeObserverSymbol} from "../customelement.mjs"; +import {extractKeys} from "./extract-keys.mjs"; export {initOptionsFromAttributes}; @@ -75,63 +76,10 @@ function initOptionsFromAttributes(element, options, mapping = {}, prefix = 'dat } finder.setVia(optionName, value); - - // if element has an attribute observer, then register the attribute observer - if (element?.[attributeObserverSymbol]) { - element[attributeObserverSymbol][name] = (newValue, oldValue) => { - - if (newValue === oldValue) return; - - let changedValue = newValue; - - if (typeOfOptionValue === 'boolean') { - changedValue = changedValue === 'true'; - } else if (typeOfOptionValue === 'number') { - changedValue = Number(changedValue); - } else if (typeOfOptionValue === 'string') { - changedValue = String(changedValue); - } else if (typeOfOptionValue === 'object') { - changedValue = JSON.parse(changedValue); - } - - finder.setVia(optionName, changedValue); - } - } - - } }) return options; } -/** - * Extracts the keys from the given object and returns a map with the keys and values. - * - * @private - * @param {object} obj - * @param {string} keyPrefix - * @param {string} keySeparator - * @param {string} valueSeparator - * @returns {Map<any, any>} - */ -function extractKeys(obj, keyPrefix = '', keySeparator = '-', valueSeparator = '.') { - const resultMap = new Map(); - function helper(currentObj, currentKeyPrefix, currentValuePrefix) { - for (const key in currentObj) { - if (typeof currentObj[key] === 'object' && !Array.isArray(currentObj[key])) { - const newKeyPrefix = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase(); - const newValuePrefix = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key; - helper(currentObj[key], newKeyPrefix, newValuePrefix); - } else { - const finalKey = currentKeyPrefix ? currentKeyPrefix + keySeparator + key.toLowerCase() : key.toLowerCase(); - const finalValue = currentValuePrefix ? currentValuePrefix + valueSeparator + key : key; - resultMap.set(finalKey, finalValue); - } - } - } - - helper(obj, keyPrefix, keyPrefix); - return resultMap; -} diff --git a/application/source/dom/util/set-option-from-attribute.mjs b/application/source/dom/util/set-option-from-attribute.mjs new file mode 100644 index 0000000000000000000000000000000000000000..fe39a793697d41f8f528639583e6cba9ed28c32c --- /dev/null +++ b/application/source/dom/util/set-option-from-attribute.mjs @@ -0,0 +1,83 @@ +/** + * Copyright schukai GmbH and contributors 2023. All Rights Reserved. + * Node module: @schukai/monster + * This file is licensed under the AGPLv3 License. + * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html + */ + +import {Pathfinder} from '../../data/pathfinder.mjs'; +import {isFunction} from '../../types/is.mjs'; +import {attributeObserverSymbol} from "../customelement.mjs"; +import {extractKeys} from "./extract-keys.mjs"; + +export {setOptionFromAttribute}; + +/** + * Set the given options object based on the attributes of the current DOM element. + * The function looks for attributes with the prefix 'data-monster-option-', and maps them to + * properties in the options object. It replaces the dashes with dots to form the property path. + * For example, the attribute 'data-monster-option-url' maps to the 'url' property in the options object. + * + * With the mapping parameter, the attribute value can be mapped to a different value. + * For example, the attribute 'data-monster-option-foo' maps to the 'bar' property in the options object. + * + * The mapping object would look like this: + * { + * 'foo': (value) => value + 'bar' + * // the value of the attribute 'data-monster-option-foo' is appended with 'bar' + * // and assigned to the 'bar' property in the options object. + * // e.g. <div data-monster-option-foo="foo"></div> + * 'bar.baz': (value) => value + 'bar' + * // the value of the attribute 'data-monster-option-bar-baz' is appended with 'bar' + * // and assigned to the 'bar.baz' property in the options object. + * // e.g. <div data-monster-option-bar-baz="foo"></div> + * } + * + * @since 3.45.0 + * @param {HTMLElement} element - The DOM element to be used as the source of the attributes. + * @param {Object} name - The attribute object to be used as the source of the attributes. + * @param {Object} options - The options object to be initialized. + * @param {Object} mapping - A mapping between the attribute value and the property value. + * @param {string} prefix - The prefix of the attributes to be considered. + * @returns {Object} - The initialized options object. + * @this HTMLElement - The context of the DOM element. + */ +function setOptionFromAttribute(element, name, options, mapping = {}, prefix = 'data-monster-option-') { + if (!(element instanceof HTMLElement)) return options; + if (!element.hasAttributes()) return options; + + const keyMap = extractKeys(options); + const finder = new Pathfinder(options); + + // check if the attribute name is a valid option. + // the mapping between the attribute is simple. The dash is replaced by a dot. + // e.g. data-monster-url => url + const optionName = keyMap.get(name.substring(prefix.length).toLowerCase()); + if (!finder.exists(optionName)) return; + + if (!element.hasAttribute(name)) { + return options; + } + + let value = element.getAttribute(name); + if (mapping.hasOwnProperty(optionName) && isFunction(mapping[optionName])) { + value = mapping[optionName](value); + } + + const typeOfOptionValue = typeof finder.getVia(optionName); + if (typeOfOptionValue === 'boolean') { + value = value === 'true'; + } else if (typeOfOptionValue === 'number') { + value = Number(value); + } else if (typeOfOptionValue === 'string') { + value = String(value); + } else if (typeOfOptionValue === 'object') { + value = JSON.parse(value); + } + + finder.setVia(optionName, value); + + return options; +} + + diff --git a/development/playground/dom-arguments/index.html b/development/playground/dom-arguments/index.html index 538ebea6df6c2ddd777c2c8eaa1359b5f4aa05e1..0f02359c4e18aa7f994549cf597204241eaf6506 100644 --- a/development/playground/dom-arguments/index.html +++ b/development/playground/dom-arguments/index.html @@ -22,8 +22,9 @@ } } </script> - -<monster-1>Monster1</monster-1> +<form> +<monster-1 data-monster-option-a-b-c="114">Monster1</monster-1> +</form> </main> </body> </html> diff --git a/development/playground/dom-arguments/main.mjs b/development/playground/dom-arguments/main.mjs index 70268e2ea2ae04adb513c7ad116fe2845a1a1119..05c646b3284f0323de408c0347c20b762e9e1779 100644 --- a/development/playground/dom-arguments/main.mjs +++ b/development/playground/dom-arguments/main.mjs @@ -2,7 +2,7 @@ // import {Updater} from '../../../application/source/dom/updater.mjs'; import { attributeObserverSymbol, - CustomElement, + CustomElement,CustomControl, registerCustomElement } from '../../../application/source/monster.mjs'; import {domReady} from '../../../application/source/dom/ready.mjs'; @@ -12,7 +12,7 @@ import {domReady} from '../../../application/source/dom/ready.mjs'; const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol"); -class Monster1 extends CustomElement { +class Monster1 extends CustomControl { constructor() { super();