From b326c0e050fa98a6df6770833287aae29a861812 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Wed, 31 Jan 2024 14:37:57 +0100 Subject: [PATCH] local --- examples/components/webComponent.js | 7 +- index.d.ts | 1 + observables.d.ts | 3 +- package.json | 6 +- src/customElement.js | 38 +++++--- src/dom-common.js | 1 + src/dom.js | 1 - src/events-observer.js | 138 +++++++++++++++++++++++++++ src/events.js | 139 ++-------------------------- src/observables-lib.js | 19 +++- 10 files changed, 195 insertions(+), 158 deletions(-) create mode 100644 src/events-observer.js diff --git a/examples/components/webComponent.js b/examples/components/webComponent.js index 7f334ec..8f67f0d 100644 --- a/examples/components/webComponent.js +++ b/examples/components/webComponent.js @@ -12,9 +12,7 @@ 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, this.attributes) - ); + customElementRender(this, this.attachShadow({ mode: "open" }), this.render, this.attributes) } attributes(element){ @@ -23,6 +21,7 @@ export class CustomHTMLTestElement extends HTMLElement{ } render({ name, preName, test }){ console.log(scope.state); + console.log({ name, preName, test }); scope.host( on.connected(()=> console.log(CustomHTMLTestElement)), on.attributeChanged(e=> console.log(e)), @@ -63,7 +62,7 @@ export class CustomSlottingHTMLElement extends HTMLElement{ )); } connectedCallback(){ - this.append(customElementRender(this, this.render)); + customElementRender(this, this, this.render); } } customElementWithDDE(CustomSlottingHTMLElement); diff --git a/index.d.ts b/index.d.ts index e99ce12..1a17427 100644 --- a/index.d.ts +++ b/index.d.ts @@ -180,6 +180,7 @@ export function customElementRender< P extends any = Record >( custom_element: EL, + target: ShadowRoot | EL, render: (props: P)=> SupportedElement, props?: P | ((...args: any[])=> P) ): EL diff --git a/observables.d.ts b/observables.d.ts index da4b77a..745f46b 100644 --- a/observables.d.ts +++ b/observables.d.ts @@ -3,6 +3,7 @@ type Action= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typ //type SymbolObservable= Symbol; type SymbolOnclear= symbol; type Actions= Record>; +type OnListenerOptions= Pick & { first_time?: boolean }; interface observable{ _: Symbol /** @@ -40,7 +41,7 @@ interface observable{ ...params: A[N] extends (...args: infer P)=> any ? P : never ): void; clear(...observables: Observable[]): void; - on(observable: Observable, onchange: (a: T)=> void, options?: AddEventListenerOptions): void; + on(observable: Observable, onchange: (a: T)=> void, options?: OnListenerOptions): void; symbols: { //observable: SymbolObservable; onclear: SymbolOnclear; diff --git a/package.json b/package.json index 0ae939c..cdc8de0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "author": "Jan Andrle ", "license": "MIT", @@ -59,14 +59,14 @@ "size-limit": [ { "path": "./index.js", - "limit": "10 kB", + "limit": "10.5 kB", "gzip": false, "brotli": false }, { "path": "./observables.js", - "limit": "11.5 kB", + "limit": "12 kB", "gzip": false, "brotli": false diff --git a/src/customElement.js b/src/customElement.js index 1267c73..64f3209 100644 --- a/src/customElement.js +++ b/src/customElement.js @@ -1,30 +1,40 @@ +import { keyLTE } from "./dom-common.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: custom_element, - host: (...c)=> c.length ? c.forEach(c=> c(custom_element)) : custom_element, - custom_element + host: (...c)=> c.length ? c.forEach(c=> c(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); + 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(); - return out; + return target.append(out); } export function lifecycleToEvents(class_declaration){ - for (const name of [ "connected", "disconnected" ]) - wrapMethod(class_declaration.prototype, name+"Callback", function(target, thisArg, detail){ - target.apply(thisArg, detail); - thisArg.dispatchEvent(new Event("dde:"+name)); - }); - const name= "attributeChanged"; - wrapMethod(class_declaration.prototype, name+"Callback", function(target, thisArg, detail){ + wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail){ + target.apply(thisArg, detail); + thisArg.dispatchEvent(new Event("dde:connected")); + }); + wrapMethod(class_declaration.prototype, "disconnectedCallback", 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; - thisArg.dispatchEvent(new CustomEvent("dde:"+name, { + thisArg.dispatchEvent(new CustomEvent("dde:attributeChanged", { detail: [ attribute, value ] })); target.apply(thisArg, detail); }); - class_declaration.prototype.__dde_lifecycleToEvents= true; + class_declaration.prototype[keyLTE]= true; return class_declaration; } export { lifecycleToEvents as customElementWithDDE }; @@ -35,4 +45,4 @@ function wrapMethod(obj, method, apply){ import { observedAttributes as oA } from "./helpers.js"; export function observedAttributes(instance){ return oA(instance, (i, n)=> i.getAttribute(n)); -} +} \ No newline at end of file diff --git a/src/dom-common.js b/src/dom-common.js index fa7d2a0..5abb74e 100644 --- a/src/dom-common.js +++ b/src/dom-common.js @@ -26,3 +26,4 @@ function setDeleteAttr(obj, prop, val){ if(Reflect.get(obj, prop)==="undefined") return Reflect.set(obj, prop, ""); } +export const keyLTE= "__dde_lifecycleToEvents"; //boolean \ No newline at end of file diff --git a/src/dom.js b/src/dom.js index a65354b..a2756ab 100644 --- a/src/dom.js +++ b/src/dom.js @@ -5,7 +5,6 @@ import { enviroment as env } from './dom-common.js'; const scopes= [ { get scope(){ return env.D.body; }, host: c=> c ? c(env.D.body) : env.D.body, - custom_element: false, prevent: true, } ]; export const scope= { diff --git a/src/events-observer.js b/src/events-observer.js new file mode 100644 index 0000000..a4d7376 --- /dev/null +++ b/src/events-observer.js @@ -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); + }; + } +} diff --git a/src/events.js b/src/events.js index 0d6cbd3..de42896 100644 --- a/src/events.js +++ b/src/events.js @@ -1,5 +1,5 @@ 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){ if(!options) options= {}; @@ -19,13 +19,12 @@ export function on(event, listener, options){ }; } -const c_ch_o= env.M ? connectionsChangesObserverConstructor() : new Proxy({}, { - get(){ return ()=> {}; } -}); +import { c_ch_o } from "./events-observer.js"; const els_attribute_store= new WeakSet(); import { scope } from "./dom.js"; import { onAbort } from './helpers.js'; //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){ const { custom_element }= scope.current; const name= "connected"; @@ -36,7 +35,7 @@ on.connected= function(listener, options){ if(custom_element) element= custom_element; const event= "dde:"+name; 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 ); 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; const event= "dde:"+name; 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)); if(c) c_ch_o.onDisconnected(element, listener); @@ -77,7 +76,7 @@ on.attributeChanged= function(listener, options){ return function registerElement(element){ const event= "dde:"+name; element.addEventListener(event, listener, options); - if(element.__dde_lifecycleToEvents || els_attribute_store.has(element)) + if(element[keyLTE] || els_attribute_store.has(element)) return element; if(!env.M) return element; @@ -92,128 +91,4 @@ on.attributeChanged= function(listener, options){ //TODO: clean up when element disconnected 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; - } -} +}; \ No newline at end of file diff --git a/src/observables-lib.js b/src/observables-lib.js index 21538f9..765a0a3 100644 --- a/src/observables-lib.js +++ b/src/observables-lib.js @@ -96,7 +96,7 @@ observable.el= function(o, map){ out.append(mark_start, mark_end); const { current }= scope; const reRenderReactiveElement= v=> { - if(!mark_start.parentNode || !mark_end.parentNode) + if(!mark_start.parentNode || !mark_end.parentNode) // isConnected or wasn’t yet rendered return removeObservableListener(o, reRenderReactiveElement); scope.push(current); let els= map(v); @@ -107,12 +107,21 @@ observable.el= function(o, map){ while(( el_r= mark_start.nextSibling ) !== mark_end) el_r.remove(); mark_start.after(...els); + if(mark_start.isConnected) + requestCleanUpReactives(current.host()); }; addObservableListener(o, reRenderReactiveElement); removeObservablesFromElements(o, reRenderReactiveElement, mark_start, map); reRenderReactiveElement(o()); 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 { observedAttributes } from "./helpers.js"; const observedAttributeActions= { @@ -152,7 +161,11 @@ export const observables_config= { isObservable, processReactiveAttribute(element, key, attrs, set){ 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); removeObservablesFromElements(attrs, l, element, key); return attrs(); @@ -256,4 +269,4 @@ function removeObservableListener(o, listener, clear_when_empty){ deps.get(c).forEach(sig=> removeObservableListener(sig, c, true)); } return out; -} +} \ No newline at end of file