From 404971f4849ec677b6a17e2bd5cfcbef226da634 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Thu, 24 Aug 2023 14:15:55 +0200 Subject: [PATCH] :sparkles: Add signals functionality and reactive attributes in `assign` --- README.md | 12 +++++------- index.js | 1 + src/dom.js | 10 +++++++++- src/events.js | 2 ++ src/signals.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/index.js | 47 ++++++++++++++++++--------------------------- 6 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 src/signals.js diff --git a/README.md b/README.md index f784292..9dd976f 100644 --- a/README.md +++ b/README.md @@ -97,15 +97,13 @@ document.body.append( ); ``` -## Events and dynamic parts +## Events and signals for reactivity *investigation*: ```js -const output_dynamic= eventsSink(store=> ({ - element, - onchange: listen("change", event=> assign(store.element, { textContent: event.target.value })) -})); +const value= S(""); document.body.append( - el("span", { style: { fontWeight: "bold" }, textContent: "" }, output_dynamic.target), - el("input", { type: "text" }, output_dynamic.onchange) + el("span", { style: { fontWeight: "bold" }, textContent: ()=> S(value) }), + el("input", { type: "text" }, + listen("change", event=> S(value, event.target, value))) ); ``` diff --git a/index.js b/index.js index d4732cd..6b55dce 100644 --- a/index.js +++ b/index.js @@ -4,3 +4,4 @@ }); 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 ec1fe71..555eb95 100644 --- a/src/dom.js +++ b/src/dom.js @@ -20,14 +20,22 @@ export function createElementNS(tag, attributes, attributes_todo){ } export { createElementNS as elNS }; +import { watch } from './signals.js'; +function isReactive(key, attr){ + if(typeof attr !== "function") return false; + if(key.startsWith("on")) return false; + return true; +} 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([ key, attr ]){ + Object.entries(Object.assign({}, ...attributes)).forEach(function assignNth([ key, attr ]){ if(key[0]==="=") return setRemoveAttr(key.slice(1), attr); if(key[0]===".") return setDelete(element, key.slice(1), attr); + if(isReactive(key, attr)) + return watch(()=> assignNth([ key, attr() ])); if(typeof attr === "object"){ switch(key){ case "style": return forEachEntries(attr, setRemove.bind(null, element.style, "Property")) diff --git a/src/events.js b/src/events.js index d20f733..393481b 100644 --- a/src/events.js +++ b/src/events.js @@ -1,4 +1,6 @@ +import { isSignal } from './signals.js'; export function listen(event, listener, options){ + if(isSignal(event)) return event.listeners.add(listener); return element=> element.addEventListener(event, listener, options); } export function dispatch(event, detail){ diff --git a/src/signals.js b/src/signals.js new file mode 100644 index 0000000..1b239e5 --- /dev/null +++ b/src/signals.js @@ -0,0 +1,52 @@ +const mark= Symbol.for("signal"); +export function isSignal(candidate){ + try{ + return Reflect.has(candidate, mark); + } catch(e){ + return false; + } +} +export function S(signal, ...value){ + if(typeof signal==="function"){ + const out= create(); + watch(()=> S(out, signal())); + return out; + } + if(!isSignal(signal)) + return create(signal); + + if(value.length===0) + return read(signal); + return write(signal, value[0]); +} +const stack= []; +export function watch(context){ + stack.push(context); + context(); + stack.pop(); +}; + +function currentContext(){ + return stack[stack.length - 1]; +} +function create(value){ + if(typeof value==="object" && value!==null) + return Object.fromEntries( + Object.entries(value) + .map(([ key, value ])=> [ key, create(value) ]) + ); + return { + [mark]: true, + value, + listeners: new Set() + }; +} +function read({ value, listeners }){ + const context= currentContext(); + if(context) listeners.add(context); + return value; +} +function write(signal, value){ + signal.value= value; + signal.listeners.forEach(fn=> fn(value)) +} diff --git a/test/index.js b/test/index.js index 904ffad..7c768d1 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,5 @@ -import { el, elNS, assign, listen, dispatch } from "../index.js"; -Object.assign(globalThis, { el, elNS, assign, listen, dispatch }); +import { S, watch, el, elNS, assign, listen, dispatch } from "../index.js"; +Object.assign(globalThis, { S, watch, el, elNS, assign, listen, dispatch }); const { style, css }= createStyle(); globalThis.test= console.log; @@ -10,48 +10,39 @@ console.log(app, app instanceof HTMLDivElement); document.head.append(style); document.body.append(app); -function component({ value= "World" }= {}){ - const name= "naiveForm"; +function component({ name= "World", surname= "" }= {}){ + const className= "naiveForm"; css` - .${name}{ + .${className}{ display: flex; flex-flow: column nowrap; } - .${name} input{ + .${className} input{ margin-inline-start: .5em; } `; + const store= S({ name, surname }); + const full_name= S(()=> S(store.name)+" "+S(store.surname)); + listen(full_name, console.log); - const output= eventsSink(store=> ({ - onchange: listen("change", function(event){ - assign(store.element, { textContent: event.target.value }); - }) - })); - const input= eventsSink(store=> ({ - onchange: listen("change", function(event){ - assign(store.element, { value: event.detail }); - dispatch("change")(input.element); - }) - })); - return el("div", { className: name }, input.onchange).append( + return el("div", { className }).append( el("p").append( el("#text", { textContent: "Hello " }), - el("strong", { textContent: value }, output.target), + el("strong", { textContent: ()=> S(full_name) }), + el("#text", { textContent: "!" }), ), el("label").append( el("#text", { textContent: "Set name:" }), - el("input", { type: "text", value }, output.onchange, input.target) + el("input", { type: "text", value: ()=> S(store.name) }, + listen("change", ev=> S(store.name, ev.target.value))), + ), + el("label").append( + el("#text", { textContent: "Set surname:" }), + el("input", { type: "text", value: ()=> S(store.surname) }, + listen("change", ev=> S(store.surname, ev.target.value))), ) ) } -function eventsSink(fn){ - const store= { - element: null, - target: function(element){ store.element= element; }, - }; - Object.assign(store, fn(store)); - return store; -} function createStyle(){ const style= el("style"); return {