1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2024-11-24 01:29:36 +01:00
This commit is contained in:
Jan Andrle 2024-01-31 14:37:57 +01:00
parent 13c75fede1
commit b326c0e050
Signed by: jaandrle
GPG Key ID: B3A25AED155AFFAB
10 changed files with 195 additions and 158 deletions

View File

@ -12,9 +12,7 @@ export class CustomHTMLTestElement extends HTMLElement{
} }
connectedCallback(){ connectedCallback(){
if(!this.hasAttribute("pre-name")) this.setAttribute("pre-name", "default"); if(!this.hasAttribute("pre-name")) this.setAttribute("pre-name", "default");
this.attachShadow({ mode: "open" }).append( customElementRender(this, this.attachShadow({ mode: "open" }), this.render, this.attributes)
customElementRender(this, this.render, this.attributes)
);
} }
attributes(element){ attributes(element){
@ -23,6 +21,7 @@ export class CustomHTMLTestElement extends HTMLElement{
} }
render({ name, preName, test }){ render({ name, preName, test }){
console.log(scope.state); console.log(scope.state);
console.log({ name, preName, test });
scope.host( scope.host(
on.connected(()=> console.log(CustomHTMLTestElement)), on.connected(()=> console.log(CustomHTMLTestElement)),
on.attributeChanged(e=> console.log(e)), on.attributeChanged(e=> console.log(e)),
@ -63,7 +62,7 @@ export class CustomSlottingHTMLElement extends HTMLElement{
)); ));
} }
connectedCallback(){ connectedCallback(){
this.append(customElementRender(this, this.render)); customElementRender(this, this, this.render);
} }
} }
customElementWithDDE(CustomSlottingHTMLElement); customElementWithDDE(CustomSlottingHTMLElement);

1
index.d.ts vendored
View File

@ -180,6 +180,7 @@ export function customElementRender<
P extends any = Record<string, any> P extends any = Record<string, any>
>( >(
custom_element: EL, custom_element: EL,
target: ShadowRoot | EL,
render: (props: P)=> SupportedElement, render: (props: P)=> SupportedElement,
props?: P | ((...args: any[])=> P) props?: P | ((...args: any[])=> P)
): EL ): EL

3
observables.d.ts vendored
View File

