1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2024-11-21 15:39:36 +01:00

💥 customElement (#11)

* 🎉

* Update customElement.js

* 💥 `observedAttributes`
This commit is contained in:
Jan Andrle 2024-01-05 16:49:05 +01:00 committed by GitHub
parent eb920f7bbd
commit e88a495525
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 133 additions and 103 deletions

File diff suppressed because one or more lines are too long

29
dist/dde.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/esm.js vendored

File diff suppressed because one or more lines are too long

View File

@ -121,7 +121,7 @@ document.head.append(
const interval= 5 * 1000;
setTimeout(clearInterval, 10*interval,
setInterval(()=> count(count()+1), interval));
</code></div><script>Flems(document.getElementById("code-example-1-ehcq40v0h5k"), JSON.parse("{\"files\":[{\"name\":\".js\",\"content\":\"import { O } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\nconst count= O(0);\\n\\nimport { el } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\ndocument.body.append(\\n\\tel(\\\"p\\\", O(()=> \\\"Currently: \\\"+count())),\\n\\tel(\\\"p\\\", { classList: { red: O(()=> count()%2) }, dataset: { count }, textContent: \\\"Attributes example\\\" })\\n);\\ndocument.head.append(\\n\\tel(\\\"style\\\", \\\".red { color: red; }\\\")\\n);\\n\\nconst interval= 5 * 1000;\\nsetTimeout(clearInterval, 10*interval,\\n\\tsetInterval(()=> count(count()+1), interval));\\n\"}],\"toolbar\":false}"));</script><p>To derived attribute based on value of observable variable just use the observable as a&nbsp;value of the attribute (<code>assign(element, { attribute: O('value') })</code>). <code>assign</code>/<code>el</code> provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in attributes <code>dataset</code>, <code>ariaset</code> and <code>classList</code>.</p><p>For computation, you can use the derived observable (see above) like <code>assign(element, { textContent: O(()=&gt; 'Hello '+WorldObservable()) })</code>.</p><p>To represent part of the template filled dynamically based on the observable value use <code>O.el(observable, DOMgenerator)</code>. This was already used in the todo example above or see:</p><!--<dde:mark type="component" name="example" host="this" ssr/>--><div id="code-example-2-8r8qappf8mo" class="example"><!--<dde:mark type="component" name="code" host="parentElement" ssr/>--><code class="language-js">import { O } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js";
</code></div><script>Flems(document.getElementById("code-example-1-ehcq40v0h5k"), JSON.parse("{\"files\":[{\"name\":\".js\",\"content\":\"import { O } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\nconst count= O(0);\\n\\nimport { el } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\ndocument.body.append(\\n\\tel(\\\"p\\\", O(()=> \\\"Currently: \\\"+count())),\\n\\tel(\\\"p\\\", { classList: { red: O(()=> count()%2) }, dataset: { count }, textContent: \\\"Attributes example\\\" })\\n);\\ndocument.head.append(\\n\\tel(\\\"style\\\", \\\".red { color: red; }\\\")\\n);\\n\\nconst interval= 5 * 1000;\\nsetTimeout(clearInterval, 10*interval,\\n\\tsetInterval(()=> count(count()+1), interval));\\n\"}],\"toolbar\":false}"));</script><p>To derived attribute based on value of observable variable just use the observable as a&nbsp;value of the attribute (<code>assign(element, { attribute: O('value') })</code>). <code>assign</code>/<code>el</code> provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in attributes <code>dataset</code>, <code>ariaset</code> and <code>classList</code>.</p><p>For computation, you can use the derived observable (see above) like <code>assign(element, { textContent: O(()=&gt; 'Hello '+WorldObservable()) })</code>. This is read-only observable its value is computed based on given function and updated when any observable used in the function changes.</p><p>To represent part of the template filled dynamically based on the observable value use <code>O.el(observable, DOMgenerator)</code>. This was already used in the todo example above or see:</p><!--<dde:mark type="component" name="example" host="this" ssr/>--><div id="code-example-2-8r8qappf8mo" class="example"><!--<dde:mark type="component" name="code" host="parentElement" ssr/>--><code class="language-js">import { O } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js";
const count= O(0, {
add(){ this.value= this.value + Math.round(Math.random()*10); }
});
@ -147,4 +147,4 @@ setTimeout(clearInterval, 10*interval, setInterval(function(){
O.action(count, "add");
O.action(numbers, "push", count());
}, interval));
</code></div><script>Flems(document.getElementById("code-example-2-8r8qappf8mo"), JSON.parse("{\"files\":[{\"name\":\".js\",\"content\":\"import { O } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\nconst count= O(0, {\\n\\tadd(){ this.value= this.value + Math.round(Math.random()*10); }\\n});\\nconst numbers= O([ count() ], {\\n\\tpush(next){ this.value.push(next); }\\n});\\n\\nimport { el } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\ndocument.body.append(\\n\\tO.el(count, count=> count%2\\n\\t\\t? el(\\\"p\\\", \\\"Last number is odd.\\\")\\n\\t\\t: el()\\n\\t),\\n\\tel(\\\"p\\\", \\\"Lucky numbers:\\\"),\\n\\tel(\\\"ul\\\").append(\\n\\t\\tO.el(numbers, numbers=> numbers.toReversed()\\n\\t\\t\\t.map(n=> el(\\\"li\\\", n)))\\n\\t)\\n);\\n\\nconst interval= 5*1000;\\nsetTimeout(clearInterval, 10*interval, setInterval(function(){\\n\\tO.action(count, \\\"add\\\");\\n\\tO.action(numbers, \\\"push\\\", count());\\n}, interval));\\n\"}],\"toolbar\":false}"));</script><div class="notice"><!--<dde:mark type="component" name="mnemonic" host="parentElement" ssr/>--><h3 id="h-mnemonic"><!--<dde:mark type="component" name="h3" host="parentElement" ssr/>--><a href="#h-mnemonic" tabindex="-1">#</a> Mnemonic</h3><ul><li><code>O(&lt;value&gt;)</code> — observable: reactive value</li><li><code>O(()=&gt; &lt;computation&gt;)</code> — observable: reactive value dependent on calculation using other observables</li><li><code>O.on(&lt;observable&gt;, &lt;listener&gt;[, &lt;options&gt;])</code> — listen to the observable value changes</li><li><code>O.clear(...&lt;observables&gt;)</code> — off and clear observables</li><li><code>O(&lt;value&gt;, &lt;actions&gt;)</code> — observable: pattern to create complex reactive objects/arrays</li><li><code>O.action(&lt;observable&gt;, &lt;action-name&gt;, ...&lt;action-arguments&gt;)</code> — invoke an&nbsp;action for given observable</li><li><code>O.el(&lt;observable&gt;, &lt;function-returning-dom&gt;)</code> — render partial dom structure (template) based on the current observable value</li></ul></div><div class="prevNext"><!--<dde:mark type="component" name="prevNext" host="parentElement" ssr/>--><a rel="prev" href="p03-events" title="Using not only events in UI declaratively."><!--<dde:mark type="component" name="pageLink" host="parentElement" ssr/>-->Events and Addons (previous)</a><a rel="next" href="p05-scopes" title="Organizing UI into components"><!--<dde:mark type="component" name="pageLink" host="parentElement" ssr/>-->(next) Scopes and components</a></div></main></body></html>
</code></div><script>Flems(document.getElementById("code-example-2-8r8qappf8mo"), JSON.parse("{\"files\":[{\"name\":\".js\",\"content\":\"import { O } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\nconst count= O(0, {\\n\\tadd(){ this.value= this.value + Math.round(Math.random()*10); }\\n});\\nconst numbers= O([ count() ], {\\n\\tpush(next){ this.value.push(next); }\\n});\\n\\nimport { el } from \\\"https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js\\\";\\ndocument.body.append(\\n\\tO.el(count, count=> count%2\\n\\t\\t? el(\\\"p\\\", \\\"Last number is odd.\\\")\\n\\t\\t: el()\\n\\t),\\n\\tel(\\\"p\\\", \\\"Lucky numbers:\\\"),\\n\\tel(\\\"ul\\\").append(\\n\\t\\tO.el(numbers, numbers=> numbers.toReversed()\\n\\t\\t\\t.map(n=> el(\\\"li\\\", n)))\\n\\t)\\n);\\n\\nconst interval= 5*1000;\\nsetTimeout(clearInterval, 10*interval, setInterval(function(){\\n\\tO.action(count, \\\"add\\\");\\n\\tO.action(numbers, \\\"push\\\", count());\\n}, interval));\\n\"}],\"toolbar\":false}"));</script><div class="notice"><!--<dde:mark type="component" name="mnemonic" host="parentElement" ssr/>--><h3 id="h-mnemonic"><!--<dde:mark type="component" name="h3" host="parentElement" ssr/>--><a href="#h-mnemonic" tabindex="-1">#</a> Mnemonic</h3><ul><li><code>O(&lt;value&gt;)</code> — observable: reactive value</li><li><code>O(()=&gt; &lt;computation&gt;)</code>read-only observable: reactive value dependent on calculation using other observables</li><li><code>O.on(&lt;observable&gt;, &lt;listener&gt;[, &lt;options&gt;])</code> — listen to the observable value changes</li><li><code>O.clear(...&lt;observables&gt;)</code> — off and clear observables</li><li><code>O(&lt;value&gt;, &lt;actions&gt;)</code> — observable: pattern to create complex reactive objects/arrays</li><li><code>O.action(&lt;observable&gt;, &lt;action-name&gt;, ...&lt;action-arguments&gt;)</code> — invoke an&nbsp;action for given observable</li><li><code>O.el(&lt;observable&gt;, &lt;function-returning-dom&gt;)</code> — render partial dom structure (template) based on the current observable value</li></ul></div><div class="prevNext"><!--<dde:mark type="component" name="prevNext" host="parentElement" ssr/>--><a rel="prev" href="p03-events" title="Using not only events in UI declaratively."><!--<dde:mark type="component" name="pageLink" host="parentElement" ssr/>-->Events and Addons (previous)</a><a rel="next" href="p05-scopes" title="Organizing UI into components"><!--<dde:mark type="component" name="pageLink" host="parentElement" ssr/>-->(next) Scopes and components</a></div></main></body></html>

View File

@ -7,7 +7,7 @@ export function mnemonic(){
el("code", "O(<value>)"), " — observable: reactive value",
),
el("li").append(
el("code", "O(()=> <computation>)"), " — observable: reactive value dependent on calculation using other observables",
el("code", "O(()=> <computation>)"), " — read-only observable: reactive value dependent on calculation using other observables",
),
el("li").append(
el("code", "O.on(<observable>, <listener>[, <options>])"), " — listen to the observable value changes",

View File

@ -88,7 +88,9 @@ export function page({ pkg, info }){
el("code", "ariaset"), " and ", el("code", "classList"), "."
),
el("p").append(
"For computation, you can use the derived observable (see above) like ", el("code", "assign(element, { textContent: O(()=> 'Hello '+WorldObservable()) })"), "."
"For computation, you can use the “derived observable” (see above) like ", el("code", "assign(element, { textContent: O(()=> 'Hello '+WorldObservable()) })"), ".",
" ",
"This is read-only observable its value is computed based on given function and updated when any observable used in the function changes."
),
el("p").append(
"To represent part of the template filled dynamically based on the observable value use ", el("code", "O.el(observable, DOMgenerator)"), ".",

View File

@ -1,4 +1,4 @@
import { style, el, O } from '../exports.js';
import { style, el, O, isObservable } from '../exports.js';
const className= style.host(thirdParty).css`
:host {
color: green;
@ -22,12 +22,13 @@ export function thirdParty(){
// Array.from((new URL(location)).searchParams.entries())
// .forEach(([ key, value ])=> O.action(store, "set", key, value));
// O.on(store, data=> history.replaceState("", "", "?"+(new URLSearchParams(JSON.parse(JSON.stringify(data)))).toString()));
useAdapter(store, store_adapter, {
useStore(store_adapter, {
onread(data){
Array.from(data.entries())
.forEach(([ key, value ])=> O.action(store, "set", key, value));
return store;
}
});
})();
return el("input", {
className,
value: store().value(),
@ -36,9 +37,14 @@ export function thirdParty(){
});
}
function useAdapter(observable, adapter, { onread, onbeforewrite }= {}){
if(!onread) onread= observable;
function useStore(adapter_in, { onread, onbeforewrite }= {}){
const adapter= typeof adapter_in === "function" ? { read: adapter_in } : adapter_in;
if(!onread) onread= O;
if(!onbeforewrite) onbeforewrite= data=> JSON.parse(JSON.stringify(data));
onread(adapter.read()); //TODO OK as synchronous
return function useStoreInner(data_read){
const observable= onread(adapter.read(data_read)); //TODO OK as synchronous
if(adapter.write && isObservable(observable))
O.on(observable, data=> adapter.write(onbeforewrite(data)));
return observable;
};
}

View File

@ -13,24 +13,29 @@ export class CustomHTMLTestElement extends HTMLElement{
connectedCallback(){
if(!this.hasAttribute("pre-name")) this.setAttribute("pre-name", "default");
this.attachShadow({ mode: "open" }).append(
customElementRender(this, this.render)
customElementRender(this, this.render, this.attributes)
);
}
render({ test }){
attributes(element){
const observed= O.observedAttributes(element);
return Object.assign({ test: element.test }, observed);
}
render({ name, preName, test }){
console.log(scope.state);
scope.host(
on.connected(()=> console.log(CustomHTMLTestElement)),
on.attributeChanged(e=> console.log(e)),
on.disconnected(()=> console.log(CustomHTMLTestElement))
);
const name= O.attribute("name");
const preName= O.attribute("pre-name");
console.log({ name, test, preName});
const text= text=> el().append(
el("#text", text),
" | "
);
return el("p").append(
el("#text", name),
el("#text", preName),
text(test),
text(name),
text(preName),
el("button", { type: "button", textContent: "pre-name", onclick: ()=> preName("Ahoj") })
);
}

View File

@ -1,6 +1,6 @@
{
"name": "deka-dom-el",
"version": "0.7.5",
"version": "0.7.6",
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
"author": "Jan Andrle <andrle.jan@centrum.cz>",
"license": "MIT",
@ -46,6 +46,7 @@
"browser": true,
"undef": "true",
"latedef": "true",
"-W014": true,
"maxparams": 5,
"maxdepth": 3,
"maxcomplexity": 14,
@ -58,7 +59,7 @@
"size-limit": [
{
"path": "./index.js",
"limit": "9.75 kB",
"limit": "9.85 kB",
"gzip": false,
"brotli": false

View File

@ -1,10 +1,11 @@
import { scope } from "./dom.js";
export function customElementRender(custom_element, render, props= custom_element){
export function customElementRender(custom_element, render, props= observedAttributes){
scope.push({
scope: custom_element,
host: (...c)=> c.length ? c.forEach(c=> c(custom_element)) : custom_element,
custom_element
});
if(typeof props==="function") props= props.call(custom_element, custom_element);
const out= render.call(custom_element, props);
scope.pop();
return out;
@ -31,3 +32,8 @@ export { lifecycleToEvents as customElementWithDDE };
function wrapMethod(obj, method, apply){
obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
}
import { observedAttributes as oA } from "./helpers.js";
export function observedAttributes(instance){
return oA(instance, (i, n)=> i.getAttribute(n));
}

View File

@ -84,6 +84,7 @@ on.attributeChanged= function(listener, options){
});
const c= onAbort(options.signal, ()=> observer.disconnect());
if(c) observer.observe(element, { attributes: true });
//TODO: clean up when element disconnected
return element;
};
};

View File

@ -15,3 +15,12 @@ export function onAbort(signal, listener){
signal.removeEventListener("abort", listener);
};
}
export function observedAttributes(instance, observedAttribute){
const { observedAttributes= [] }= instance.constructor;
return observedAttributes
.reduce(function(out, name){
Reflect.set(out, kebabToCamel(name), observedAttribute(instance, name));
return out;
}, {});
}
function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); }

View File

@ -9,7 +9,7 @@ const stack_watch= [];
/**
* ### `WeakMap<function, Set<ddeObservable<any, any>>>`
* The `Set` is in the form of `[ source, ...depended observables (DSs) ]`.
* When the DS is cleaned (`S.clear`) it is removed from DSs,
* When the DS is cleaned (`O.clear`) it is removed from DSs,
* if remains only one (`source`) it is cleared too.
* ### `WeakMap<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
@ -18,16 +18,16 @@ const stack_watch= [];
const deps= new WeakMap();
export function observable(value, actions){
if(typeof value!=="function")
return create(value, actions);
return create(false, value, actions);
if(isObservable(value)) return value;
const out= create();
const out= create(true);
const contextReWatch= function(){
const [ origin, ...deps_old ]= deps.get(contextReWatch);
deps.set(contextReWatch, new Set([ origin ]));
stack_watch.push(contextReWatch);
out(value());
write(out, value());
stack_watch.pop();
if(!deps_old.length) return;
@ -113,41 +113,38 @@ observable.el= function(o, map){
return out;
};
import { on } from "./events.js";
const key_attributes= "__dde_attributes";
observable.attribute= function(name, initial= null){
//TODO host=element & reuse existing
const out= observable(initial);
let element;
scope.host(el=> {
element= el;
if(elementAttribute(element, "has", name)) out(elementAttribute(element, "get", name));
else if(initial!==null) elementAttribute(element, "set", name, initial);
if(el[key_attributes]){
el[key_attributes][name]= out;
return;
import { observedAttributes } from "./helpers.js";
function observedAttribute(instance, name){
const out= (...args)=> !args.length
? instance.getAttribute(name)
: instance.setAttribute(name, ...args);
out.attribute= name;
return out;
}
element[key_attributes]= { [name]: out };
const key_attributes= "__dde_attributes";
observable.observedAttributes= function(element){
const attrs= observedAttributes(element, observedAttribute);
const store= element[key_attributes]= {};
const actions= {
_set(value){ this.value= value; },
};
Object.keys(attrs).forEach(name=> {
const attr= attrs[name]= toObservable(attrs[name], attrs[name](), actions);
store[attr.attribute]= attr;
});
on.attributeChanged(function attributeChangeToObservable({ detail }){
/*! This maps attributes to observables (`S.attribute`).
/*! This maps attributes to observables (`O.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
const [ name, value ]= detail;
const curr= element[key_attributes][name];
if(curr) return curr(value);
if(curr) return observable.action(curr, "_set", value);
})(element);
on.disconnected(function(){
/*! This removes all observables mapped to attributes (`S.attribute`).
/*! This removes all observables mapped to attributes (`O.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
observable.clear(...Object.values(element[key_attributes]));
})(element);
});
return new Proxy(out, {
apply(target, _, args){
if(!args.length) return target();
const value= args[0];
return elementAttribute(element, "set", name, value);
}
});
return attrs;
};
import { typeOf } from './helpers.js';
@ -169,7 +166,7 @@ function removeObservablesFromElements(o, listener, ...notes){
element[key_reactive]= [];
on.disconnected(()=>
/*!
* Clears all Observables listeners added in the current scope/host (`S.el`, `assign`, ?).
* Clears all Observables listeners added in the current scope/host (`O.el`, `assign`, ?).
* You can investigate the `__dde_reactive` key of the element.
* */
element[key_reactive].forEach(([ [ o, listener ] ])=>
@ -180,9 +177,10 @@ function removeObservablesFromElements(o, listener, ...notes){
});
}
function create(value, actions){
const o= (...value)=>
value.length ? write(o, ...value) : read(o);
function create(is_readonly, value, actions){
const o= is_readonly
? ()=> read(o)
: (...value)=> value.length ? write(o, ...value) : read(o);
return toObservable(o, value, actions);
}
const protoSigal= Object.assign(Object.create(null), {