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

refactor: attribute and form improvments

parent b1cefacc
No related branches found
No related tags found
No related merge requests found
...@@ -9,6 +9,7 @@ import { extend } from "../data/extend.mjs"; ...@@ -9,6 +9,7 @@ import { extend } from "../data/extend.mjs";
import {ATTRIBUTE_VALUE} from "./constants.mjs"; import {ATTRIBUTE_VALUE} from "./constants.mjs";
import {CustomElement, attributeObserverSymbol} from "./customelement.mjs"; import {CustomElement, attributeObserverSymbol} from "./customelement.mjs";
import {instanceSymbol} from "../constants.mjs"; import {instanceSymbol} from "../constants.mjs";
export {CustomControl}; export {CustomControl};
/** /**
...@@ -41,6 +42,7 @@ const attachedInternalSymbol = Symbol("attachedInternal"); ...@@ -41,6 +42,7 @@ const attachedInternalSymbol = Symbol("attachedInternal");
* @see {@link https://www.npmjs.com/package/element-internals-polyfill} * @see {@link https://www.npmjs.com/package/element-internals-polyfill}
* @see {@link https://github.com/WICG/webcomponents} * @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/multipage/custom-elements.html#custom-elements}
* @see {@link https://html.spec.whatwg.org/dev/custom-elements.html#custom-element-reactions}
* @license AGPLv3 * @license AGPLv3
* @since 1.14.0 * @since 1.14.0
* @copyright schukai GmbH * @copyright schukai GmbH
...@@ -74,7 +76,7 @@ class CustomControl extends CustomElement { ...@@ -74,7 +76,7 @@ class CustomControl extends CustomElement {
* @since 2.1.0 * @since 2.1.0
*/ */
static get [instanceSymbol]() { 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 { ...@@ -84,20 +86,18 @@ class CustomControl extends CustomElement {
* @since 1.15.0 * @since 1.15.0
*/ */
static get observedAttributes() { static get observedAttributes() {
const list = super.observedAttributes; return super.observedAttributes;
list.push(ATTRIBUTE_VALUE);
return list;
} }
/** /**
* 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://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 * @since 1.14.0
* @return {boolean} * @return {boolean}
*/ */
static get formAssociated() { static formAssociated = true
return true;
}
/** /**
* Derived classes can override and extend this method as follows. * Derived classes can override and extend this method as follows.
...@@ -298,6 +298,46 @@ class CustomControl extends CustomElement { ...@@ -298,6 +298,46 @@ class CustomControl extends CustomElement {
reportValidity() { reportValidity() {
return getInternal.call(this)?.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() { ...@@ -313,7 +353,7 @@ function getInternal() {
throw new Error("ElementInternals is not supported and a polyfill is necessary"); throw new Error("ElementInternals is not supported and a polyfill is necessary");
} }
return this[attachedInternalSymbol]; return self[attachedInternalSymbol];
} }
/** /**
......
...@@ -31,6 +31,7 @@ import {instanceSymbol} from "../constants.mjs"; ...@@ -31,6 +31,7 @@ import {instanceSymbol} from "../constants.mjs";
import {getDocumentTranslations, Translations} from "../i18n/translations.mjs"; import {getDocumentTranslations, Translations} from "../i18n/translations.mjs";
import {getSlottedElements} from "./slotted.mjs"; import {getSlottedElements} from "./slotted.mjs";
import {initOptionsFromAttributes} from "./util/init-options-from-attributes.mjs"; import {initOptionsFromAttributes} from "./util/init-options-from-attributes.mjs";
import {setOptionFromAttribute} from "./util/set-option-from-attribute.mjs";
export { export {
CustomElement, CustomElement,
...@@ -220,7 +221,7 @@ class CustomElement extends HTMLElement { ...@@ -220,7 +221,7 @@ class CustomElement extends HTMLElement {
* @since 2.1.0 * @since 2.1.0
*/ */
static get [instanceSymbol]() { 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 { ...@@ -232,7 +233,7 @@ class CustomElement extends HTMLElement {
* @since 1.15.0 * @since 1.15.0
*/ */
static get observedAttributes() { static get observedAttributes() {
return [ATTRIBUTE_OPTIONS, ATTRIBUTE_DISABLED]; return [];
} }
/** /**
...@@ -309,7 +310,7 @@ class CustomElement extends HTMLElement { ...@@ -309,7 +310,7 @@ class CustomElement extends HTMLElement {
*/ */
get defaults() { get defaults() {
return { return {
ATTRIBUTE_DISABLED: this.getAttribute(ATTRIBUTE_DISABLED), disabled: false,
shadowMode: "open", shadowMode: "open",
delegatesFocus: true, delegatesFocus: true,
templates: { templates: {
...@@ -605,8 +606,11 @@ class CustomElement extends HTMLElement { ...@@ -605,8 +606,11 @@ class CustomElement extends HTMLElement {
attributeChangedCallback(attrName, oldVal, newVal) { attributeChangedCallback(attrName, oldVal, newVal) {
const self = this; 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)) { if (isFunction(callback)) {
try { try {
callback.call(self, newVal, oldVal); callback.call(self, newVal, oldVal);
......
/**
* 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
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import {Pathfinder} from '../../data/pathfinder.mjs'; import {Pathfinder} from '../../data/pathfinder.mjs';
import {isFunction} from '../../types/is.mjs'; import {isFunction} from '../../types/is.mjs';
import {attributeObserverSymbol} from "../customelement.mjs"; import {attributeObserverSymbol} from "../customelement.mjs";
import {extractKeys} from "./extract-keys.mjs";
export {initOptionsFromAttributes}; export {initOptionsFromAttributes};
...@@ -75,63 +76,10 @@ function initOptionsFromAttributes(element, options, mapping = {}, prefix = 'dat ...@@ -75,63 +76,10 @@ function initOptionsFromAttributes(element, options, mapping = {}, prefix = 'dat
} }
finder.setVia(optionName, value); 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; 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;
}
/**
* 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;
}
...@@ -22,8 +22,9 @@ ...@@ -22,8 +22,9 @@
} }
} }
</script> </script>
<form>
<monster-1>Monster1</monster-1> <monster-1 data-monster-option-a-b-c="114">Monster1</monster-1>
</form>
</main> </main>
</body> </body>
</html> </html>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// import {Updater} from '../../../application/source/dom/updater.mjs'; // import {Updater} from '../../../application/source/dom/updater.mjs';
import { import {
attributeObserverSymbol, attributeObserverSymbol,
CustomElement, CustomElement,CustomControl,
registerCustomElement registerCustomElement
} from '../../../application/source/monster.mjs'; } from '../../../application/source/monster.mjs';
import {domReady} from '../../../application/source/dom/ready.mjs'; import {domReady} from '../../../application/source/dom/ready.mjs';
...@@ -12,7 +12,7 @@ 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"); const initMethodSymbol = Symbol.for("@schukai/monster/dom/@@initMethodSymbol");
class Monster1 extends CustomElement { class Monster1 extends CustomControl {
constructor() { constructor() {
super(); super();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment