1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2024-11-23 00:59:38 +01:00

Compare commits

..

2 Commits

Author SHA1 Message Date
971b595927
fixes #9 2024-02-03 14:36:17 +01:00
b740b8e020
Memo/reuse in O.el (#18)
* 🎉

*  Add hasOwn helper and replace `Reflect.*`

* Update package.json
2024-02-03 14:29:24 +01:00
17 changed files with 105 additions and 93 deletions

File diff suppressed because one or more lines are too long

32
dist/dde.js vendored

File diff suppressed because one or more lines are too long

View File

@ -245,7 +245,7 @@ export function customElementRender<
props?: P | ((...args: any[])=> P) props?: P | ((...args: any[])=> P)
): EL ): EL
export function customElementWithDDE<EL extends HTMLElement>(custom_element: EL): EL export function customElementWithDDE<EL extends HTMLElement>(custom_element: EL): EL
export function lifecycleToEvents<EL extends HTMLElement>(custom_element: EL): EL export function lifecyclesToEvents<EL extends HTMLElement>(custom_element: EL): EL
export function observedAttributes(custom_element: HTMLElement): Record<string, string> export function observedAttributes(custom_element: HTMLElement): Record<string, string>
/* TypeScript MEH */ /* TypeScript MEH */

File diff suppressed because one or more lines are too long

2
dist/esm.d.ts vendored
View File

@ -245,7 +245,7 @@ export function customElementRender<
props?: P | ((...args: any[])=> P) props?: P | ((...args: any[])=> P)
): EL ): EL
export function customElementWithDDE<EL extends HTMLElement>(custom_element: EL): EL export function customElementWithDDE<EL extends HTMLElement>(custom_element: EL): EL
export function lifecycleToEvents<EL extends HTMLElement>(custom_element: EL): EL export function lifecyclesToEvents<EL extends HTMLElement>(custom_element: EL): EL
export function observedAttributes(custom_element: HTMLElement): Record<string, string> export function observedAttributes(custom_element: HTMLElement): Record<string, string>
/* TypeScript MEH */ /* TypeScript MEH */

2
dist/esm.js vendored

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@ import { O } from "deka-dom-el/observables";
O.observedAttributes; O.observedAttributes;
// “internal” utils // “internal” utils
import { lifecycleToEvents } from "deka-dom-el"; import { lifecyclesToEvents } from "deka-dom-el";
</code></div><h3 id="h-custom-elements-introduction"><!--<dde:mark type="component" name="h3" host="parentElement" ssr/>--><a href="#h-custom-elements-introduction" tabindex="-1">#</a> Custom Elements Introduction</h3><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements" title="Article about custom elements on MDN">Using custom elements</a></p><div class="code" data-js="todo"><!--<dde:mark type="component" name="code" host="parentElement" ssr/>--><code class="language-js">class CustomHTMLElement extends HTMLElement{ </code></div><h3 id="h-custom-elements-introduction"><!--<dde:mark type="component" name="h3" host="parentElement" ssr/>--><a href="#h-custom-elements-introduction" tabindex="-1">#</a> Custom Elements Introduction</h3><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements" title="Article about custom elements on MDN">Using custom elements</a></p><div class="code" data-js="todo"><!--<dde:mark type="component" name="code" host="parentElement" ssr/>--><code class="language-js">class CustomHTMLElement extends HTMLElement{
static tagName = "custom-element"; // just suggestion, we can use `el(CustomHTMLElement.tagName)` static tagName = "custom-element"; // just suggestion, we can use `el(CustomHTMLElement.tagName)`
static observedAttributes= [ "custom-attribute" ]; static observedAttributes= [ "custom-attribute" ];

View File

@ -9,4 +9,4 @@ import { O } from "deka-dom-el/observables";
O.observedAttributes; O.observedAttributes;
// “internal” utils // “internal” utils
import { lifecycleToEvents } from "deka-dom-el"; import { lifecyclesToEvents } from "deka-dom-el";

