mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-01 20:32:13 +02:00
45
src/dom.js
45
src/dom.js
@ -8,13 +8,15 @@ export function namespace(namespace){
|
||||
};
|
||||
}
|
||||
export function createElement(tag, attributes, ...connect){
|
||||
const s= signals(this);
|
||||
let el;
|
||||
//TODO Array.isArray(tag) ⇒ set key (cache els)
|
||||
if("<>"===tag){
|
||||
if(signals.isReactiveAtrribute(attributes))
|
||||
return signals.reactiveElement(attributes, ...connect);
|
||||
if(s.isReactiveAtrribute(attributes))
|
||||
return s.reactiveElement(attributes, ...connect);
|
||||
el= document.createDocumentFragment();
|
||||
}
|
||||
if(signals.isTextContent(attributes))
|
||||
if(s.isTextContent(attributes))
|
||||
attributes= { textContent: attributes };
|
||||
switch(true){
|
||||
case typeof tag==="function": el= tag(attributes || undefined); break;
|
||||
@ -27,16 +29,19 @@ export function createElement(tag, attributes, ...connect){
|
||||
}
|
||||
export { createElement as el };
|
||||
|
||||
const prop_cache= new Map();
|
||||
export function assign(element, ...attributes){
|
||||
const s= signals(this);
|
||||
if(!attributes.length) return element;
|
||||
const is_svg= element instanceof SVGElement;
|
||||
const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
|
||||
|
||||
Object.entries(Object.assign({}, ...attributes)).forEach(function assignNth([ key, attr ]){
|
||||
if(signals.isReactiveAtrribute(attr, key))
|
||||
attr= signals.process(key, attr, assignNth);
|
||||
if(key[0]==="=") return setRemoveAttr(key.slice(1), attr);
|
||||
if(key[0]===".") return setDelete(element, key.slice(1), attr);
|
||||
if(s.isReactiveAtrribute(attr, key))
|
||||
attr= s.processReactiveAttribute(el, key, attr, assignNth);
|
||||
const [ k ]= key;
|
||||
if("="===k) return setRemoveAttr(key.slice(1), attr);
|
||||
if("."===k) return setDelete(element, key.slice(1), attr);
|
||||
if(typeof attr === "object"){
|
||||
switch(key){
|
||||
case "style": return forEachEntries(attr, setRemove.bind(null, element.style, "Property"))
|
||||
@ -51,17 +56,13 @@ export function assign(element, ...attributes){
|
||||
return setRemoveAttr(key, attr);
|
||||
}
|
||||
switch(key){
|
||||
case "href" || "src" || "style":
|
||||
return setRemoveAttr(key, attr);
|
||||
case "xlink:href":
|
||||
return setRemoveAttr(key, attr, "http://www.w3.org/1999/xlink");
|
||||
case "textContent" || "innerText":
|
||||
if(!is_svg) break;
|
||||
return element.appendChild(document.createTextNode(attr));
|
||||
}
|
||||
if(key in element && !is_svg)
|
||||
return setDelete(element, key, attr);
|
||||
return setRemoveAttr(key, attr);
|
||||
return isPropSetter(element, key) ? setDelete(element, key, attr) : setRemoveAttr(key, attr);
|
||||
});
|
||||
return element;
|
||||
}
|
||||
@ -73,8 +74,24 @@ export function classListDeclartive(element, toggle){
|
||||
element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val)))
|
||||
return element;
|
||||
}
|
||||
|
||||
export function empty(el){ Array.from(el.children).forEach(el=> el.remove()); return el; }
|
||||
export function empty(el){
|
||||
Array.from(el.children).forEach(el=> el.remove());
|
||||
return el;
|
||||
}
|
||||
function isPropSetter(el, key){
|
||||
const cache_key= el.nodeName+","+key;
|
||||
if(prop_cache.has(cache_key)) return prop_cache.get(cache_key);
|
||||
const des= getPropDescriptor(el, key);
|
||||
const is_set= !isUndef(des.set);
|
||||
prop_cache.set(cache_key, is_set);
|
||||
return is_set;
|
||||
}
|
||||
function getPropDescriptor(p, key){
|
||||
p= Object.getPrototypeOf(p);
|
||||
if(!p) return {};
|
||||
const des= Object.getOwnPropertyDescriptor(p, key);
|
||||
return des ? des : getPropDescriptor(p, key);
|
||||
}
|
||||
|
||||
function forEachEntries(obj, cb){ return Object.entries(obj).forEach(([ key, val ])=> cb(key, val)); }
|
||||
function isUndef(value){ return typeof value==="undefined"; }
|
||||
|
@ -1,37 +1,25 @@
|
||||
import { signals } from './signals-common.js';
|
||||
export { registerReactivity } from './signals-common.js';
|
||||
|
||||
export function on(event, listener, options){
|
||||
if(!signals.isReactiveAtrribute(event))
|
||||
return element=> element.addEventListener(event, listener, options);
|
||||
//TODO cleanup when signal removed (also TODO)
|
||||
if(options && options.signal)
|
||||
options.signal.addEventListener("abort", ()=> signals.off(event, listener));
|
||||
return signals.on(event, listener);
|
||||
}
|
||||
export function off(){//TODO is needed?
|
||||
const abort= new AbortController();
|
||||
return new Proxy(()=> abort.abort(), {
|
||||
get(_, p){ return Reflect.get(abort, p); },
|
||||
ownKeys(){ return [ "signal" ]; }
|
||||
})
|
||||
}
|
||||
export function dispatch(event, detail){
|
||||
if(typeof event === "string")
|
||||
event= typeof detail==="undefined" ? new Event(event) : new CustomEvent(event, { detail });
|
||||
return element=> element.dispatchEvent(event);
|
||||
return element=> {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
|
||||
const connections_changes= connectionsChangesObserverConstructor();
|
||||
//TODO remove listener(s) options
|
||||
on.connected= function(listener){
|
||||
on.connected= function(listener, options){
|
||||
return function registerElement(element){
|
||||
connections_changes.onConnected(element, listener);
|
||||
if(options && options.signal)
|
||||
options.signal.addEventListener("abort", ()=> connections_changes.offConnected(element, listener));
|
||||
};
|
||||
};
|
||||
on.disconnected= function(listener){
|
||||
on.disconnected= function(listener, options){
|
||||
return function registerElement(element){
|
||||
connections_changes.onDisconnected(element, listener);
|
||||
if(options && options.signal)
|
||||
options.signal.addEventListener("abort", ()=> connections_changes.offDisconnected(element, listener));
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { typeOf } from './helpers.js';
|
||||
export const signals= {
|
||||
export const signals_global= {
|
||||
isReactiveAtrribute(attr, key){ return false; },
|
||||
isTextContent(attributes){ return typeOf(attributes)!=="[object Object]"; },
|
||||
process(key, attr, assignNth){ return false; },
|
||||
on(signal, listener){ return false; },
|
||||
off(signal, listener){ return false; },
|
||||
processReactiveAttribute(el, key, attr, assignNth){ return false; },
|
||||
reactiveElement(attributes, ...connect){ return document.createDocumentFragment(); }
|
||||
};
|
||||
export function registerReactivity(def){
|
||||
return Object.assign(signals, def);
|
||||
export function registerReactivity(def, global= true){
|
||||
if(global) return Object.assign(signals_global, def);
|
||||
Object.setPrototypeOf(def, signals_global);
|
||||
return def;
|
||||
}
|
||||
export function signals(_this){
|
||||
return signals_global.isPrototypeOf(_this) && _this!==signals_global ? _this : signals_global;
|
||||
}
|
||||
|
131
src/signals-lib.js
Normal file
131
src/signals-lib.js
Normal file
@ -0,0 +1,131 @@
|
||||
export const mark= Symbol.for("signal");
|
||||
|
||||
export function isSignal(candidate){
|
||||
try{ return Reflect.has(candidate, mark); }
|
||||
catch(e){ return false; }
|
||||
}
|
||||
export function S(value, actions){
|
||||
if(typeof value!=="function")
|
||||
return create(value, actions);
|
||||
if(isSignal(value)) return value;
|
||||
|
||||
const out= create();
|
||||
watch(()=> out(value()));
|
||||
return out;
|
||||
}
|
||||
S.action= function(signal, name, ...a){
|
||||
if(!isSignal(signal))
|
||||
throw new Error(`'${signal}' is not a signal!`);
|
||||
const s= signal[mark], { actions }= s;
|
||||
if(!actions || !Reflect.has(actions, name))
|
||||
throw new Error(`'${signal}' 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){
|
||||
if(Array.isArray(signals)) return signals.forEach(s=> on(s, listener, options));
|
||||
addSignalListener(signals, listener);
|
||||
if(options && options.signal)
|
||||
options.signal.addEventListener("abort", ()=> removeSignalListener(signals, listener));
|
||||
//TODO cleanup when signal removed (also TODO)
|
||||
}
|
||||
S.clear= function(...signals){
|
||||
for(const signal of signals){
|
||||
signal[mark].listeners.clear();
|
||||
Reflect.deleteProperty(signal, mark);
|
||||
}
|
||||
};
|
||||
|
||||
import { typeOf } from './helpers.js';
|
||||
export const signals_config= {
|
||||
isReactiveAtrribute(attr, key){ return isSignal(attr); },
|
||||
isTextContent(attributes){
|
||||
return typeOf(attributes)==="string" || ( isSignal(attributes) && typeOf(valueOfSignal(attributes))==="string" );
|
||||
},
|
||||
processReactiveAttribute(_, key, attrS, assignNth){
|
||||
addSignalListener(attrS, attr=> assignNth([ key, attr ]));
|
||||
return attrS();
|
||||
},
|
||||
reactiveElement(signal, map){
|
||||
const mark_start= document.createComment("<> #reactive");
|
||||
const mark_end= document.createComment("</> #reactive");
|
||||
const out= document.createDocumentFragment();
|
||||
out.append(mark_start, mark_end);
|
||||
const toEls= v=> {
|
||||
if(!mark_start.parentNode || !mark_end.parentNode)
|
||||
return removeSignalListener(signal, toEls);
|
||||
let els= map(v);
|
||||
if(!Array.isArray(els))
|
||||
els= [ els ];
|
||||
let el_r= mark_start;
|
||||
while(( el_r= mark_start.nextSibling ) !== mark_end)
|
||||
el_r.remove();
|
||||
mark_start.after(...els);
|
||||
};
|
||||
addSignalListener(signal, toEls);
|
||||
toEls(signal());
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
function create(value, actions){
|
||||
const signal= (...value)=>
|
||||
value.length ? write(signal, value[0]) : read(signal);
|
||||
return toSignal(signal, value, actions);
|
||||
}
|
||||
const protoSigal= Object.assign(Object.create(null), {
|
||||
stopPropagation(){
|
||||
this.skip= true;
|
||||
}
|
||||
});
|
||||
function toSignal(signal, value, actions){
|
||||
if(typeOf(actions)!=="[object Object]")
|
||||
actions= {};
|
||||
signal[mark]= {
|
||||
value, actions,
|
||||
listeners: new Set(),
|
||||
};
|
||||
Object.setPrototypeOf(signal[mark], protoSigal);
|
||||
return signal;
|
||||
}
|
||||
|
||||
const stack_watch= [];
|
||||
export function watch(context){
|
||||
const contextReWatch= function(){
|
||||
stack_watch.push(contextReWatch);
|
||||
context();
|
||||
stack_watch.pop();
|
||||
};
|
||||
stack_watch.push(contextReWatch);
|
||||
context();
|
||||
stack_watch.pop();
|
||||
};
|
||||
function currentContext(){
|
||||
return stack_watch[stack_watch.length - 1];
|
||||
}
|
||||
function read(signal){
|
||||
if(!signal[mark]) return;
|
||||
const { value, listeners }= signal[mark];
|
||||
const context= currentContext();
|
||||
if(context) listeners.add(context);
|
||||
return value;
|
||||
}
|
||||
function write(signal, value){
|
||||
if(!signal[mark]) return;
|
||||
const s= signal[mark];
|
||||
if(s.value===value) return;
|
||||
s.value= value;
|
||||
s.listeners.forEach(fn=> fn(value))
|
||||
return value;
|
||||
}
|
||||
|
||||
function valueOfSignal(signal){
|
||||
return signal[mark].value;
|
||||
}
|
||||
function addSignalListener(signal, listener){
|
||||
return signal[mark].listeners.add(listener);
|
||||
}
|
||||
function removeSignalListener(signal, listener){
|
||||
return signal[mark].listeners.delete(listener);
|
||||
}
|
15
src/signals.d.ts
vendored
Normal file
15
src/signals.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
export type Signal<V, A>= (set?: V)=> V & A;
|
||||
type Action<V>= (this: { value: V }, ...a: any[])=> typeof S._ | void;
|
||||
type Actions<V>= Record<string, Action<V>>;
|
||||
interface S {
|
||||
_: Symbol
|
||||
<V, A extends Actions<V>>(value: V, actions?: A): Signal<V, A>;
|
||||
action<S extends Signal<any, Actions<any>>, A extends (S extends Signal<any, infer A> ? A : never), N extends keyof A>(
|
||||
signal: S,
|
||||
name: N,
|
||||
...params: A[N] extends (...args: infer P)=> any ? P : never
|
||||
): void;
|
||||
clear(...signals: Signal<any, any>): void;
|
||||
on<T>(signal: Signal<T, any>, onchange: (a: T)=> void, options?: AddEventListenerOptions): void
|
||||
}
|
||||
export const S: S;
|
155
src/signals.js
155
src/signals.js
@ -1,153 +1,4 @@
|
||||
export const mark= Symbol.for("signal");
|
||||
|
||||
export function isSignal(candidate){
|
||||
try{ return Reflect.has(candidate, mark); }
|
||||
catch(e){ return false; }
|
||||
}
|
||||
import { typeOf } from './helpers.js';
|
||||
export { S, isSignal, watch } from "./signals-lib.js";
|
||||
import { signals_config } from "./signals-lib.js";
|
||||
import { registerReactivity } from "./signals-common.js";
|
||||
registerReactivity({
|
||||
isReactiveAtrribute(attr, key){ return isSignal(attr); },
|
||||
isTextContent(attributes){
|
||||
//TODO FIX el(…, S.reactive(…))
|
||||
return typeOf(attributes)!=="[object Object]" || ( isSignal(attributes) && typeOf(valueOfSignal(attributes))!=="[object Object]" );
|
||||
},
|
||||
process(key, attr, assignNth){ //TODO: unmounted
|
||||
addSignalListener(attr, attr=> assignNth([ key, attr ]));
|
||||
return attr();
|
||||
},
|
||||
on: addSignalListener,
|
||||
off: removeSignalListener,
|
||||
reactiveElement(signal, map){
|
||||
const mark= document.createComment("reactive");
|
||||
const out= document.createDocumentFragment();
|
||||
out.append(mark);
|
||||
let cache;
|
||||
const toEls= v=> {
|
||||
let els= map(v);
|
||||
if(!Array.isArray(els))
|
||||
els= [ els ];
|
||||
if(cache) cache.forEach(el=> el.remove());
|
||||
cache= els;
|
||||
mark.before(...els);
|
||||
};
|
||||
addSignalListener(signal, toEls);
|
||||
toEls(signal());
|
||||
return out;
|
||||
}
|
||||
});
|
||||
|
||||
export function S(value){
|
||||
if(typeof value!=="function")
|
||||
return create(value);
|
||||
if(isSignal(value)) return value;
|
||||
|
||||
const out= create();
|
||||
watch(()=> out(value()));
|
||||
return out;
|
||||
}
|
||||
S.reactive= reactive;
|
||||
function reactive(data){
|
||||
if(isSignal(data))
|
||||
return data;
|
||||
if(typeof data!=="object" || data===null)
|
||||
return create(data);
|
||||
|
||||
let type;
|
||||
if(Array.isArray(data)){
|
||||
type= "array";
|
||||
data= data.map(v=> reactive(v));
|
||||
} else if(data.toString()!=="[object Object]"){
|
||||
return create(data);
|
||||
} else {
|
||||
type= "object";
|
||||
data= Object.fromEntries(
|
||||
Object.entries(data)
|
||||
.map(([ key, value ])=> [ key, reactive(value) ])
|
||||
);
|
||||
}
|
||||
const signal= (...value)=>
|
||||
value.length ? write(signal, reactive(value[0])) : read(signal[mark]);
|
||||
return createWrapObject(type, toSignal(signal, data));
|
||||
};
|
||||
function toSignal(signal, value){
|
||||
signal[mark]= {
|
||||
value,
|
||||
listeners: new Set()
|
||||
};
|
||||
return signal;
|
||||
}
|
||||
|
||||
const stack= [];
|
||||
export function watch(context){
|
||||
stack.push(function contextReWatch(){
|
||||
stack.push(contextReWatch);
|
||||
context();
|
||||
stack.pop();
|
||||
});
|
||||
context();
|
||||
stack.pop();
|
||||
};
|
||||
|
||||
function currentContext(){
|
||||
return stack[stack.length - 1];
|
||||
}
|
||||
function create(value){
|
||||
if(isSignal(value)) return value;
|
||||
const signal= (...value)=>
|
||||
value.length ? write(signal, value[0]) : read(signal[mark]);
|
||||
return toSignal(signal, value);
|
||||
}
|
||||
function createWrapObject(type, signal){
|
||||
return new Proxy(signal, {
|
||||
set(_, p, newValue){
|
||||
const s= signal[mark];
|
||||
if(p in s.value){
|
||||
const v= s.value[p];
|
||||
if(isSignal(v)) return v(newValue);
|
||||
return (s.value[p]= newValue);
|
||||
}
|
||||
const v= reactive(newValue);
|
||||
s.value[p]= v;
|
||||
s.listeners.forEach(fn=> fn(s.value));
|
||||
return v;
|
||||
},
|
||||
deleteProperty(_, p){
|
||||
const s= signal[mark];
|
||||
Reflect.deleteProperty(s.value, p);
|
||||
s.listeners.forEach(fn=> fn(s.value));
|
||||
},
|
||||
get(_, p){
|
||||
if(mark===p) return signal[mark];
|
||||
if("array"!==type || !(p in Array.prototype) || p==="length")
|
||||
return Reflect.get(signal[mark].value, p);
|
||||
return (...a)=> {
|
||||
const s= signal[mark];
|
||||
const result= Array.prototype[p].call(s.value, ...a);
|
||||
//TODO optimize!
|
||||
s.value.forEach((v, i)=> Reflect.set(s.value, i, reactive(v)));
|
||||
s.listeners.forEach(fn=> fn(s.value));
|
||||
return result;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
function read({ value, listeners }){
|
||||
const context= currentContext();
|
||||
if(context) listeners.add(context);
|
||||
return value;
|
||||
}
|
||||
function write(signal, value){
|
||||
signal[mark].value= value;
|
||||
signal[mark].listeners.forEach(fn=> fn(value))
|
||||
return value;
|
||||
}
|
||||
function valueOfSignal(signal){
|
||||
return signal[mark].value;
|
||||
}
|
||||
export function addSignalListener(signal, listener){
|
||||
return signal[mark].listeners.add(listener);
|
||||
}
|
||||
export function removeSignalListener(signal, listener){
|
||||
return signal[mark].listeners.delete(listener);
|
||||
}
|
||||
registerReactivity(signals_config);
|
||||
|
Reference in New Issue
Block a user