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

💥 rename signals to observables

This commit is contained in:
2023-11-24 20:41:04 +01:00
parent fc4037f3eb
commit 58b73dcbc7
44 changed files with 372 additions and 333 deletions

View File

@ -1,4 +1,4 @@
import { signals } from "./signals-common.js";
import { observables } from "./observables-common.js";
import { enviroment } from './dom-common.js';
/** @type {{ scope: object, prevent: boolean, host: function }[]} */
@ -32,11 +32,11 @@ export function chainableAppend(el){ if(el.append===append) return el; el.append
let namespace;
export function createElement(tag, attributes, ...addons){
/* jshint maxcomplexity: 15 */
const s= signals(this);
const s= observables(this);
let scoped= 0;
let el, el_host;
//TODO Array.isArray(tag) ⇒ set key (cache els)
if(Object(attributes)!==attributes || s.isSignal(attributes))
if(Object(attributes)!==attributes || s.isObservable(attributes))
attributes= { textContent: attributes };
switch(true){
case typeof tag==="function": {
@ -168,11 +168,11 @@ function assignContext(element, _this){
if(assign_context.has(element)) return assign_context.get(element);
const is_svg= element instanceof SVGElement;
const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
const s= signals(_this);
const s= observables(_this);
return { setRemoveAttr, s };
}
export function classListDeclarative(element, toggle){
const s= signals(this);
const s= observables(this);
forEachEntries(s, toggle,
(class_name, val)=>
element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val)));

View File

@ -1,4 +1,4 @@
export { registerReactivity } from './signals-common.js';
export { registerReactivity } from './observables-common.js';
export function dispatchEvent(name, options, host){
if(!options) options= {};

13
src/observables-common.js Normal file
View File

@ -0,0 +1,13 @@
export const observables_global= {
isObservable(attributes){ return false; },
processReactiveAttribute(obj, key, attr, set){ return attr; },
};
export function registerReactivity(def, global= true){
if(global) return Object.assign(observables_global, def);
Object.setPrototypeOf(def, observables_global);
return def;
}
/** @param {unknown} _this @returns {typeof observables_global} */
export function observables(_this){
return observables_global.isPrototypeOf(_this) && _this!==observables_global ? _this : observables_global;
}

View File

@ -1,25 +1,25 @@
export const mark= Symbol.for("Signal");
export const mark= Symbol.for("observable");
export function isSignal(candidate){
export function isObservable(candidate){
try{ return Reflect.has(candidate, mark); }
catch(e){ return false; }
}
/** @type {function[]} */
const stack_watch= [];
/**
* ### `WeakMap<function, Set<ddeSignal<any, any>>>`
* The `Set` is in the form of `[ source, ...depended signals (DSs) ]`.
* ### `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,
* 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`.
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|function>}
* @type {WeakMap<function|object,Set<ddeObservable<any, any>>|function>}
* */
const deps= new WeakMap();
export function S(value, actions){
export function observable(value, actions){
if(typeof value!=="function")
return create(value, actions);
if(isSignal(value)) return value;
if(isObservable(value)) return value;
const out= create();
const contextReWatch= function(){
@ -32,9 +32,9 @@ export function S(value, actions){
if(!deps_old.length) return;
const deps_curr= deps.get(contextReWatch);
for (const dep_signal of deps_old){
if(deps_curr.has(dep_signal)) continue;
removeSignalListener(dep_signal, contextReWatch);
for (const dep_observable of deps_old){
if(deps_curr.has(dep_observable)) continue;
removeObservableListener(dep_observable, contextReWatch);
}
};
deps.set(out[mark], contextReWatch);
@ -42,44 +42,45 @@ export function S(value, actions){
contextReWatch();
return out;
}
S.action= function(signal, name, ...a){
const s= signal[mark], { actions }= s;
export { observable as O };
observable.action= function(observable, name, ...a){
const s= observable[mark], { actions }= s;
if(!actions || !Reflect.has(actions, name))
throw new Error(`'${signal}' has no action with name '${name}'!`);
throw new Error(`'${observable}' has no action with name '${name}'!`);
actions[name].apply(s, a);
if(s.skip) return Reflect.deleteProperty(s, "skip");
s.listeners.forEach(l=> l(s.value));
};
S.on= function on(signals, listener, options= {}){
const { signal: as }= options;
observable.on= function on(observables, listener, options= {}){
const { observable: as }= options;
if(as && as.aborted) return;
if(Array.isArray(signals)) return signals.forEach(s=> on(s, listener, options));
addSignalListener(signals, listener);
if(as) as.addEventListener("abort", ()=> removeSignalListener(signals, listener));
//TODO cleanup when signal removed
if(Array.isArray(observables)) return observables.forEach(s=> on(s, listener, options));
addObservableListener(observables, listener);
if(as) as.addEventListener("abort", ()=> removeObservableListener(observables, listener));
//TODO cleanup when observable removed
};
S.symbols= {
signal: mark,
onclear: Symbol.for("Signal.onclear")
observable.symbols= {
observable: mark,
onclear: Symbol.for("Observable.onclear")
};
S.clear= function(...signals){
for(const signal of signals){
Reflect.deleteProperty(signal, "toJSON");
const s= signal[mark];
observable.clear= function(...observables){
for(const observable of observables){
Reflect.deleteProperty(observable, "toJSON");
const s= observable[mark];
s.onclear.forEach(f=> f.call(s));
clearListDeps(signal, s);
Reflect.deleteProperty(signal, mark);
clearListDeps(observable, s);
Reflect.deleteProperty(observable, mark);
}
function clearListDeps(signal, s){
function clearListDeps(observable, s){
s.listeners.forEach(l=> {
s.listeners.delete(l);
if(!deps.has(l)) return;
const ls= deps.get(l);
ls.delete(signal);
ls.delete(observable);
if(ls.size>1) return;
S.clear(...ls);
observable.clear(...ls);
deps.delete(l);
});
}
@ -87,7 +88,7 @@ S.clear= function(...signals){
const key_reactive= "__dde_reactive";
import { el, elementAttribute } from "./dom.js";
import { scope } from "./dom.js";
S.el= function(signal, map){
observable.el= function(observable, map){
const mark_start= el.mark({ type: "reactive" }, false);
const mark_end= mark_start.end;
const out= document.createDocumentFragment();
@ -95,7 +96,7 @@ S.el= function(signal, map){
const { current }= scope;
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode)
return removeSignalListener(signal, reRenderReactiveElement);
return removeObservableListener(observable, reRenderReactiveElement);
scope.push(current);
let els= map(v);
scope.pop();
@ -106,16 +107,16 @@ S.el= function(signal, map){
el_r.remove();
mark_start.after(...els);
};
addSignalListener(signal, reRenderReactiveElement);
removeSignalsFromElements(signal, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(signal());
addObservableListener(observable, reRenderReactiveElement);
removeObservablesFromElements(observable, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(observable());
return out;
};
import { on } from "./events.js";
const key_attributes= "__dde_attributes";
S.attribute= function(name, initial= null){
observable.attribute= function(name, initial= null){
//TODO host=element & reuse existing
const out= S(initial);
const out= observable(initial);
let element;
scope.host(el=> {
element= el;
@ -127,17 +128,17 @@ S.attribute= function(name, initial= null){
return;
}
element[key_attributes]= { [name]: out };
on.attributeChanged(function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.attribute`).
on.attributeChanged(function attributeChangeToObservable({ detail }){
/*! This maps attributes to observables (`S.attribute`).
* Investigate `__dde_attributes` key of the element.*/
const [ name, value ]= detail;
const curr= element[key_attributes][name];
if(curr) return curr(value);
})(element);
on.disconnected(function(){
/*! This removes all signals mapped to attributes (`S.attribute`).
/*! This removes all observables mapped to attributes (`S.attribute`).
* Investigate `__dde_attributes` key of the element.*/
S.clear(...Object.values(element[key_attributes]));
observable.clear(...Object.values(element[key_attributes]));
})(element);
});
return new Proxy(out, {
@ -150,17 +151,17 @@ S.attribute= function(name, initial= null){
};
import { typeOf } from './helpers.js';
export const signals_config= {
isSignal,
export const observables_config= {
isObservable,
processReactiveAttribute(element, key, attrs, set){
if(!isSignal(attrs)) return attrs;
if(!isObservable(attrs)) return attrs;
const l= attr=> set(key, attr);
addSignalListener(attrs, l);
removeSignalsFromElements(attrs, l, element, key);
addObservableListener(attrs, l);
removeObservablesFromElements(attrs, l, element, key);
return attrs();
}
};
function removeSignalsFromElements(signal, listener, ...notes){
function removeObservablesFromElements(observable, listener, ...notes){
const { current }= scope;
if(current.prevent) return;
current.host(function(element){
@ -168,28 +169,28 @@ function removeSignalsFromElements(signal, listener, ...notes){
element[key_reactive]= [];
on.disconnected(()=>
/*!
* Clears all signals listeners added in the current scope/host (`S.el`, `assign`, ?).
* Clears all Observables listeners added in the current scope/host (`S.el`, `assign`, ?).
* You can investigate the `__dde_reactive` key of the element.
* */
element[key_reactive].forEach(([ [ signal, listener ] ])=>
removeSignalListener(signal, listener, signal[mark]?.host() === element))
element[key_reactive].forEach(([ [ observable, listener ] ])=>
removeObservableListener(observable, listener, observable[mark]?.host() === element))
)(element);
}
element[key_reactive].push([ [ signal, listener ], ...notes ]);
element[key_reactive].push([ [ observable, listener ], ...notes ]);
});
}
function create(value, actions){
const signal= (...value)=>
value.length ? write(signal, ...value) : read(signal);
return toSignal(signal, value, actions);
const observable= (...value)=>
value.length ? write(observable, ...value) : read(observable);
return toObservable(observable, value, actions);
}
const protoSigal= Object.assign(Object.create(null), {
stopPropagation(){
this.skip= true;
}
});
class SignalDefined extends Error{
class ObservableDefined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
@ -197,64 +198,64 @@ class SignalDefined extends Error{
this.stack= rest.find(l=> !l.includes(curr_file));
}
}
function toSignal(signal, value, actions){
function toObservable(observable, value, actions){
const onclear= [];
if(typeOf(actions)!=="[object Object]")
actions= {};
const { onclear: ocs }= S.symbols;
const { onclear: ocs }= observable.symbols;
if(actions[ocs]){
onclear.push(actions[ocs]);
Reflect.deleteProperty(actions, ocs);
}
const { host }= scope;
Reflect.defineProperty(signal, mark, {
Reflect.defineProperty(observable, mark, {
value: {
value, actions, onclear, host,
listeners: new Set(),
defined: new SignalDefined()
defined: new ObservableDefined()
},
enumerable: false,
writable: false,
configurable: true
});
signal.toJSON= ()=> signal();
Object.setPrototypeOf(signal[mark], protoSigal);
return signal;
observable.toJSON= ()=> observable();
Object.setPrototypeOf(observable[mark], protoSigal);
return observable;
}
function currentContext(){
return stack_watch[stack_watch.length - 1];
}
function read(signal){
if(!signal[mark]) return;
const { value, listeners }= signal[mark];
function read(observable){
if(!observable[mark]) return;
const { value, listeners }= observable[mark];
const context= currentContext();
if(context) listeners.add(context);
if(deps.has(context)) deps.get(context).add(signal);
if(deps.has(context)) deps.get(context).add(observable);
return value;
}
function write(signal, value, force){
if(!signal[mark]) return;
const s= signal[mark];
function write(observable, value, force){
if(!observable[mark]) return;
const s= observable[mark];
if(!force && s.value===value) return;
s.value= value;
s.listeners.forEach(l=> l(value));
return value;
}
function addSignalListener(signal, listener){
if(!signal[mark]) return;
return signal[mark].listeners.add(listener);
function addObservableListener(observable, listener){
if(!observable[mark]) return;
return observable[mark].listeners.add(listener);
}
function removeSignalListener(signal, listener, clear_when_empty){
const s= signal[mark];
function removeObservableListener(observable, listener, clear_when_empty){
const s= observable[mark];
if(!s) return;
const out= s.listeners.delete(listener);
if(clear_when_empty && !s.listeners.size){
S.clear(signal);
observable.clear(observable);
if(!deps.has(s)) return out;
const c= deps.get(s);
if(!deps.has(c)) return out;
deps.get(c).forEach(sig=> removeSignalListener(sig, c, true));
deps.get(c).forEach(sig=> removeObservableListener(sig, c, true));
}
return out;
}

View File

@ -1,13 +0,0 @@
export const signals_global= {
isSignal(attributes){ return false; },
processReactiveAttribute(obj, key, attr, set){ return attr; },
};
export function registerReactivity(def, global= true){
if(global) return Object.assign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
/** @param {unknown} _this @returns {typeof signals_global} */
export function signals(_this){
return signals_global.isPrototypeOf(_this) && _this!==signals_global ? _this : signals_global;
}