View File

@ -7,7 +7,7 @@ export function mnemonic(){
el("code", "customElementRender(<custom-element>, <render-function>[, <properties>])"), " — use function to render DOM structure for given <custom-element>", el("code", "customElementRender(<custom-element>, <render-function>[, <properties>])"), " — use function to render DOM structure for given <custom-element>",
), ),
el("li").append( el("li").append(
el("code", "customElementWithDDE(<custom-element>)"), " — register <custom-element> to DDE library, see also `lifecycleToEvents`, can be also used as decorator", el("code", "customElementWithDDE(<custom-element>)"), " — register <custom-element> to DDE library, see also `lifecyclesToEvents`, can be also used as decorator",
), ),
el("li").append( el("li").append(
el("code", "observedAttributes(<custom-element>)"), " — returns record of observed attributes (keys uses camelCase)", el("code", "observedAttributes(<custom-element>)"), " — returns record of observed attributes (keys uses camelCase)",
@ -16,7 +16,7 @@ export function mnemonic(){
el("code", "O.observedAttributes(<custom-element>)"), " — returns record of observed attributes (keys uses camelCase and values are observables)", el("code", "O.observedAttributes(<custom-element>)"), " — returns record of observed attributes (keys uses camelCase and values are observables)",
), ),
el("li").append( el("li").append(
el("code", "lifecycleToEvents(<class-declaration>)"), " — convert lifecycle methods to events, can be also used as decorator", el("code", "lifecyclesToEvents(<class-declaration>)"), " — convert lifecycle methods to events, can be also used as decorator",
) )
); );
} }

View File

