1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2024-11-24 17:39:36 +01:00

💥 Signals are now optional + reactive element

- `registerReactivity` can be used to register custom behavior
- Signals are automatically registered when `signals.js` is imported;
- `el("<>", signal, map)`
This commit is contained in:
Jan Andrle 2023-08-26 17:32:58 +02:00
parent a36786042b
commit f5adefbc9c
Signed by: jaandrle
GPG Key ID: B3A25AED155AFFAB
6 changed files with 86 additions and 58 deletions

View File

@ -4,4 +4,3 @@
}); });
export * from "./src/dom.js"; export * from "./src/dom.js";
export * from "./src/events.js"; export * from "./src/events.js";
export * from "./src/signals.js";

View File

@ -1,3 +1,5 @@
import { signals } from "./signals-common.js";
let namespace_curr= "html"; let namespace_curr= "html";
export function namespace(namespace){ export function namespace(namespace){
namespace_curr= namespace==="svg" ? "http://www.w3.org/2000/svg" : namespace; namespace_curr= namespace==="svg" ? "http://www.w3.org/2000/svg" : namespace;
@ -5,15 +7,17 @@ export function namespace(namespace){
append(el){ namespace_curr= "html"; return el; } append(el){ namespace_curr= "html"; return el; }
}; };
} }
import { typeOf } from './helpers.js';
import { isSignal, valueOfSignal } from './signals-common.js';
export function createElement(tag, attributes, ...connect){ export function createElement(tag, attributes, ...connect){
if(typeOf(attributes)!=="[object Object]" || ( isSignal(attributes) && typeOf(valueOfSignal(attributes))!=="[object Object]" ))
attributes= { textContent: attributes };
let el; let el;
if("<>"===tag){
if(signals.isReactiveAtrribute(attributes))
return signals.reactiveElement(attributes, ...connect);
el= document.createDocumentFragment();
}
if(signals.isTextContent(attributes))
attributes= { textContent: attributes };
switch(true){ switch(true){
case typeof tag==="function": el= tag(attributes || undefined); break; case typeof tag==="function": el= tag(attributes || undefined); break;
case tag==="<>": el= document.createDocumentFragment(); break;
case tag==="#text": el= assign(document.createTextNode(""), attributes); break; case tag==="#text": el= assign(document.createTextNode(""), attributes); break;
case namespace_curr!=="html": el= assign(document.createElementNS(namespace_curr, tag), attributes); break; case namespace_curr!=="html": el= assign(document.createElementNS(namespace_curr, tag), attributes); break;
default: el= assign(document.createElement(tag), attributes); default: el= assign(document.createElement(tag), attributes);
@ -23,17 +27,14 @@ export function createElement(tag, attributes, ...connect){
} }
export { createElement as el }; export { createElement as el };
import { addSignalListener } from './signals-common.js';
export function assign(element, ...attributes){ export function assign(element, ...attributes){
if(!attributes.length) return element; if(!attributes.length) return element;
const is_svg= element instanceof SVGElement; const is_svg= element instanceof SVGElement;
const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute"); const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
Object.entries(Object.assign({}, ...attributes)).forEach(function assignNth([ key, attr ]){ Object.entries(Object.assign({}, ...attributes)).forEach(function assignNth([ key, attr ]){
if(isSignal(attr)){ //TODO: unmounted if(signals.isReactiveAtrribute(attr, key))
addSignalListener(attr, attr=> assignNth([ key, attr ])); attr= signals.process(key, attr, assignNth);
attr= attr();
}
if(key[0]==="=") return setRemoveAttr(key.slice(1), attr); if(key[0]==="=") return setRemoveAttr(key.slice(1), attr);
if(key[0]===".") return setDelete(element, key.slice(1), attr); if(key[0]===".") return setDelete(element, key.slice(1), attr);
if(typeof attr === "object"){ if(typeof attr === "object"){

View File

@ -1,11 +1,13 @@
import { isSignal, addSignalListener, removeSignalListener } from './signals-common.js'; import { signals } from './signals-common.js';
export { registerReactivity } from './signals-common.js';
export function on(event, listener, options){ export function on(event, listener, options){
if(!isSignal(event)) if(!signals.isReactiveAtrribute(event))
return element=> element.addEventListener(event, listener, options); return element=> element.addEventListener(event, listener, options);
//TODO cleanup when signal removed (also TODO) //TODO cleanup when signal removed (also TODO)
if(options && options.signal) if(options && options.signal)
options.signal.addEventListener("abort", ()=> removeSignalListener(event, listener)); options.signal.addEventListener("abort", ()=> signals.off(event, listener));
return addSignalListener(event, listener); return signals.on(event, listener);
} }
export function off(){//TODO is needed? export function off(){//TODO is needed?
const abort= new AbortController(); const abort= new AbortController();

View File

@ -1,23 +1,12 @@
export const mark= Symbol.for("signal"); import { typeOf } from './helpers.js';
export const signals= {
export function isSignal(candidate){ isReactiveAtrribute(attr, key){ return false; },
try{ return Reflect.has(candidate, mark); } isTextContent(attributes){ return typeOf(attributes)!=="[object Object]"; },
catch(e){ return false; } process(key, attr, assignNth){ return false; },
} on(signal, listener){ return false; },
export function valueOfSignal(signal){ off(signal, listener){ return false; },
return signal[mark].value; reactiveElement(attributes, ...connect){ return document.createDocumentFragment(); }
} };
export function toSignal(signal, value){ export function registerReactivity(def){
signal[mark]= { return Object.assign(signals, def);
value,
listeners: new Set()
};
return signal;
}
export function addSignalListener(signal, listener){
return signal[mark].listeners.add(listener);
}
export function removeSignalListener(signal, listener){
return signal[mark].listeners.delete(listener);
} }

View File

@ -1,5 +1,41 @@
import { mark, isSignal, toSignal, addSignalListener } from "./signals-common.js"; export const mark= Symbol.for("signal");
export { isSignal, addSignalListener };
export function isSignal(candidate){
try{ return Reflect.has(candidate, mark); }
catch(e){ return false; }
}
import { typeOf } from './helpers.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){ export function S(value){
if(typeof value!=="function") if(typeof value!=="function")
@ -34,6 +70,14 @@ function reactive(data){
value.length ? write(signal, reactive(value[0])) : read(signal[mark]); value.length ? write(signal, reactive(value[0])) : read(signal[mark]);
return createWrapObject(type, toSignal(signal, data)); return createWrapObject(type, toSignal(signal, data));
}; };
function toSignal(signal, value){
signal[mark]= {
value,
listeners: new Set()
};
return signal;
}
const stack= []; const stack= [];
export function watch(context){ export function watch(context){
stack.push(function contextReWatch(){ stack.push(function contextReWatch(){
@ -98,3 +142,12 @@ function write(signal, value){
signal[mark].listeners.forEach(fn=> fn(value)) signal[mark].listeners.forEach(fn=> fn(value))
return 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);
}

View File

@ -1,4 +1,5 @@
import { S, el, on, off } from "../index.js"; import { el, on, off } from "../index.js";
import { S } from "../src/signals.js";
//import { empty, namespace, on, dispatch } from "../index.js"; //import { empty, namespace, on, dispatch } from "../index.js";
Object.assign(globalThis, { S, el, on, off }); Object.assign(globalThis, { S, el, on, off });
@ -39,8 +40,7 @@ function todosComponent({ todos= [] }= {}){
return el("div", { className }).append( return el("div", { className }).append(
el("div").append( el("div").append(
el("h1", "Todos:"), el("h1", "Todos:"),
elR(todos, el("<>", todos, ts=> !ts.length
ts=> !ts.length
? el("p", "No todos yet") ? el("p", "No todos yet")
: ts.map((t, i)=> el(todoComponent, { textContent: t, value: i, className }, onremove))) : ts.map((t, i)=> el(todoComponent, { textContent: t, value: i, className }, onremove)))
), ),
@ -64,22 +64,6 @@ function todoComponent({ textContent, className, value }){
el("button", { type: "button", value, textContent: "-" }) el("button", { type: "button", value, textContent: "-" })
); );
} }
function elR(signal, map){
const mark= document.createComment("reactive");
const out= el("<>").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);
};
on(signal, toEls);
toEls(signal());
return out;
}
function createStyle(){ function createStyle(){
const element= el("style"); const element= el("style");
const store= new WeakSet(); const store= new WeakSet();