1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-07-01 12:22:15 +02:00

🔤 🐛 v0.9.1-alpha (#30)

* :tap: removed on.attributeChanged and static observedAttributes

*  import optimalization

*  scope.signal

* 🔤 🐛

*  🐛 registerReactivity and types

* 🔤

* 

* 🔤

* 🐛 Node in enviroment

*  todos

* 

*  🔤

*  lint

*  memo

* 🔤 🐛 memo

*  🔤 todomvc

* 🐛 types

* 🔤 p08 signal factory

* 🔤  types

*  🔤 lint

* 🔤

* 🔤

* 🔤

* 🔤

* 📺
This commit is contained in:
2025-03-12 18:37:42 +01:00
committed by GitHub
parent e1f321004d
commit 25d475ec04
83 changed files with 4899 additions and 2182 deletions

View File

@ -7,10 +7,10 @@ import { c_ch_o } from "./events-observer.js";
*
* @param {Element|ShadowRoot} target - The custom element or shadow root to render into
* @param {Function} render - The render function that returns content
* @param {Function|Object} [props=observedAttributes] - Props to pass to the render function
* @param {Function|Object} [props= {}] - Props to pass to the render function
* @returns {Node} The rendered content
*/
export function customElementRender(target, render, props= observedAttributes){
export function customElementRender(target, render, props= {}){
const custom_element= target.host || target;
scope.push({
scope: custom_element,
@ -69,15 +69,3 @@ export { lifecyclesToEvents as customElementWithDDE };
function wrapMethod(obj, method, apply){
obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
}
import { observedAttributes as oA } from "./helpers.js";
/**
* Gets observed attributes for a custom element
*
* @param {Element} instance - Custom element instance
* @returns {Object} Object mapping camelCase attribute names to their values
*/
export function observedAttributes(instance){
return oA(instance, (i, n)=> i.getAttribute(n));
}

View File

@ -14,6 +14,7 @@ export const enviroment= {
setDeleteAttr,
ssr: "",
D: globalThis.document,
N: globalThis.Node,
F: globalThis.DocumentFragment,
H: globalThis.HTMLElement,
S: globalThis.SVGElement,

View File

@ -1,6 +1,7 @@
import { signals } from "./signals-lib/common.js";
import { enviroment as env } from './dom-common.js';
import { isInstance, isUndef, oAssign } from "./helpers.js";
import { on } from "./events.js";
/**
* Queues a promise, this is helpful for crossplatform components (on server side we can wait for all registered
@ -19,6 +20,8 @@ const scopes= [ {
host: c=> c ? c(env.D.body) : env.D.body,
prevent: true,
} ];
/** Store for disconnect abort controllers */
const store_abort= new WeakMap();
/**
* Scope management utility for tracking component hierarchies
*/
@ -35,6 +38,19 @@ export const scope= {
*/
get host(){ return this.current.host; },
/**
* Creates/gets an AbortController that triggers when the element disconnects
* */
get signal(){
const { host }= this;
if(store_abort.has(host)) return store_abort.get(host);
const a= new AbortController();
store_abort.set(host, a);
host(on.disconnected(()=> a.abort()));
return a.signal;
},
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context

View File

@ -167,9 +167,9 @@ function connectionsChangesObserverConstructor(){
if(store.size > 30)//TODO?: limit
await requestIdle();
const out= [];
if(!isInstance(element, Node)) return out;
if(!isInstance(element, env.N)) return out;
for(const el of store.keys()){
if(el===element || !isInstance(el, Node)) continue;
if(el===element || !isInstance(el, env.N)) continue;
if(element.contains(el))
out.push(el);
}
@ -214,6 +214,7 @@ function connectionsChangesObserverConstructor(){
const ls= store.get(element);
if(!ls.length_d) continue;
// support for S.el, see https://vuejs.org/guide/extras/web-components.html#lifecycle
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out= true;
}

View File

@ -1,5 +1,4 @@
export { registerReactivity } from './signals-lib/common.js';
import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js';
import { keyLTE, evc, evd } from './dom-common.js';
import { oAssign, onAbort } from './helpers.js';
/**
@ -48,7 +47,6 @@ import { c_ch_o } from "./events-observer.js";
const lifeOptions= obj=> oAssign({}, typeof obj==="object" ? obj : null, { once: true });
//TODO: cleanUp when event before abort?
//TODO: docs (e.g.) https://nolanlawson.com/2024/01/13/web-component-gotcha-constructor-vs-connectedcallback/
/**
* Creates a function to register connected lifecycle event listeners
@ -88,53 +86,3 @@ on.disconnected= function(listener, options){
return element;
};
};
/** Store for disconnect abort controllers */
const store_abort= new WeakMap();
/**
* Creates an AbortController that triggers when the element disconnects
*
* @param {Function} host - Host element or function taking an element
* @returns {AbortSignal} AbortSignal that aborts on disconnect
*/
on.disconnectedAsAbort= function(host){
if(store_abort.has(host)) return store_abort.get(host);
const a= new AbortController();
store_abort.set(host, a);
host(on.disconnected(()=> a.abort()));
return a.signal;
};
/** Store for elements with attribute observers */
const els_attribute_store= new WeakSet();
/**
* Creates a function to register attribute change event listeners
*
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the attribute change listener
*/
on.attributeChanged= function(listener, options){
if(typeof options !== "object")
options= {};
return function registerElement(element){
element.addEventListener(eva, listener, options);
if(element[keyLTE] || els_attribute_store.has(element))
return element;
if(!env.M) return element;
const observer= new env.M(function(mutations){
for(const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [ attributeName, target.getAttribute(attributeName) ] }));
});
const c= onAbort(options.signal, ()=> observer.disconnect());
if(c) observer.observe(element, { attributes: true });
//TODO: clean up when element disconnected
return element;
};
};

50
src/memo.js Normal file
View File

@ -0,0 +1,50 @@
import { hasOwn, oCreate } from "./helpers.js";
const memoMark= "__dde_memo";
const memo_scope= [];
/**
* ```js
* const fun= memo.scope(function (list){
* return list.map(item=> memo(item.key, ()=> el(heavy, item.title)));
* }, { onlyLast: true });
* ```
* …this is internally used in `S.el`:
* ```
* S.el(listSignal, list=>
* list.map(item=> memo(item.key, ()=>
* el(heavy, item.title))));
* ```
* */
export function memo(key, generator){
if(!memo_scope.length) return generator(key);
const k= typeof key === "object" ? JSON.stringify(key) : key;
const [ { cache, after } ]= memo_scope;
return after(k, hasOwn(cache, k) ? cache[k] : generator(key));
}
memo.isScope= function(obj){ return obj[memoMark]; };
/**
* @param {Function} fun
* @param {Object} [options={}]
* @param {AbortSignal} options.signal
* @param {boolean} [options.onlyLast=false]
* */
memo.scope= function memoScope(fun, { signal, onlyLast }= {}){
let cache= oCreate();
function memoScope(...args){
if(signal && signal.aborted)
return fun.apply(this, args);
let cache_local= onlyLast ? cache : oCreate();
memo_scope.unshift({
cache,
after(key, val){ return (cache_local[key]= val); }
});
const out= fun.apply(this, args);
memo_scope.shift();
cache= cache_local;
return out;
}
memoScope[memoMark]= true;
memoScope.clear= ()=> cache= oCreate();
if(signal) signal.addEventListener("abort", memoScope.clear);
return memoScope;
};

View File

@ -159,38 +159,31 @@ signal.clear= function(...signals){
};
/** Property key for tracking reactive elements */
const key_reactive= "__dde_reactive";
import { enviroment as env } from "../dom-common.js";
import { enviroment as env, eva } from "../dom-common.js";
import { el } from "../dom.js";
import { scope } from "../dom.js";
import { on } from "../events.js";
import { memo } from "../memo.js";
export function cache(store= oCreate()){
return (key, fun)=> hasOwn(store, key) ? store[key] : (store[key]= fun());
}
/**
* Creates a reactive DOM element that re-renders when signal changes
*
* @TODO Third argument for handle `cache_tmp` in re-render
* @param {Object} s - Signal object to watch
* @param {Function} map - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements
*/
signal.el= function(s, map){
map= memo.isScope(map) ? map : memo.scope(map, { onlyLast: true });
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
let cache_shared= oCreate();
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
const memo= cache(cache_shared);
cache_shared= oCreate();
scope.push(current);
let els= map(v, function useCache(key, fun){
return (cache_shared[key]= memo(key, fun));
});
let els= map(v);
scope.pop();
if(!Array.isArray(els))
els= [ els ];
@ -209,7 +202,7 @@ signal.el= function(s, map){
reRenderReactiveElement(s.get());
current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */
cache_shared= {}
map.clear()
));
return out;
};
@ -265,7 +258,7 @@ const key_attributes= "__dde_attributes";
signal.observedAttributes= function(element){
const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }){
on(eva, function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */
const [ name, value ]= detail;