@ -16,10 +16,12 @@ const className= style.host(todosComponent).css`
`; `;
/** @param {{ todos: string[] }} */ /** @param {{ todos: string[] }} */
export function todosComponent({ todos= [ "Task A" ] }= {}){ export function todosComponent({ todos= [ "Task A" ] }= {}){
const todosO= O(todos.map(t=> O(t)), { let key= 0;
add(v){ this.value.push(O(v)); }, const todosO= O(new Map(), {
remove(i){ O.clear(this.value.splice(i, 1)[0]); } add(v){ this.value.set(key++, O(v)); },
remove(key){ O.clear(this.value.get(key)); this.value.delete(key); }
}); });
todos.forEach(text=> O.action(todosO, "add", text));
const name= "todoName"; const name= "todoName";
const onsubmitAdd= on("submit", event=> { const onsubmitAdd= on("submit", event=> {
@ -31,24 +33,16 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){
const onremove= on("remove", event=> const onremove= on("remove", event=>
O.action(todosO, "remove", event.detail)); O.action(todosO, "remove", event.detail));
const ul_todos_version= 1; // ul_todos_v1/ul_todos_v2
const ul_todos_v1= el("ul").append(
O.el(todosO, ts=> ts
.map((textContent, value)=>
el(todoComponent, { textContent, value, className }, onremove))
));
const ul_todos_v2= ts=> el("ul").append(
...ts.map((textContent, value)=>
el(todoComponent, { textContent, value, className }, onremove))
);
return el("div", { className }).append( return el("div", { className }).append(
el("div").append( el("div").append(
el("h2", "Todos:"), el("h2", "Todos:"),
el("h3", "List of todos:"), el("h3", "List of todos:"),
O.el(todosO, ts=> !ts.length O.el(todosO, (ts, memo)=> !ts.size
? el("p", "No todos yet") ? el("p", "No todos yet")
: ( !(ul_todos_version-1) ? ul_todos_v1 : ul_todos_v2(ts) ) : el("ul").append(
...Array.from(ts).map(([ value, textContent ])=>
memo(value, ()=> el(todoComponent, { textContent, value, className }, onremove)))
)
), ),
el("p", "Click to the text to edit it.") el("p", "Click to the text to edit it.")
), ),
@ -61,7 +55,7 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){
), ),
el("div").append( el("div").append(
el("h3", "Output (JSON):"), el("h3", "Output (JSON):"),
el("output", O(()=> JSON.stringify(todosO, null, "\t"))) el("output", O(()=> JSON.stringify(Array.from(todosO()), null, "\t")))
) )
) )
} }

2
index.d.ts vendored
View File

@ -180,7 +180,7 @@ export function customElementRender<
props?: P | ((...args: any[])=> P) props?: P | ((...args: any[])=> P)
): EL ): EL
export function customElementWithDDE<EL extends HTMLElement>(custom_element: EL): EL export function customElementWithDDE<EL extends HTMLElement>(custom_element: EL): EL
export function lifecycleToEvents<EL extends HTMLElement>(custom_element: EL): EL export function lifecyclesToEvents<EL extends HTMLElement>(custom_element: EL): EL
export function observedAttributes(custom_element: HTMLElement): Record<string, string> export function observedAttributes(custom_element: HTMLElement): Record<string, string>
/* TypeScript MEH */ /* TypeScript MEH */

View File

@ -79,7 +79,7 @@
}, },
{ {
"path": "./index-with-observables.js", "path": "./index-with-observables.js",
"limit": "5 kB" "limit": "5.25 kB"
} }
], ],
"modifyEsbuildConfig": { "modifyEsbuildConfig": {

View File

@ -8,7 +8,7 @@ export function customElementRender(custom_element, target, render, props= obser
}); });
if(typeof props==="function") props= props.call(custom_element, custom_element); if(typeof props==="function") props= props.call(custom_element, custom_element);
const is_lte= custom_element[keyLTE]; const is_lte= custom_element[keyLTE];
if(!is_lte) lifecycleToEvents(custom_element); if(!is_lte) lifecyclesToEvents(custom_element);
const out= render.call(custom_element, props); const out= render.call(custom_element, props);
if(!is_lte) custom_element.dispatchEvent(new Event(evc)); if(!is_lte) custom_element.dispatchEvent(new Event(evc));
if(target.nodeType===11 && typeof target.mode==="string") // is ShadowRoot if(target.nodeType===11 && typeof target.mode==="string") // is ShadowRoot
@ -16,7 +16,7 @@ export function customElementRender(custom_element, target, render, props= obser
scope.pop(); scope.pop();
return target.append(out); return target.append(out);
} }
export function lifecycleToEvents(class_declaration){ export function lifecyclesToEvents(class_declaration){
wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail){ wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail){
target.apply(thisArg, detail); target.apply(thisArg, detail);
thisArg.dispatchEvent(new Event(evc)); thisArg.dispatchEvent(new Event(evc));
@ -37,7 +37,7 @@ export function lifecycleToEvents(class_declaration){
class_declaration.prototype[keyLTE]= true; class_declaration.prototype[keyLTE]= true;
return class_declaration; return class_declaration;
} }
export { lifecycleToEvents as customElementWithDDE }; export { lifecyclesToEvents as customElementWithDDE };
function wrapMethod(obj, method, apply){ function wrapMethod(obj, method, apply){
obj[method]= new Proxy(obj[method] || (()=> {}), { apply }); obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
} }

View File

@ -26,7 +26,7 @@ function setDeleteAttr(obj, prop, val){
if(Reflect.get(obj, prop)==="undefined") if(Reflect.get(obj, prop)==="undefined")
return Reflect.set(obj, prop, ""); return Reflect.set(obj, prop, "");
} }
export const keyLTE= "__dde_lifecycleToEvents"; //boolean export const keyLTE= "__dde_lifecyclesToEvents"; //boolean
export const evc= "dde:connected"; export const evc= "dde:connected";
export const evd= "dde:disconnected"; export const evd= "dde:disconnected";
export const eva= "dde:attributeChanged"; export const eva= "dde:attributeChanged";

View File

@ -65,12 +65,13 @@ export function createElement(tag, attributes, ...addons){
scoped= 2; scoped= 2;
return el; return el;
} }
import { hasOwn } from "./helpers.js";
/** @param {HTMLElement} element @param {HTMLElement} [root] */ /** @param {HTMLElement} element @param {HTMLElement} [root] */
export function simulateSlots(element, root= element, mapper= undefined){ export function simulateSlots(element, root= element, mapper= undefined){
const _default= Symbol.for("default"); const _default= Symbol.for("default");
const slots= Array.from(root.querySelectorAll("slot")) const slots= Array.from(root.querySelectorAll("slot"))
.reduce((out, curr)=> Reflect.set(out, curr.name || _default, curr) && out, {}); .reduce((out, curr)=> Reflect.set(out, curr.name || _default, curr) && out, {});
const has_d= Reflect.has(slots, _default); const has_d= hasOwn(slots, _default);
element.append= new Proxy(element.append, { element.append= new Proxy(element.append, {
apply(orig, _, els){ apply(orig, _, els){
if(!els.length) return element; if(!els.length) return element;
@ -198,7 +199,7 @@ export function elementAttribute(element, op, key, value){
import { isUndef } from "./helpers.js"; import { isUndef } from "./helpers.js";
//TODO add cache? `(Map/Set)<el.tagName+key,isUndef>` //TODO add cache? `(Map/Set)<el.tagName+key,isUndef>`
function isPropSetter(el, key){ function isPropSetter(el, key){
if(!Reflect.has(el, key)) return false; if(!(key in el)) return false;
const des= getPropDescriptor(el, key); const des= getPropDescriptor(el, key);
return !isUndef(des.set); return !isUndef(des.set);
} }

View File

@ -1,3 +1,4 @@
export const hasOwn= (...a)=> Object.prototype.hasOwnProperty.call(...a);
export function isUndef(value){ return typeof value==="undefined"; } export function isUndef(value){ return typeof value==="undefined"; }
export function typeOf(v){ export function typeOf(v){
const t= typeof v; const t= typeof v;
@ -19,7 +20,7 @@ export function observedAttributes(instance, observedAttribute){
const { observedAttributes= [] }= instance.constructor; const { observedAttributes= [] }= instance.constructor;
return observedAttributes return observedAttributes
.reduce(function(out, name){ .reduce(function(out, name){
Reflect.set(out, kebabToCamel(name), observedAttribute(instance, name)); out[kebabToCamel(name)]= observedAttribute(instance, name);
return out; return out;
}, {}); }, {});
} }

View File

@ -1,7 +1,8 @@
export const mark= "__dde_observable"; export const mark= "__dde_observable";
import { hasOwn } from "./helpers.js";
export function isObservable(candidate){ export function isObservable(candidate){
try{ return Reflect.has(candidate, mark); } try{ return hasOwn(candidate, mark); }
catch(e){ return false; } catch(e){ return false; }
} }
/** @type {function[]} */ /** @type {function[]} */
@ -45,10 +46,10 @@ export function observable(value, actions){
export { observable as O }; export { observable as O };
observable.action= function(o, name, ...a){ observable.action= function(o, name, ...a){
const s= o[mark], { actions }= s; const s= o[mark], { actions }= s;
if(!actions || !Reflect.has(actions, name)) if(!actions || !(name in actions))
throw new Error(`'${o}' has no action with name '${name}'!`); throw new Error(`'${o}' has no action with name '${name}'!`);
actions[name].apply(s, a); actions[name].apply(s, a);
if(s.skip) return Reflect.deleteProperty(s, "skip"); if(s.skip) return (delete s.skip);
s.listeners.forEach(l=> l(s.value)); s.listeners.forEach(l=> l(s.value));
}; };
observable.on= function on(o, listener, options= {}){ observable.on= function on(o, listener, options= {}){
@ -65,11 +66,12 @@ observable.symbols= {
}; };
observable.clear= function(...observables){ observable.clear= function(...observables){
for(const o of observables){ for(const o of observables){
Reflect.deleteProperty(o, "toJSON");
const s= o[mark]; const s= o[mark];
if(!s) continue;
delete o.toJSON;
s.onclear.forEach(f=> f.call(s)); s.onclear.forEach(f=> f.call(s));
clearListDeps(o, s); clearListDeps(o, s);
Reflect.deleteProperty(o, mark); delete o[mark];
} }
function clearListDeps(o, s){ function clearListDeps(o, s){
s.listeners.forEach(l=> { s.listeners.forEach(l=> {
@ -89,24 +91,38 @@ const key_reactive= "__dde_reactive";
import { enviroment as env } from "./dom-common.js"; import { enviroment as env } from "./dom-common.js";
import { el } from "./dom.js"; import { el } from "./dom.js";
import { scope } from "./dom.js"; import { scope } from "./dom.js";
// TODO: third argument for handle `cache_tmp` in re-render
observable.el= function(o, map){ observable.el= function(o, map){
const mark_start= el.mark({ type: "reactive" }, true); const mark_start= el.mark({ type: "reactive" }, true);
const mark_end= mark_start.end; const mark_end= mark_start.end;
const out= env.D.createDocumentFragment(); const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end); out.append(mark_start, mark_end);
const { current }= scope; const { current }= scope;
let cache= {};
const reRenderReactiveElement= v=> { const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // isConnected or wasnt yet rendered if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeObservableListener(o, reRenderReactiveElement); return removeObservableListener(o, reRenderReactiveElement);
const cache_tmp= cache; // will be reused in the useCache or removed in the while loop on the end
cache= {};
scope.push(current); scope.push(current);
let els= map(v); let els= map(v, function useCache(key, fun){
let value;
if(hasOwn(cache_tmp, key)){
value= cache_tmp[key];
delete cache_tmp[key];
} else
value= fun();
cache[key]= value;
return value;
});
scope.pop(); scope.pop();
if(!Array.isArray(els)) if(!Array.isArray(els))
els= [ els ]; els= [ els ];
let el_r= mark_start;
while(( el_r= mark_start.nextSibling ) !== mark_end)
el_r.remove();
mark_start.after(...els); mark_start.after(...els);
const el_last_keep= els[els.length-1];
let el_r;
while(( el_r= el_last_keep.nextSibling ) !== mark_end)
el_r.remove();
if(mark_start.isConnected) if(mark_start.isConnected)
requestCleanUpReactives(current.host()); requestCleanUpReactives(current.host());
}; };
@ -183,7 +199,7 @@ function removeObservablesFromElements(o, listener, ...notes){
* You can investigate the `__dde_reactive` key of the element. * You can investigate the `__dde_reactive` key of the element.
* */ * */
element[key_reactive].forEach(([ [ o, listener ] ])=> element[key_reactive].forEach(([ [ o, listener ] ])=>
removeObservableListener(o, listener, o[mark]?.host() === element)) removeObservableListener(o, listener, o[mark] && o[mark].host && o[mark].host() === element))
)(element); )(element);
} }
element[key_reactive].push([ [ o, listener ], ...notes ]); element[key_reactive].push([ [ o, listener ], ...notes ]);
@ -216,7 +232,7 @@ function toObservable(o, value, actions, readonly= false){
const { onclear: ocs }= observable.symbols; const { onclear: ocs }= observable.symbols;
if(actions[ocs]){ if(actions[ocs]){
onclear.push(actions[ocs]); onclear.push(actions[ocs]);
Reflect.deleteProperty(actions, ocs); delete actions[ocs];
} }
const { host }= scope; const { host }= scope;
Reflect.defineProperty(o, mark, { Reflect.defineProperty(o, mark, {