From 7f1506781f38732a85044709a5edd9ccca9720f2 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Sat, 26 Aug 2023 09:36:55 +0200 Subject: [PATCH] =?UTF-8?q?:rocket:=20simplify=20text,=20S=E2=87=94reactiv?= =?UTF-8?q?e=3F,=20todo=20as=20an=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dom.js | 10 +++-- src/signals.js | 12 ++++-- test/index.html | 2 +- test/index.js | 103 ++++++++++++++++++++++++++++++++++-------------- 4 files changed, 91 insertions(+), 36 deletions(-) diff --git a/src/dom.js b/src/dom.js index df2ad30..5ca0a8a 100644 --- a/src/dom.js +++ b/src/dom.js @@ -1,3 +1,4 @@ +import { addSignalListener, isSignal } from './signals.js'; let namespace_curr= "html"; export function namespace(namespace){ namespace_curr= namespace==="svg" ? "http://www.w3.org/2000/svg" : namespace; @@ -6,6 +7,8 @@ export function namespace(namespace){ }; } export function createElement(tag, attributes, ...connect){ + if(typeof attributes==="string" || ( isSignal(attributes) /* TODO && isText*/ )) + attributes= { textContent: attributes }; let el; switch(true){ case typeof tag==="function": el= tag(attributes || undefined); break; @@ -19,15 +22,16 @@ export function createElement(tag, attributes, ...connect){ } export { createElement as el }; -import { watch, isSignal } from './signals.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 - return watch(()=> assignNth([ key, attr() ])); + if(isSignal(attr)){ //TODO: unmounted + addSignalListener(attr, attr=> assignNth([ key, attr ])); + attr= attr(); + } 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/signals.js b/src/signals.js index 6d17d44..7cf322f 100644 --- a/src/signals.js +++ b/src/signals.js @@ -15,7 +15,8 @@ export function S(value){ watch(()=> out(value())); return out; } -export function reactive(data){ +S.reactive= reactive; +function reactive(data){ if(isSignal(data)) return data; if(typeof data!=="object" || data===null) @@ -37,10 +38,14 @@ export function reactive(data){ const signal= (...value)=> value.length ? write(signal, reactive(value[0])) : read(signal[mark]); return createWrapObject(type, toSignal(signal, data)); -} +}; const stack= []; export function watch(context){ - stack.push(context); + stack.push(function contextReWatch(){ + stack.push(contextReWatch); + context(); + stack.pop(); + }); context(); stack.pop(); }; @@ -103,4 +108,5 @@ function read({ value, listeners }){ function write(signal, value){ signal[mark].value= value; signal[mark].listeners.forEach(fn=> fn(value)) + return value; } diff --git a/test/index.html b/test/index.html index 87046fe..d4b04f6 100644 --- a/test/index.html +++ b/test/index.html @@ -5,7 +5,7 @@ - + Small DOM element creation enhancements diff --git a/test/index.js b/test/index.js index 7c83861..835b0ed 100644 --- a/test/index.js +++ b/test/index.js @@ -1,18 +1,17 @@ -import { S, reactive, watch, el, namespace, assign, on, dispatch } from "../index.js"; -Object.assign(globalThis, { S, reactive, watch, el, namespace, assign, on, dispatch }); +import { S, empty, watch, el, namespace, assign, on, dispatch } from "../index.js"; +Object.assign(globalThis, { S, watch, el, namespace, assign, on, dispatch }); -const { style, css }= createStyle(); -globalThis.test= console.log; -const app= el(component, null, on("change", globalThis.test)); +const style= createStyle(); +const app= el(todosComponent); dispatch("change", "Peter")(app); console.log(app, app instanceof HTMLDivElement); -document.head.append(style); +document.head.append(style.element); document.body.append(app); -function component({ name= "World", surname= "" }= {}){ - const className= "naiveForm"; - css` +function todosComponent({ todos= [] }= {}){ + const className= "basicTodoForm"; + style.css` .${className}{ display: flex; flex-flow: column nowrap; @@ -21,34 +20,80 @@ function component({ name= "World", surname= "" }= {}){ margin-inline-start: .5em; } `; - const store= reactive({ name, surname }); - const full_name= S(()=> store.name()+" "+store.surname()); - on(full_name, console.log); + todos= S.reactive(todos); + globalThis.__todos__= todos; //TODO + const name= "todoName"; + const onsubmit= on("submit", event=> { + const value= event.target.elements[name].value; + if(!value) return; + + event.preventDefault(); + todos.push(value) + event.target.elements[name].value= ""; + }); + const onremove= on("click", event=> { + const value= Number(event.target.value); + if(Number.isNaN(value)) return; + event.preventDefault(); + todos.splice(value, 1); + }); - return el("div", { className }, on.connected(console.log)).append( - el("p").append( - el("#text", { textContent: "Hello " }), - el("strong", { textContent: full_name }), - el("#text", { textContent: "!" }), + return el("div", { className }).append( + el("div").append( + el("h1", "Todos:"), + elR(todos, + ts=> !ts.length + ? el("p", "No todos yet") + : ts.map((t, i)=> el(todoComponent, { textContent: t, value: i, className }, onremove))) ), - el("label").append( - el("#text", { textContent: "Set name:" }), - el("input", { type: "text", value: store.name }, - on("change", ev=> store.name(ev.target.value))), - ), - el("label").append( - el("#text", { textContent: "Set surname:" }), - el("input", { type: "text", value: store.surname }, - on("change", ev=> store.surname(ev.target.value))), + el("form", null, onsubmit).append( + el("h2", "Add:"), + el("label", "New todo: ").append( + el("input", { name, type: "text", required: true }), + ), + el("button", "+") ) ) } +function todoComponent({ textContent, className, value }){ + style.key(todoComponent).css` + .${className} button{ + margin-inline-start: 1em; + } + `; + return el("p").append( + el("#text", 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(){ - const style= el("style"); + const element= el("style"); + const store= new WeakSet(); return { - style, + element, + key(k){ + if(store.has(k)) return { css: ()=> {} }; + store.add(k); + return this; + }, css(...args){ - style.appendChild(el("#text", { textContent: String.raw(...args) })); + element.appendChild(el("#text", { textContent: String.raw(...args) })); } }; }