@ -3,6 +3,7 @@ type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typ
//type SymbolObservable= Symbol; //type SymbolObservable= Symbol;
type SymbolOnclear= symbol; type SymbolOnclear= symbol;
type Actions<V>= Record<string | SymbolOnclear, Action<V>>; type Actions<V>= Record<string | SymbolOnclear, Action<V>>;
type OnListenerOptions= Pick<AddEventListenerOptions, "signal"> & { first_time?: boolean };
interface observable{ interface observable{
_: Symbol _: Symbol
/** /**
@ -40,7 +41,7 @@ interface observable{
...params: A[N] extends (...args: infer P)=> any ? P : never ...params: A[N] extends (...args: infer P)=> any ? P : never
): void; ): void;
clear(...observables: Observable<any, any>[]): void; clear(...observables: Observable<any, any>[]): void;
on<T>(observable: Observable<T, any>, onchange: (a: T)=> void, options?: AddEventListenerOptions): void; on<T>(observable: Observable<T, any>, onchange: (a: T)=> void, options?: OnListenerOptions): void;
symbols: { symbols: {
//observable: SymbolObservable; //observable: SymbolObservable;
onclear: SymbolOnclear; onclear: SymbolOnclear;

View File

@ -1,6 +1,6 @@
{ {
"name": "deka-dom-el", "name": "deka-dom-el",
"version": "0.7.7", "version": "0.7.8",
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.", "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>", "author": "Jan Andrle <andrle.jan@centrum.cz>",
"license": "MIT", "license": "MIT",
@ -59,14 +59,14 @@
"size-limit": [ "size-limit": [
{ {
"path": "./index.js", "path": "./index.js",
"limit": "10 kB", "limit": "10.5 kB",
"gzip": false, "gzip": false,
"brotli": false "brotli": false
}, },
{ {
"path": "./observables.js", "path": "./observables.js",
"limit": "11.5 kB", "limit": "12 kB",
"gzip": false, "gzip": false,
"brotli": false "brotli": false

View File

@ -1,30 +1,40 @@
import { keyLTE } from "./dom-common.js";
import { scope } from "./dom.js"; import { scope } from "./dom.js";
export function customElementRender(custom_element, render, props= observedAttributes){ import { c_ch_o } from "./events-observer.js";
export function customElementRender(custom_element, target, render, props= observedAttributes){
scope.push({ scope.push({
scope: custom_element, scope: custom_element,
host: (...c)=> c.length ? c.forEach(c=> c(custom_element)) : 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); if(typeof props==="function") props= props.call(custom_element, custom_element);
const is_lte= custom_element[keyLTE];
if(!is_lte) lifecycleToEvents(custom_element);
const out= render.call(custom_element, props); const out= render.call(custom_element, props);
if(!is_lte) custom_element.dispatchEvent(new Event("dde:connected"));
if(target.nodeType===11 && typeof target.mode==="string") // is ShadowRoot
custom_element.addEventListener("dde:disconnected", c_ch_o.observe(target), { once: true });
scope.pop(); scope.pop();
return out; return target.append(out);
} }
export function lifecycleToEvents(class_declaration){ export function lifecycleToEvents(class_declaration){
for (const name of [ "connected", "disconnected" ]) wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail){
wrapMethod(class_declaration.prototype, name+"Callback", function(target, thisArg, detail){
target.apply(thisArg, detail); target.apply(thisArg, detail);
thisArg.dispatchEvent(new Event("dde:"+name)); thisArg.dispatchEvent(new Event("dde:connected"));
}); });
const name= "attributeChanged"; wrapMethod(class_declaration.prototype, "disconnectedCallback", function(target, thisArg, detail){
wrapMethod(class_declaration.prototype, name+"Callback", function(target, thisArg, detail){ target.apply(thisArg, detail);
(queueMicrotask || setTimeout)(
()=> !thisArg.isConnected && thisArg.dispatchEvent(new Event("dde:disconnected"))
);
});
wrapMethod(class_declaration.prototype, "attributeChangedCallback", function(target, thisArg, detail){
const [ attribute, , value ]= detail; const [ attribute, , value ]= detail;
thisArg.dispatchEvent(new CustomEvent("dde:"+name, { thisArg.dispatchEvent(new CustomEvent("dde:attributeChanged", {
detail: [ attribute, value ] detail: [ attribute, value ]
})); }));
target.apply(thisArg, detail); target.apply(thisArg, detail);
}); });
class_declaration.prototype.__dde_lifecycleToEvents= true; class_declaration.prototype[keyLTE]= true;
return class_declaration; return class_declaration;
} }
export { lifecycleToEvents as customElementWithDDE }; export { lifecycleToEvents as customElementWithDDE };

View File

@ -26,3 +26,4 @@ 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

View File

@ -5,7 +5,6 @@ import { enviroment as env } from './dom-common.js';
const scopes= [ { const scopes= [ {
get scope(){ return env.D.body; }, get scope(){ return env.D.body; },
host: c=> c ? c(env.D.body) : env.D.body, host: c=> c ? c(env.D.body) : env.D.body,
custom_element: false,
prevent: true, prevent: true,
} ]; } ];
export const scope= { export const scope= {

138
src/events-observer.js Normal file
View File

@ -0,0 +1,138 @@
import { enviroment as env } from './dom-common.js';
export const c_ch_o= env.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get(){ return ()=> {}; }
});
function connectionsChangesObserverConstructor(){
const store= new Map();
let is_observing= false;
const observerListener= stop=> function(mutations){
for(const mutation of mutations){
if(mutation.type!=="childList") continue;
if(observerAdded(mutation.addedNodes, true)){
stop();
continue;
}
if(observerRemoved(mutation.removedNodes, true))
stop();
}
};
const observer= new env.M(observerListener(stop));
return {
observe(element){
const o= new env.M(observerListener(()=> {}));
o.observe(element, { childList: true, subtree: true });
return ()=> o.disconnect();
},
onConnected(element, listener){
start();
const listeners= getElementStore(element);
if(listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c+= 1;
},
offConnected(element, listener){
if(!store.has(element)) return;
const ls= store.get(element);
if(!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c-= 1;
cleanWhenOff(element, ls);
},
onDisconnected(element, listener){
start();
const listeners= getElementStore(element);
if(listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d+= 1;
},
offDisconnected(element, listener){
if(!store.has(element)) return;
const ls= store.get(element);
if(!ls.disconnected.has(listener)) return;
ls.disconnected.delete(listener);
ls.length_d-= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls){
if(ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element){
if(store.has(element)) return store.get(element);
const out= {
connected: new WeakSet(),
length_c: 0,
disconnected: new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start(){
if(is_observing) return;
is_observing= true;
observer.observe(env.D.body, { childList: true, subtree: true });
}
function stop(){
if(!is_observing || store.size) return;
is_observing= false;
observer.disconnect();
}
//TODO remount support?
function requestIdle(){ return new Promise(function(resolve){
(requestIdleCallback || requestAnimationFrame)(resolve);
}); }
async function collectChildren(element){
if(store.size > 30)//TODO limit?
await requestIdle();
const out= [];
if(!(element instanceof Node)) return out;
for(const el of store.keys()){
if(el===element || !(el instanceof Node)) continue;
if(element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root){
let out= false;
for(const element of addedNodes){
if(is_root) collectChildren(element).then(observerAdded);
if(!store.has(element)) continue;
const ls= store.get(element);
if(!ls.length_c) continue;
element.dispatchEvent(new Event("dde:connected"));
ls.connected= new WeakSet();
ls.length_c= 0;
if(!ls.length_d) store.delete(element);
out= true;
}
return out;
}
function observerRemoved(removedNodes, is_root){
let out= false;
for(const element of removedNodes){
if(is_root) collectChildren(element).then(observerRemoved);
if(!store.has(element)) continue;
const ls= store.get(element);
if(!ls.length_d) continue;
(queueMicrotask || setTimeout)(dispatchRemove(element));
out= true;
}
return out;
}
function dispatchRemove(element){
return ()=> {
if(element.isConnected) return;
element.dispatchEvent(new Event("dde:disconnected"));
store.delete(element);
};
}
}

View File

@ -1,5 +1,5 @@
export { registerReactivity } from './observables-common.js'; export { registerReactivity } from './observables-common.js';
import { enviroment as env } from './dom-common.js'; import { enviroment as env, keyLTE } from './dom-common.js';
export function dispatchEvent(name, options, host){ export function dispatchEvent(name, options, host){
if(!options) options= {}; if(!options) options= {};
@ -19,13 +19,12 @@ export function on(event, listener, options){
}; };
} }
const c_ch_o= env.M ? connectionsChangesObserverConstructor() : new Proxy({}, { import { c_ch_o } from "./events-observer.js";
get(){ return ()=> {}; }
});
const els_attribute_store= new WeakSet(); const els_attribute_store= new WeakSet();
import { scope } from "./dom.js"; import { scope } from "./dom.js";
import { onAbort } from './helpers.js'; import { onAbort } from './helpers.js';
//TODO: cleanUp when event before abort? //TODO: cleanUp when event before abort?
//TODO: docs (e.g.) https://nolanlawson.com/2024/01/13/web-component-gotcha-constructor-vs-connectedcallback/
on.connected= function(listener, options){ on.connected= function(listener, options){
const { custom_element }= scope.current; const { custom_element }= scope.current;
const name= "connected"; const name= "connected";
@ -36,7 +35,7 @@ on.connected= function(listener, options){
if(custom_element) element= custom_element; if(custom_element) element= custom_element;
const event= "dde:"+name; const event= "dde:"+name;
element.addEventListener(event, listener, options); element.addEventListener(event, listener, options);
if(element.__dde_lifecycleToEvents) return element; if(element[keyLTE]) return element;
if(element.isConnected) return ( element.dispatchEvent(new Event(event)), element ); if(element.isConnected) return ( element.dispatchEvent(new Event(event)), element );
const c= onAbort(options.signal, ()=> c_ch_o.offConnected(element, listener)); const c= onAbort(options.signal, ()=> c_ch_o.offConnected(element, listener));
@ -54,7 +53,7 @@ on.disconnected= function(listener, options){
if(custom_element) element= custom_element; if(custom_element) element= custom_element;
const event= "dde:"+name; const event= "dde:"+name;
element.addEventListener(event, listener, options); element.addEventListener(event, listener, options);
if(element.__dde_lifecycleToEvents) return element; if(element[keyLTE]) return element;
const c= onAbort(options.signal, ()=> c_ch_o.offDisconnected(element, listener)); const c= onAbort(options.signal, ()=> c_ch_o.offDisconnected(element, listener));
if(c) c_ch_o.onDisconnected(element, listener); if(c) c_ch_o.onDisconnected(element, listener);
@ -77,7 +76,7 @@ on.attributeChanged= function(listener, options){
return function registerElement(element){ return function registerElement(element){
const event= "dde:"+name; const event= "dde:"+name;
element.addEventListener(event, listener, options); element.addEventListener(event, listener, options);
if(element.__dde_lifecycleToEvents || els_attribute_store.has(element)) if(element[keyLTE] || els_attribute_store.has(element))
return element; return element;
if(!env.M) return element; if(!env.M) return element;
@ -93,127 +92,3 @@ on.attributeChanged= function(listener, options){
return element; return element;
}; };
}; };
function connectionsChangesObserverConstructor(){
const store= new Map();
let is_observing= false;
const observer= new env.M(function(mutations){
for(const mutation of mutations){
if(mutation.type!=="childList") continue;
if(observerAdded(mutation.addedNodes, true)){
stop();
continue;
}
if(observerRemoved(mutation.removedNodes, true))
stop();
}
});
return {
onConnected(element, listener){
start();
const listeners= getElementStore(element);
if(listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c+= 1;
},
offConnected(element, listener){
if(!store.has(element)) return;
const ls= store.get(element);
if(!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c-= 1;
cleanWhenOff(element, ls);
},
onDisconnected(element, listener){
start();
const listeners= getElementStore(element);
if(listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d+= 1;
},
offDisconnected(element, listener){
if(!store.has(element)) return;
const ls= store.get(element);
if(!ls.disconnected.has(listener)) return;
ls.disconnected.delete(listener);
ls.length_d-= 1;
cleanWhenOff(element, ls);
}
};
function cleanWhenOff(element, ls){
if(ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element){
if(store.has(element)) return store.get(element);
const out= {
connected: new WeakSet(),
length_c: 0,
disconnected: new WeakSet(),
length_d: 0
};
store.set(element, out);
return out;
}
function start(){
if(is_observing) return;
is_observing= true;
observer.observe(env.D.body, { childList: true, subtree: true });
}
function stop(){
if(!is_observing || store.size) return;
is_observing= false;
observer.disconnect();
}
//TODO remount support?
function requestIdle(){ return new Promise(function(resolve){
(requestIdleCallback || requestAnimationFrame)(resolve);
}); }
async function collectChildren(element, filter){
if(store.size > 30)//TODO limit?
await requestIdle();
const out= [];
if(!(element instanceof Node)) return out;
for(const el of store.keys()){
if(el===element || !(el instanceof Node) || filter(el)) continue;
if(element.contains(el))
out.push(el);
}
return out;
}
function observerAdded(addedNodes, is_root){
let out= false;
for(const element of addedNodes){
if(is_root) collectChildren(element, el=> !el.isConnectedd).then(observerAdded);
if(!store.has(element)) continue;
const ls= store.get(element);
if(!ls.length_c) continue;
element.dispatchEvent(new Event("dde:connected"));
ls.connected= new WeakSet();
ls.length_c= 0;
if(!ls.length_d) store.delete(element);
out= true;
}
return out;
}
function observerRemoved(removedNodes, is_root){
let out= false;
for(const element of removedNodes){
if(is_root) collectChildren(element, el=> el.isConnectedd).then(observerRemoved);
if(!store.has(element)) continue;
const ls= store.get(element);
if(!ls.length_d) continue;
element.dispatchEvent(new Event("dde:disconnected"));
store.delete(element);
out= true;
}
return out;
}
}

View File

@ -96,7 +96,7 @@ observable.el= function(o, map){
out.append(mark_start, mark_end); out.append(mark_start, mark_end);
const { current }= scope; const { current }= scope;
const reRenderReactiveElement= v=> { const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) if(!mark_start.parentNode || !mark_end.parentNode) // isConnected or wasnt yet rendered
return removeObservableListener(o, reRenderReactiveElement); return removeObservableListener(o, reRenderReactiveElement);
scope.push(current); scope.push(current);
let els= map(v); let els= map(v);
@ -107,12 +107,21 @@ observable.el= function(o, map){
while(( el_r= mark_start.nextSibling ) !== mark_end) while(( el_r= mark_start.nextSibling ) !== mark_end)
el_r.remove(); el_r.remove();
mark_start.after(...els); mark_start.after(...els);
if(mark_start.isConnected)
requestCleanUpReactives(current.host());
}; };
addObservableListener(o, reRenderReactiveElement); addObservableListener(o, reRenderReactiveElement);
removeObservablesFromElements(o, reRenderReactiveElement, mark_start, map); removeObservablesFromElements(o, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(o()); reRenderReactiveElement(o());
return out; return out;
}; };
function requestCleanUpReactives(host){
if(!host || !host[key_reactive]) return;
(requestIdleCallback || setTimeout)(function(){
host[key_reactive]= host[key_reactive]
.filter(([ o, el ])=> el.isConnected ? true : (removeObservableListener(...o), false));
});
}
import { on } from "./events.js"; import { on } from "./events.js";
import { observedAttributes } from "./helpers.js"; import { observedAttributes } from "./helpers.js";
const observedAttributeActions= { const observedAttributeActions= {
@ -152,7 +161,11 @@ export const observables_config= {
isObservable, isObservable,
processReactiveAttribute(element, key, attrs, set){ processReactiveAttribute(element, key, attrs, set){
if(!isObservable(attrs)) return attrs; if(!isObservable(attrs)) return attrs;
const l= attr=> set(key, attr); const l= attr=> {
if(!element.isConnected)
return removeObservableListener(attrs, l);
set(key, attr);
};
addObservableListener(attrs, l); addObservableListener(attrs, l);
removeObservablesFromElements(attrs, l, element, key); removeObservablesFromElements(attrs, l, element, key);
return attrs(); return attrs();