From f5adefbc9c35065101c7ec99b482852a63815cd2 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Sat, 26 Aug 2023 17:32:58 +0200 Subject: [PATCH] :boom: 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)` --- index.js | 1 - src/dom.js | 21 ++++++++-------- src/events.js | 10 +++++--- src/signals-common.js | 33 +++++++++---------------- src/signals.js | 57 +++++++++++++++++++++++++++++++++++++++++-- test/index.js | 22 +++-------------- 6 files changed, 86 insertions(+), 58 deletions(-) diff --git a/index.js b/index.js index 6b55dce..d4732cd 100644 --- a/index.js +++ b/index.js @@ -4,4 +4,3 @@ }); export * from "./src/dom.js"; export * from "./src/events.js"; -export * from "./src/signals.js"; diff --git a/src/dom.js b/src/dom.js index 1916463..9e9db00 100644 --- a/src/dom.js +++ b/src/dom.js @@ -1,3 +1,5 @@ +import { signals } from "./signals-common.js"; + let namespace_curr= "html"; export function namespace(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; } }; } -import { typeOf } from './helpers.js'; -import { isSignal, valueOfSignal } from './signals-common.js'; export function createElement(tag, attributes, ...connect){ - if(typeOf(attributes)!=="[object Object]" || ( isSignal(attributes) && typeOf(valueOfSignal(attributes))!=="[object Object]" )) - attributes= { textContent: attributes }; 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){ 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 namespace_curr!=="html": el= assign(document.createElementNS(namespace_curr, tag), attributes); break; default: el= assign(document.createElement(tag), attributes); @@ -23,17 +27,14 @@ export function createElement(tag, attributes, ...connect){ } export { createElement as el }; -import { addSignalListener } from './signals-common.js'; export function assign(element, ...attributes){ 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(isSignal(attr)){ //TODO: unmounted - addSignalListener(attr, attr=> assignNth([ key, attr ])); - attr= 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(typeof attr === "object"){ diff --git a/src/events.js b/src/events.js index fe76e0a..3c30c78 100644 --- a/src/events.js +++ b/src/events.js @@ -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){ - if(!isSignal(event)) + 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", ()=> removeSignalListener(event, listener)); - return addSignalListener(event, listener); + options.signal.addEventListener("abort", ()=> signals.off(event, listener)); + return signals.on(event, listener); } export function off(){//TODO is needed? const abort= new AbortController(); diff --git a/src/signals-common.js b/src/signals-common.js index 8b208b0..e1b577e 100644 --- a/src/signals-common.js +++ b/src/signals-common.js @@ -1,23 +1,12 @@ -export const mark= Symbol.for("signal"); - -export function isSignal(candidate){ - try{ return Reflect.has(candidate, mark); } - catch(e){ return false; } -} -export function valueOfSignal(signal){ - return signal[mark].value; -} -export function toSignal(signal, value){ - signal[mark]= { - 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); +import { typeOf } from './helpers.js'; +export const signals= { + 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; }, + reactiveElement(attributes, ...connect){ return document.createDocumentFragment(); } +}; +export function registerReactivity(def){ + return Object.assign(signals, def); } diff --git a/src/signals.js b/src/signals.js index 7b16061..db4c181 100644 --- a/src/signals.js +++ b/src/signals.js @@ -1,5 +1,41 @@ -import { mark, isSignal, toSignal, addSignalListener } from "./signals-common.js"; -export { isSignal, addSignalListener }; +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'; +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") @@ -34,6 +70,14 @@ function reactive(data){ 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(){ @@ -98,3 +142,12 @@ function write(signal, 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); +} diff --git a/test/index.js b/test/index.js index 19cd0e2..fbe8b5c 100644 --- a/test/index.js +++ b/test/index.js @@ -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"; Object.assign(globalThis, { S, el, on, off }); @@ -39,8 +40,7 @@ function todosComponent({ todos= [] }= {}){ return el("div", { className }).append( el("div").append( el("h1", "Todos:"), - elR(todos, - ts=> !ts.length + el("<>", todos, ts=> !ts.length ? el("p", "No todos yet") : 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: "-" }) ); } -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(){ const element= el("style"); const store= new WeakSet();