From cd62782c7b976430a7295ddd61d9edca129b1fc1 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Wed, 22 May 2024 21:43:49 +0200 Subject: [PATCH] =?UTF-8?q?:zap:=20Replace=20=E2=80=9Cobservable=E2=80=9D?= =?UTF-8?q?=20term=20with=20=E2=80=9Csignal=E2=80=9D=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :zap: refact docs to make editing (now renaming observables to signal) easier * :zap: :zap: use signal(s) term isntead of observable(s) * :zap: :abc: version + typo * :bug: customElement example (0→S) * :tv: version in package-lock.json --- README.md | 15 +- bs/build.js | 6 +- bs/docs.js | 23 +- ...ith-observables.js => dde-with-signals.js} | 346 +++++++++--------- dist/dde.js | 88 ++--- ...observables.d.ts => esm-with-signals.d.ts} | 66 ++-- ...ith-observables.js => esm-with-signals.js} | 346 +++++++++--------- dist/esm.d.ts | 66 ++-- dist/esm.js | 88 ++--- docs/index.html | 10 +- docs/p02-elements.html | 18 +- docs/p03-events.html | 14 +- docs/p04-signals.html | 147 +++++++- docs/p05-scopes.html | 35 +- docs/p06-customElement.html | 8 +- docs_src/components/example.html.js | 6 +- .../examples/customElement/intro.js | 6 +- docs_src/components/examples/helloWorld.js | 6 +- .../observables/computations-abort.js | 16 - .../components/examples/observables/intro.js | 6 - .../examples/observables/observables.js | 12 - .../components/examples/scopes/cleaning.js | 4 +- .../components/examples/scopes/declarative.js | 12 +- docs_src/components/examples/scopes/intro.js | 2 +- .../{observables => signals}/actions-demo.js | 8 +- .../{observables => signals}/actions-todos.js | 18 +- .../examples/signals/computations-abort.js | 16 + .../{observables => signals}/dom-attrs.js | 8 +- .../{observables => signals}/dom-el.js | 14 +- docs_src/components/examples/signals/intro.js | 6 + .../components/examples/signals/signals.js | 12 + .../components/mnemonic/customElement-init.js | 2 +- .../components/mnemonic/observables-init.js | 28 -- docs_src/components/mnemonic/signals-init.js | 28 ++ docs_src/index.html.js | 11 +- docs_src/p02-elements.html.js | 8 +- docs_src/p03-events.html.js | 6 +- ...bservables.html.js => p04-signals.html.js} | 64 ++-- docs_src/p05-scopes.html.js | 16 +- docs_src/p06-customElement.html.js | 6 +- docs_src/ssr.js | 13 +- examples/components/3rd-party.js | 26 +- examples/components/fullNameComponent.js | 6 +- examples/components/todosComponent.js | 22 +- examples/components/webComponent.js | 4 +- examples/exports.js | 8 +- index-with-observables.d.ts | 2 - index-with-observables.js | 2 - index-with-signals.d.ts | 2 + index-with-signals.js | 2 + index.d.ts | 18 +- observables.d.ts | 66 ---- observables.js | 4 - package-lock.json | 4 +- package.json | 20 +- signals.d.ts | 66 ++++ signals.js | 4 + src/dom.js | 10 +- src/events.js | 2 +- src/observables-common.js | 13 - src/observables-lib.d.ts | 4 - src/observables-lib.js | 200 +++++----- src/signals-common.js | 13 + src/signals-lib.d.ts | 4 + src/signals-lib.js | 292 +++++++++++++++ 65 files changed, 1426 insertions(+), 978 deletions(-) rename dist/{dde-with-observables.js => dde-with-signals.js} (64%) rename dist/{esm-with-observables.d.ts => esm-with-signals.d.ts} (92%) rename dist/{esm-with-observables.js => esm-with-signals.js} (64%) delete mode 100644 docs_src/components/examples/observables/computations-abort.js delete mode 100644 docs_src/components/examples/observables/intro.js delete mode 100644 docs_src/components/examples/observables/observables.js rename docs_src/components/examples/{observables => signals}/actions-demo.js (58%) rename docs_src/components/examples/{observables => signals}/actions-todos.js (78%) create mode 100644 docs_src/components/examples/signals/computations-abort.js rename docs_src/components/examples/{observables => signals}/dom-attrs.js (62%) rename docs_src/components/examples/{observables => signals}/dom-el.js (62%) create mode 100644 docs_src/components/examples/signals/intro.js create mode 100644 docs_src/components/examples/signals/signals.js delete mode 100644 docs_src/components/mnemonic/observables-init.js create mode 100644 docs_src/components/mnemonic/signals-init.js rename docs_src/{p04-observables.html.js => p04-signals.html.js} (60%) delete mode 100644 index-with-observables.d.ts delete mode 100644 index-with-observables.js create mode 100644 index-with-signals.d.ts create mode 100644 index-with-signals.js delete mode 100644 observables.d.ts delete mode 100644 observables.js create mode 100644 signals.d.ts create mode 100644 signals.js delete mode 100644 src/observables-common.js delete mode 100644 src/observables-lib.d.ts create mode 100644 src/signals-common.js create mode 100644 src/signals-lib.d.ts create mode 100644 src/signals-lib.js diff --git a/README.md b/README.md index 7f13b5c..e3bda25 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ document.body.append( ); function component({ textContent, className }){ - const value= O("onchange"); + const value= S("onchange"); return el().append( el("p", { textContent, className }), @@ -35,8 +35,7 @@ function component({ textContent, className }){ } ``` # Deka DOM Elements -Creating reactive elements, components and Web components using [IDL](https://developer.mozilla.org/en-US/docs/Glossary/IDL)/JavaScript DOM API and signals/observables -([Signals — whats going on behind the scenes | by Ryan Hoffnan | ITNEXT](https://itnext.io/signals-whats-going-on-behind-the-scenes-ec858589ea63), [The Evolution of Signals in JavaScript - DEV Community](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob) or [Observer pattern - Wikipedia](https://en.wikipedia.org/wiki/Observer_pattern)). +Creating reactive elements, components and Web components using [IDL](https://developer.mozilla.org/en-US/docs/Glossary/IDL)/JavaScript DOM API and [**signals/observables**](#signals). ## Inspiration and suggested alternatives - my previous library (mostly used internaly): [jaandrle/dollar_dom_component: Functional DOM components without JSX and virtual DOM.](https://github.com/jaandrle/dollar_dom_component) @@ -53,13 +52,13 @@ Another goal is to proceed in the best spirit of functional programming. This in pure JavaScript (DOM API) and gradually adding auxiliary functions, ranging from “minor” improvements to the capability of writing complete declarative reactive UI templates. -As a result, any “internal” function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, …, `O`, …) +As a result, any “internal” function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, …, `S`, …) can be used independently, although they are primarily designed for use in combination. This can also, hopefully, help in integrating the library into existing projects. To balance these requirements, numerous compromises have been made. To summarize: - [ ] Library size: 10–15kB minified (the original goal was a maximum of 10kB) -- [x] Optional use of *observables* with the ability to register *your own signals/observables implementation* +- [x] Optional use of *signals* with the ability to register *your own signals/observables implementation* - [x] *No build step required* - [x] Preference for a *declarative/functional* approach - [x] Focus on zero/minimal dependencies @@ -73,3 +72,9 @@ To balance these requirements, numerous compromises have been made. To summarize - Installation - npm - [dist/](dist/) (`https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/`…) + +## Signals +- [Signals — whats going on behind the scenes | by Ryan Hoffnan | ITNEXT](https://itnext.io/signals-whats-going-on-behind-the-scenes-ec858589ea63) +- [The Evolution of Signals in JavaScript - DEV Community](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob) +- there is also [tc39/proposal-signals: A proposal to add signals to JavaScript.](https://github.com/tc39/proposal-signals) +- [Observer pattern - Wikipedia](https://en.wikipedia.org/wiki/Observer_pattern) diff --git a/bs/build.js b/bs/build.js index 7fa087f..947928c 100755 --- a/bs/build.js +++ b/bs/build.js @@ -1,14 +1,14 @@ #!/usr/bin/env -S npx nodejsscript import { bundle as bundleDTS } from "dts-bundler"; -const files= [ "index", "index-with-observables" ]; +const files= [ "index", "index-with-signals" ]; const filesOut= (file, mark= "esm")=> "dist/"+file.replace("index", mark); const css= echo.css` .info{ color: gray; } `; $.api("", true) -.option("--minify", "Level of minification [ full (default), partial ]") -.action(async function main({ minify= "full" }){ +.option("--minify", "Level of minification [ full, partial (default) ]") +.action(async function main({ minify= "partial" }){ for(const file_root of files){ const file= file_root+".js"; echo("Processing: "+ file); diff --git a/bs/docs.js b/bs/docs.js index bb2cb9b..9836d29 100755 --- a/bs/docs.js +++ b/bs/docs.js @@ -2,20 +2,21 @@ /* jshint esversion: 11,-W097, -W040, module: true, node: true, expr: true, undef: true *//* global echo, $, pipe, s, fetch, cyclicLoop */ echo("Building static documentation files…"); echo("Preparing…"); -import { path_target, pages, styles, dispatchEvent } from "../docs_src/ssr.js"; +import { path_target, pages as pages_registered, styles, dispatchEvent } from "../docs_src/ssr.js"; import { createHTMl } from "./docs/jsdom.js"; import { register } from "../jsdom.js"; const pkg= s.cat("package.json").xargs(JSON.parse); -for(const info of pages){ - const { id }= info; +echo("Collecting list of pages…"); +const pages= s.ls($.xdg.main`../docs_src/*.html.js`).map(addPage); +for(const { id, info } of pages){ echo(`Generating ${id}.html…`); const serverDOM= createHTMl(""); serverDOM.registerGlobally( "HTMLScriptElement" ); const { el }= await register(serverDOM.dom); - const { page }= await import(`../docs_src/${id}.html.js`); //→ TODO: important to mention in docs!!! + const { page }= await import(`../docs_src/${id}.html.js`); serverDOM.document.body.append( el(page, { pkg, info }), ); @@ -27,3 +28,17 @@ for(const info of pages){ s.echo(styles.content).to(path_target.css+styles.name); dispatchEvent("onssrend"); echo("Done"); + +/** @param {`${string}/${string}.html.js`} path */ +function addPage(path){ + const id= idFromPath(path); + const [ info_pre ]= s.cat(path).match(/(?<=\s*export\s+const\s+info\s*=\s*)\{(.|\s)*?\}(?=;)/gm); + const info= { id, href: id, ...eval(`(${info_pre})`) }; + pages_registered.push(info); + return { id, info }; +} +/** @param {`${string}/${string}.html.js`} path */ +function idFromPath(path){ + const file_start= path.lastIndexOf("/"); + return path.slice(file_start+1, path.indexOf(".", file_start)); +} diff --git a/dist/dde-with-observables.js b/dist/dde-with-signals.js similarity index 64% rename from dist/dde-with-observables.js rename to dist/dde-with-signals.js index 8050133..1535b54 100644 --- a/dist/dde-with-observables.js +++ b/dist/dde-with-signals.js @@ -1,8 +1,8 @@ //deka-dom-el library is available via global namespace `dde` (()=> { -// src/observables-common.js +// src/signals-common.js var k = { - isObservable(t) { + isSignal(t) { return !1; }, processReactiveAttribute(t, e, n, r) { @@ -18,7 +18,7 @@ function W(t) { // src/helpers.js var T = (...t) => Object.prototype.hasOwnProperty.call(...t); -function A(t) { +function S(t) { return typeof t > "u"; } function X(t) { @@ -54,17 +54,17 @@ var d = { M: globalThis.MutationObserver }; function lt(t, e, n) { - if (Reflect.set(t, e, n), !!A(n)) { + if (Reflect.set(t, e, n), !!S(n)) { if (Reflect.deleteProperty(t, e), t instanceof d.H && t.getAttribute(e) === "undefined") return t.removeAttribute(e); if (Reflect.get(t, e) === "undefined") return Reflect.set(t, e, ""); } } -var C = "__dde_lifecyclesToEvents", y = "dde:connected", S = "dde:disconnected", P = "dde:attributeChanged"; +var C = "__dde_lifecyclesToEvents", _ = "dde:connected", O = "dde:disconnected", M = "dde:attributeChanged"; // src/dom.js -var _ = [{ +var A = [{ get scope() { return d.D.body; }, @@ -72,7 +72,7 @@ var _ = [{ prevent: !0 }], m = { get current() { - return _[_.length - 1]; + return A[A.length - 1]; }, get host() { return this.current.host; @@ -82,17 +82,17 @@ var _ = [{ return t.prevent = !0, t; }, get state() { - return [..._]; + return [...A]; }, push(t = {}) { - return _.push(Object.assign({}, this.current, { prevent: !1 }, t)); + return A.push(Object.assign({}, this.current, { prevent: !1 }, t)); }, pushRoot() { - return _.push(_[0]); + return A.push(A[0]); }, pop() { - if (_.length !== 1) - return _.pop(); + if (A.length !== 1) + return A.pop(); } }; function Y(...t) { @@ -102,68 +102,68 @@ function ht(t) { return t.append === Y || (t.appendOriginal = t.append, t.append = Y), t; } var $; -function M(t, e, ...n) { - let r = W(this), o = 0, c, s; - switch ((Object(e) !== e || r.isObservable(e)) && (e = { textContent: e }), !0) { +function j(t, e, ...n) { + let r = W(this), o = 0, c, i; + switch ((Object(e) !== e || r.isSignal(e)) && (e = { textContent: e }), !0) { case typeof t == "function": { - o = 1, m.push({ scope: t, host: (...g) => g.length ? (o === 1 ? n.unshift(...g) : g.forEach((l) => l(s)), void 0) : s }), c = t(e || void 0); + o = 1, m.push({ scope: t, host: (...v) => v.length ? (o === 1 ? n.unshift(...v) : v.forEach((l) => l(i)), void 0) : i }), c = t(e || void 0); let a = c instanceof d.F; if (c.nodeName === "#comment") break; - let h = M.mark({ + let h = j.mark({ type: "component", name: t.name, host: a ? "this" : "parentElement" }); - c.prepend(h), a && (s = h); + c.prepend(h), a && (i = h); break; } case t === "#text": - c = j.call(this, d.D.createTextNode(""), e); + c = P.call(this, d.D.createTextNode(""), e); break; case (t === "<>" || !t): - c = j.call(this, d.D.createDocumentFragment(), e); + c = P.call(this, d.D.createDocumentFragment(), e); break; case !!$: - c = j.call(this, d.D.createElementNS($, t), e); + c = P.call(this, d.D.createElementNS($, t), e); break; case !c: - c = j.call(this, d.D.createElement(t), e); + c = P.call(this, d.D.createElement(t), e); } - return ht(c), s || (s = c), n.forEach((a) => a(s)), o && m.pop(), o = 2, c; + return ht(c), i || (i = c), n.forEach((a) => a(i)), o && m.pop(), o = 2, c; } function Wt(t, e = t, n = void 0) { - let r = Symbol.for("default"), o = Array.from(e.querySelectorAll("slot")).reduce((s, a) => Reflect.set(s, a.name || r, a) && s, {}), c = T(o, r); + let r = Symbol.for("default"), o = Array.from(e.querySelectorAll("slot")).reduce((i, a) => Reflect.set(i, a.name || r, a) && i, {}), c = T(o, r); if (t.append = new Proxy(t.append, { - apply(s, a, h) { + apply(i, a, h) { if (!h.length) return t; - let g = d.D.createDocumentFragment(); + let v = d.D.createDocumentFragment(); for (let l of h) { if (!l || !l.slot) { - c && g.appendChild(l); + c && v.appendChild(l); continue; } - let x = l.slot, w = o[x]; - gt(l, "remove", "slot"), w && (bt(w, l, n), Reflect.deleteProperty(o, x)); + let x = l.slot, y = o[x]; + vt(l, "remove", "slot"), y && (bt(y, l, n), Reflect.deleteProperty(o, x)); } - return c && (o[r].replaceWith(g), Reflect.deleteProperty(o, r)), t.append = s, t; + return c && (o[r].replaceWith(v), Reflect.deleteProperty(o, r)), t.append = i, t; } }), t !== e) { - let s = Array.from(t.childNodes); - s.forEach((a) => a.remove()), t.append(...s); + let i = Array.from(t.childNodes); + i.forEach((a) => a.remove()), t.append(...i); } return e; } function bt(t, e, n) { n && n(t, e); try { - t.replaceWith(j(e, { className: [e.className, t.className], dataset: { ...t.dataset } })); + t.replaceWith(P(e, { className: [e.className, t.className], dataset: { ...t.dataset } })); } catch { t.replaceWith(e); } } -M.mark = function(t, e = !1) { +j.mark = function(t, e = !1) { t = Object.entries(t).map(([o, c]) => o + `="${c}"`).join(" "); let n = e ? "" : "/", r = d.D.createComment(``); return e && (r.end = d.D.createComment("")), r; @@ -172,12 +172,12 @@ function qt(t) { let e = this; return function(...r) { $ = t; - let o = M.call(e, ...r); + let o = j.call(e, ...r); return $ = void 0, o; }; } var U = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: tt } = d; -function j(t, ...e) { +function P(t, ...e) { if (!e.length) return t; U.set(t, rt(t, this)); @@ -193,10 +193,10 @@ function nt(t, e, n) { n, (a, h) => nt.call(c, t, a, h) ); - let [s] = e; - if (s === "=") + let [i] = e; + if (i === "=") return r(e.slice(1), n); - if (s === ".") + if (i === ".") return et(t, e.slice(1), n); if (/(aria|data)([A-Z])/.test(e)) return e = e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), r(e, n); @@ -213,7 +213,7 @@ function nt(t, e, n) { case "ariaset": return I(o, n, (a, h) => r("aria-" + a, h)); case "classList": - return vt.call(c, t, n); + return gt.call(c, t, n); } return Et(t, e) ? tt(t, e, n) : r(e, n); } @@ -223,7 +223,7 @@ function rt(t, e) { let r = (t instanceof d.S ? xt : mt).bind(null, t, "Attribute"), o = W(e); return { setRemoveAttr: r, s: o }; } -function vt(t, e) { +function gt(t, e) { let n = W(this); return I( n, @@ -234,14 +234,14 @@ function vt(t, e) { function Ft(t) { return Array.from(t.children).forEach((e) => e.remove()), t; } -function gt(t, e, n, r) { +function vt(t, e, n, r) { return t instanceof d.H ? t[e + "Attribute"](n, r) : t[e + "AttributeNS"](null, n, r); } function Et(t, e) { if (!(e in t)) return !1; let n = ot(t, e); - return !A(n.set); + return !S(n.set); } function ot(t, e) { if (t = Object.getPrototypeOf(t), !t) @@ -259,141 +259,141 @@ function ct(t) { return Array.isArray(t) ? t.filter(Boolean).join(" ") : t; } function mt(t, e, n, r) { - return t[(A(r) ? "remove" : "set") + e](n, ct(r)); + return t[(S(r) ? "remove" : "set") + e](n, ct(r)); } function xt(t, e, n, r, o = null) { - return t[(A(r) ? "remove" : "set") + e + "NS"](o, n, ct(r)); + return t[(S(r) ? "remove" : "set") + e + "NS"](o, n, ct(r)); } function et(t, e, n) { - if (Reflect.set(t, e, n), !!A(n)) + if (Reflect.set(t, e, n), !!S(n)) return Reflect.deleteProperty(t, e); } // src/events-observer.js -var D = d.M ? Ot() : new Proxy({}, { +var D = d.M ? wt() : new Proxy({}, { get() { return () => { }; } }); -function Ot() { - let t = /* @__PURE__ */ new Map(), e = !1, n = (i) => function(u) { +function wt() { + let t = /* @__PURE__ */ new Map(), e = !1, n = (s) => function(u) { for (let f of u) if (f.type === "childList") { if (l(f.addedNodes, !0)) { - i(); + s(); continue; } - x(f.removedNodes, !0) && i(); + x(f.removedNodes, !0) && s(); } }, r = new d.M(n(a)); return { - observe(i) { + observe(s) { let u = new d.M(n(() => { })); - return u.observe(i, { childList: !0, subtree: !0 }), () => u.disconnect(); + return u.observe(s, { childList: !0, subtree: !0 }), () => u.disconnect(); }, - onConnected(i, u) { - s(); - let f = c(i); + onConnected(s, u) { + i(); + let f = c(s); f.connected.has(u) || (f.connected.add(u), f.length_c += 1); }, - offConnected(i, u) { - if (!t.has(i)) + offConnected(s, u) { + if (!t.has(s)) return; - let f = t.get(i); - f.connected.has(u) && (f.connected.delete(u), f.length_c -= 1, o(i, f)); + let f = t.get(s); + f.connected.has(u) && (f.connected.delete(u), f.length_c -= 1, o(s, f)); }, - onDisconnected(i, u) { - s(); - let f = c(i); + onDisconnected(s, u) { + i(); + let f = c(s); f.disconnected.has(u) || (f.disconnected.add(u), f.length_d += 1); }, - offDisconnected(i, u) { - if (!t.has(i)) + offDisconnected(s, u) { + if (!t.has(s)) return; - let f = t.get(i); - f.disconnected.has(u) && (f.disconnected.delete(u), f.length_d -= 1, o(i, f)); + let f = t.get(s); + f.disconnected.has(u) && (f.disconnected.delete(u), f.length_d -= 1, o(s, f)); } }; - function o(i, u) { - u.length_c || u.length_d || (t.delete(i), a()); + function o(s, u) { + u.length_c || u.length_d || (t.delete(s), a()); } - function c(i) { - if (t.has(i)) - return t.get(i); + function c(s) { + if (t.has(s)) + return t.get(s); let u = { connected: /* @__PURE__ */ new WeakSet(), length_c: 0, disconnected: /* @__PURE__ */ new WeakSet(), length_d: 0 }; - return t.set(i, u), u; + return t.set(s, u), u; } - function s() { + function i() { e || (e = !0, r.observe(d.D.body, { childList: !0, subtree: !0 })); } function a() { !e || t.size || (e = !1, r.disconnect()); } function h() { - return new Promise(function(i) { - (requestIdleCallback || requestAnimationFrame)(i); + return new Promise(function(s) { + (requestIdleCallback || requestAnimationFrame)(s); }); } - async function g(i) { + async function v(s) { t.size > 30 && await h(); let u = []; - if (!(i instanceof Node)) + if (!(s instanceof Node)) return u; for (let f of t.keys()) - f === i || !(f instanceof Node) || i.contains(f) && u.push(f); + f === s || !(f instanceof Node) || s.contains(f) && u.push(f); return u; } - function l(i, u) { + function l(s, u) { let f = !1; - for (let b of i) { - if (u && g(b).then(l), !t.has(b)) + for (let b of s) { + if (u && v(b).then(l), !t.has(b)) continue; let N = t.get(b); - N.length_c && (b.dispatchEvent(new Event(y)), N.connected = /* @__PURE__ */ new WeakSet(), N.length_c = 0, N.length_d || t.delete(b), f = !0); + N.length_c && (b.dispatchEvent(new Event(_)), N.connected = /* @__PURE__ */ new WeakSet(), N.length_c = 0, N.length_d || t.delete(b), f = !0); } return f; } - function x(i, u) { + function x(s, u) { let f = !1; - for (let b of i) - u && g(b).then(x), !(!t.has(b) || !t.get(b).length_d) && ((globalThis.queueMicrotask || setTimeout)(w(b)), f = !0); + for (let b of s) + u && v(b).then(x), !(!t.has(b) || !t.get(b).length_d) && ((globalThis.queueMicrotask || setTimeout)(y(b)), f = !0); return f; } - function w(i) { + function y(s) { return () => { - i.isConnected || (i.dispatchEvent(new Event(S)), t.delete(i)); + s.isConnected || (s.dispatchEvent(new Event(O)), t.delete(s)); }; } } // src/customElement.js -function Zt(t, e, n, r = yt) { +function Zt(t, e, n, r = _t) { m.push({ scope: t, - host: (...s) => s.length ? s.forEach((a) => a(t)) : t + host: (...i) => i.length ? i.forEach((a) => a(t)) : t }), typeof r == "function" && (r = r.call(t, t)); let o = t[C]; - o || wt(t); + o || yt(t); let c = n.call(t, r); - return o || t.dispatchEvent(new Event(y)), e.nodeType === 11 && typeof e.mode == "string" && t.addEventListener(S, D.observe(e), { once: !0 }), m.pop(), e.append(c); + return o || t.dispatchEvent(new Event(_)), e.nodeType === 11 && typeof e.mode == "string" && t.addEventListener(O, D.observe(e), { once: !0 }), m.pop(), e.append(c); } -function wt(t) { +function yt(t) { return J(t.prototype, "connectedCallback", function(e, n, r) { - e.apply(n, r), n.dispatchEvent(new Event(y)); + e.apply(n, r), n.dispatchEvent(new Event(_)); }), J(t.prototype, "disconnectedCallback", function(e, n, r) { e.apply(n, r), (globalThis.queueMicrotask || setTimeout)( - () => !n.isConnected && n.dispatchEvent(new Event(S)) + () => !n.isConnected && n.dispatchEvent(new Event(O)) ); }), J(t.prototype, "attributeChangedCallback", function(e, n, r) { let [o, , c] = r; - n.dispatchEvent(new CustomEvent(P, { + n.dispatchEvent(new CustomEvent(M, { detail: [o, c] })), e.apply(n, r); }), t.prototype[C] = !0, t; @@ -402,7 +402,7 @@ function J(t, e, n) { t[e] = new Proxy(t[e] || (() => { }), { apply: n }); } -function yt(t) { +function _t(t) { return F(t, (e, n) => e.getAttribute(n)); } @@ -410,50 +410,50 @@ function yt(t) { function Qt(t, e, n) { return e || (e = {}), function(o, ...c) { n && (c.unshift(o), o = typeof n == "function" ? n() : n); - let s = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e); - return o.dispatchEvent(s); + let i = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e); + return o.dispatchEvent(i); }; } -function O(t, e, n) { +function w(t, e, n) { return function(o) { return o.addEventListener(t, e, n), o; }; } -var st = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 }); -O.connected = function(t, e) { - return e = st(e), function(r) { - return r.addEventListener(y, t, e), r[C] ? r : r.isConnected ? (r.dispatchEvent(new Event(y)), r) : (q(e.signal, () => D.offConnected(r, t)) && D.onConnected(r, t), r); +var it = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 }); +w.connected = function(t, e) { + return e = it(e), function(r) { + return r.addEventListener(_, t, e), r[C] ? r : r.isConnected ? (r.dispatchEvent(new Event(_)), r) : (q(e.signal, () => D.offConnected(r, t)) && D.onConnected(r, t), r); }; }; -O.disconnected = function(t, e) { - return e = st(e), function(r) { - return r.addEventListener(S, t, e), r[C] || q(e.signal, () => D.offDisconnected(r, t)) && D.onDisconnected(r, t), r; +w.disconnected = function(t, e) { + return e = it(e), function(r) { + return r.addEventListener(O, t, e), r[C] || q(e.signal, () => D.offDisconnected(r, t)) && D.onDisconnected(r, t), r; }; }; var Z = /* @__PURE__ */ new WeakMap(); -O.disconnectedAsAbort = function(t) { +w.disconnectedAsAbort = function(t) { if (Z.has(t)) return Z.get(t); let e = new AbortController(); - return Z.set(t, e), t(O.disconnected(() => e.abort())), e; + return Z.set(t, e), t(w.disconnected(() => e.abort())), e; }; -var _t = /* @__PURE__ */ new WeakSet(); -O.attributeChanged = function(t, e) { +var At = /* @__PURE__ */ new WeakSet(); +w.attributeChanged = function(t, e) { return typeof e != "object" && (e = {}), function(r) { - if (r.addEventListener(P, t, e), r[C] || _t.has(r) || !d.M) + if (r.addEventListener(M, t, e), r[C] || At.has(r) || !d.M) return r; - let o = new d.M(function(s) { - for (let { attributeName: a, target: h } of s) + let o = new d.M(function(i) { + for (let { attributeName: a, target: h } of i) h.dispatchEvent( - new CustomEvent(P, { detail: [a, h.getAttribute(a)] }) + new CustomEvent(M, { detail: [a, h.getAttribute(a)] }) ); }); return q(e.signal, () => o.disconnect()) && o.observe(r, { attributes: !0 }), r; }; }; -// src/observables-lib.js -var p = "__dde_observable"; +// src/signals-lib.js +var p = "__dde_signal"; function z(t) { try { return T(t, p); @@ -461,21 +461,21 @@ function z(t) { return !1; } } -var H = [], v = /* @__PURE__ */ new WeakMap(); +var H = [], g = /* @__PURE__ */ new WeakMap(); function E(t, e) { if (typeof t != "function") - return it(!1, t, e); + return st(!1, t, e); if (z(t)) return t; - let n = it(!0), r = function() { - let [o, ...c] = v.get(r); - if (v.set(r, /* @__PURE__ */ new Set([o])), H.push(r), dt(n, t()), H.pop(), !c.length) + let n = st(!0), r = function() { + let [o, ...c] = g.get(r); + if (g.set(r, /* @__PURE__ */ new Set([o])), H.push(r), dt(n, t()), H.pop(), !c.length) return; - let s = v.get(r); + let i = g.get(r); for (let a of c) - s.has(a) || L(a, r); + i.has(a) || L(a, r); }; - return v.set(n[p], r), v.set(r, /* @__PURE__ */ new Set([n])), r(), n; + return g.set(n[p], r), g.set(r, /* @__PURE__ */ new Set([n])), r(), n; } E.action = function(t, e, ...n) { let r = t[p], { actions: o } = r; @@ -494,8 +494,8 @@ E.on = function t(e, n, r = {}) { } }; E.symbols = { - //observable: mark, - onclear: Symbol.for("Observable.onclear") + //signal: mark, + onclear: Symbol.for("Signal.onclear") }; E.clear = function(...t) { for (let n of t) { @@ -504,37 +504,37 @@ E.clear = function(...t) { } function e(n, r) { r.listeners.forEach((o) => { - if (r.listeners.delete(o), !v.has(o)) + if (r.listeners.delete(o), !g.has(o)) return; - let c = v.get(o); - c.delete(n), !(c.size > 1) && (n.clear(...c), v.delete(o)); + let c = g.get(o); + c.delete(n), !(c.size > 1) && (n.clear(...c), g.delete(o)); }); } }; var R = "__dde_reactive"; E.el = function(t, e) { - let n = M.mark({ type: "reactive" }, !0), r = n.end, o = d.D.createDocumentFragment(); + let n = j.mark({ type: "reactive" }, !0), r = n.end, o = d.D.createDocumentFragment(); o.append(n, r); - let { current: c } = m, s = {}, a = (h) => { + let { current: c } = m, i = {}, a = (h) => { if (!n.parentNode || !r.parentNode) return L(t, a); - let g = s; - s = {}, m.push(c); + let v = i; + i = {}, m.push(c); let l = e(h, function(u, f) { let b; - return T(g, u) ? (b = g[u], delete g[u]) : b = f(), s[u] = b, b; + return T(v, u) ? (b = v[u], delete v[u]) : b = f(), i[u] = b, b; }); m.pop(), Array.isArray(l) || (l = [l]); let x = document.createComment(""); l.push(x), n.after(...l); - let w; - for (; (w = x.nextSibling) && w !== r; ) - w.remove(); - x.remove(), n.isConnected && At(c.host()); + let y; + for (; (y = x.nextSibling) && y !== r; ) + y.remove(); + x.remove(), n.isConnected && St(c.host()); }; return Q(t, a), ft(t, a, n, e), a(t()), o; }; -function At(t) { +function St(t) { !t || !t[R] || (requestIdleCallback || setTimeout)(function() { t[R] = t[R].filter(([e, n]) => n.isConnected ? !0 : (L(...e), !1)); }); @@ -544,7 +544,7 @@ var Ct = { this.value = t; } }; -function St(t) { +function Ot(t) { return function(e, n) { let r = (...c) => c.length ? e.setAttribute(n, ...c) : K(r), o = at(r, e.getAttribute(n), Ct); return t[n] = o, o; @@ -552,21 +552,21 @@ function St(t) { } var G = "__dde_attributes"; E.observedAttributes = function(t) { - let e = t[G] = {}, n = F(t, St(e)); - return O.attributeChanged(function({ detail: o }) { - /*! This maps attributes to observables (`O.observedAttributes`). + let e = t[G] = {}, n = F(t, Ot(e)); + return w.attributeChanged(function({ detail: o }) { + /*! This maps attributes to signals (`S.observedAttributes`). * Investigate `__dde_attributes` key of the element.*/ - let [c, s] = o, a = this[G][c]; + let [c, i] = o, a = this[G][c]; if (a) - return E.action(a, "_set", s); - })(t), O.disconnected(function() { - /*! This removes all observables mapped to attributes (`O.observedAttributes`). + return E.action(a, "_set", i); + })(t), w.disconnected(function() { + /*! This removes all signals mapped to attributes (`S.observedAttributes`). * Investigate `__dde_attributes` key of the element.*/ E.clear(...Object.values(this[G])); })(t), n; }; var ut = { - isObservable: z, + isSignal: z, processReactiveAttribute(t, e, n, r) { if (!z(n)) return n; @@ -581,18 +581,18 @@ var ut = { function ft(t, e, ...n) { let { current: r } = m; r.prevent || r.host(function(o) { - o[R] || (o[R] = [], O.disconnected( + o[R] || (o[R] = [], w.disconnected( () => ( /*! - * Clears all Observables listeners added in the current scope/host (`O.el`, `assign`, …?). + * Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?). * You can investigate the `__dde_reactive` key of the element. * */ - o[R].forEach(([[c, s]]) => L(c, s, c[p] && c[p].host && c[p].host() === o)) + o[R].forEach(([[c, i]]) => L(c, i, c[p] && c[p].host && c[p].host() === o)) ) )(o)), o[R].push([[t, e], ...n]); }); } -function it(t, e, n) { +function st(t, e, n) { let r = t ? () => K(r) : (...o) => o.length ? dt(r, ...o) : K(r); return at(r, e, n, t); } @@ -613,13 +613,13 @@ function at(t, e, n, r = !1) { X(n) !== "[object Object]" && (n = {}); let { onclear: c } = E.symbols; n[c] && (o.push(n[c]), delete n[c]); - let { host: s } = m; + let { host: i } = m; return Reflect.defineProperty(t, p, { value: { value: e, actions: n, onclear: o, - host: s, + host: i, listeners: /* @__PURE__ */ new Set(), defined: new V().stack, readonly: r @@ -636,7 +636,7 @@ function K(t) { if (!t[p]) return; let { value: e, listeners: n } = t[p], r = Rt(); - return r && n.add(r), v.has(r) && v.get(r).add(t), e; + return r && n.add(r), g.has(r) && g.get(r).add(t), e; } function dt(t, e, n) { if (!t[p]) @@ -655,41 +655,41 @@ function L(t, e, n) { return; let o = r.listeners.delete(e); if (n && !r.listeners.size) { - if (E.clear(t), !v.has(r)) + if (E.clear(t), !g.has(r)) return o; - let c = v.get(r); - if (!v.has(c)) + let c = g.get(r); + if (!g.has(c)) return o; - v.get(c).forEach((s) => L(s, c, !0)); + g.get(c).forEach((i) => L(i, c, !0)); } return o; } -// observables.js +// signals.js B(ut); globalThis.dde= { - O: E, - assign: j, + S: E, + assign: P, assignAttribute: nt, chainableAppend: ht, - classListDeclarative: vt, - createElement: M, + classListDeclarative: gt, + createElement: j, createElementNS: qt, customElementRender: Zt, - customElementWithDDE: wt, + customElementWithDDE: yt, dispatchEvent: Qt, - el: M, + el: j, elNS: qt, - elementAttribute: gt, + elementAttribute: vt, empty: Ft, - isObservable: z, - lifecyclesToEvents: wt, - observable: E, - observedAttributes: yt, - on: O, + isSignal: z, + lifecyclesToEvents: yt, + observedAttributes: _t, + on: w, registerReactivity: B, scope: m, + signal: E, simulateSlots: Wt }; diff --git a/dist/dde.js b/dist/dde.js index e91c83b..b9ebc55 100644 --- a/dist/dde.js +++ b/dist/dde.js @@ -1,8 +1,8 @@ //deka-dom-el library is available via global namespace `dde` (()=> { -// src/observables-common.js +// src/signals-common.js var C = { - isObservable(t) { + isSignal(t) { return !1; }, processReactiveAttribute(t, e, n, r) { @@ -57,18 +57,18 @@ function K(t, e, n) { return Reflect.set(t, e, ""); } } -var x = "__dde_lifecyclesToEvents", v = "dde:connected", y = "dde:disconnected", O = "dde:attributeChanged"; +var x = "__dde_lifecyclesToEvents", g = "dde:connected", y = "dde:disconnected", D = "dde:attributeChanged"; // src/dom.js -var g = [{ +var v = [{ get scope() { return a.D.body; }, host: (t) => t ? t(a.D.body) : a.D.body, prevent: !0 -}], R = { +}], S = { get current() { - return g[g.length - 1]; + return v[v.length - 1]; }, get host() { return this.current.host; @@ -78,17 +78,17 @@ var g = [{ return t.prevent = !0, t; }, get state() { - return [...g]; + return [...v]; }, push(t = {}) { - return g.push(Object.assign({}, this.current, { prevent: !1 }, t)); + return v.push(Object.assign({}, this.current, { prevent: !1 }, t)); }, pushRoot() { - return g.push(g[0]); + return v.push(v[0]); }, pop() { - if (g.length !== 1) - return g.pop(); + if (v.length !== 1) + return v.pop(); } }; function $(...t) { @@ -100,9 +100,9 @@ function Q(t) { var T; function k(t, e, ...n) { let r = L(this), o = 0, c, f; - switch ((Object(e) !== e || r.isObservable(e)) && (e = { textContent: e }), !0) { + switch ((Object(e) !== e || r.isSignal(e)) && (e = { textContent: e }), !0) { case typeof t == "function": { - o = 1, R.push({ scope: t, host: (...b) => b.length ? (o === 1 ? n.unshift(...b) : b.forEach((l) => l(f)), void 0) : f }), c = t(e || void 0); + o = 1, S.push({ scope: t, host: (...b) => b.length ? (o === 1 ? n.unshift(...b) : b.forEach((l) => l(f)), void 0) : f }), c = t(e || void 0); let d = c instanceof a.F; if (c.nodeName === "#comment") break; @@ -115,18 +115,18 @@ function k(t, e, ...n) { break; } case t === "#text": - c = D.call(this, a.D.createTextNode(""), e); + c = O.call(this, a.D.createTextNode(""), e); break; case (t === "<>" || !t): - c = D.call(this, a.D.createDocumentFragment(), e); + c = O.call(this, a.D.createDocumentFragment(), e); break; case !!T: - c = D.call(this, a.D.createElementNS(T, t), e); + c = O.call(this, a.D.createElementNS(T, t), e); break; case !c: - c = D.call(this, a.D.createElement(t), e); + c = O.call(this, a.D.createElement(t), e); } - return Q(c), f || (f = c), n.forEach((d) => d(f)), o && R.pop(), o = 2, c; + return Q(c), f || (f = c), n.forEach((d) => d(f)), o && S.pop(), o = 2, c; } function bt(t, e = t, n = void 0) { let r = Symbol.for("default"), o = Array.from(e.querySelectorAll("slot")).reduce((f, d) => Reflect.set(f, d.name || r, d) && f, {}), c = q(o, r); @@ -154,7 +154,7 @@ function bt(t, e = t, n = void 0) { function X(t, e, n) { n && n(t, e); try { - t.replaceWith(D(e, { className: [e.className, t.className], dataset: { ...t.dataset } })); + t.replaceWith(O(e, { className: [e.className, t.className], dataset: { ...t.dataset } })); } catch { t.replaceWith(e); } @@ -164,7 +164,7 @@ k.mark = function(t, e = !1) { let n = e ? "" : "/", r = a.D.createComment(``); return e && (r.end = a.D.createComment("")), r; }; -function vt(t) { +function gt(t) { let e = this; return function(...r) { T = t; @@ -173,7 +173,7 @@ function vt(t) { }; } var P = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: U } = a; -function D(t, ...e) { +function O(t, ...e) { if (!e.length) return t; P.set(t, B(t, this)); @@ -227,7 +227,7 @@ function Y(t, e) { (r, o) => t.classList.toggle(r, o === -1 ? void 0 : !!o) ), t; } -function gt(t) { +function vt(t) { return Array.from(t.children).forEach((e) => e.remove()), t; } function tt(t, e, n, r) { @@ -352,7 +352,7 @@ function ot() { if (u && b(h).then(l), !t.has(h)) continue; let m = t.get(h); - m.length_c && (h.dispatchEvent(new Event(v)), m.connected = /* @__PURE__ */ new WeakSet(), m.length_c = 0, m.length_d || t.delete(h), s = !0); + m.length_c && (h.dispatchEvent(new Event(g)), m.connected = /* @__PURE__ */ new WeakSet(), m.length_c = 0, m.length_d || t.delete(h), s = !0); } return s; } @@ -370,26 +370,26 @@ function ot() { } // src/customElement.js -function Ot(t, e, n, r = it) { - R.push({ +function Dt(t, e, n, r = it) { + S.push({ scope: t, host: (...f) => f.length ? f.forEach((d) => d(t)) : t }), typeof r == "function" && (r = r.call(t, t)); let o = t[x]; o || ct(t); let c = n.call(t, r); - return o || t.dispatchEvent(new Event(v)), e.nodeType === 11 && typeof e.mode == "string" && t.addEventListener(y, w.observe(e), { once: !0 }), R.pop(), e.append(c); + return o || t.dispatchEvent(new Event(g)), e.nodeType === 11 && typeof e.mode == "string" && t.addEventListener(y, w.observe(e), { once: !0 }), S.pop(), e.append(c); } function ct(t) { return W(t.prototype, "connectedCallback", function(e, n, r) { - e.apply(n, r), n.dispatchEvent(new Event(v)); + e.apply(n, r), n.dispatchEvent(new Event(g)); }), W(t.prototype, "disconnectedCallback", function(e, n, r) { e.apply(n, r), (globalThis.queueMicrotask || setTimeout)( () => !n.isConnected && n.dispatchEvent(new Event(y)) ); }), W(t.prototype, "attributeChangedCallback", function(e, n, r) { let [o, , c] = r; - n.dispatchEvent(new CustomEvent(O, { + n.dispatchEvent(new CustomEvent(D, { detail: [o, c] })), e.apply(n, r); }), t.prototype[x] = !0, t; @@ -410,38 +410,38 @@ function _t(t, e, n) { return o.dispatchEvent(f); }; } -function S(t, e, n) { +function R(t, e, n) { return function(o) { return o.addEventListener(t, e, n), o; }; } var G = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 }); -S.connected = function(t, e) { +R.connected = function(t, e) { return e = G(e), function(r) { - return r.addEventListener(v, t, e), r[x] ? r : r.isConnected ? (r.dispatchEvent(new Event(v)), r) : (N(e.signal, () => w.offConnected(r, t)) && w.onConnected(r, t), r); + return r.addEventListener(g, t, e), r[x] ? r : r.isConnected ? (r.dispatchEvent(new Event(g)), r) : (N(e.signal, () => w.offConnected(r, t)) && w.onConnected(r, t), r); }; }; -S.disconnected = function(t, e) { +R.disconnected = function(t, e) { return e = G(e), function(r) { return r.addEventListener(y, t, e), r[x] || N(e.signal, () => w.offDisconnected(r, t)) && w.onDisconnected(r, t), r; }; }; var j = /* @__PURE__ */ new WeakMap(); -S.disconnectedAsAbort = function(t) { +R.disconnectedAsAbort = function(t) { if (j.has(t)) return j.get(t); let e = new AbortController(); - return j.set(t, e), t(S.disconnected(() => e.abort())), e; + return j.set(t, e), t(R.disconnected(() => e.abort())), e; }; var st = /* @__PURE__ */ new WeakSet(); -S.attributeChanged = function(t, e) { +R.attributeChanged = function(t, e) { return typeof e != "object" && (e = {}), function(r) { - if (r.addEventListener(O, t, e), r[x] || st.has(r) || !a.M) + if (r.addEventListener(D, t, e), r[x] || st.has(r) || !a.M) return r; let o = new a.M(function(f) { for (let { attributeName: d, target: p } of f) p.dispatchEvent( - new CustomEvent(O, { detail: [d, p.getAttribute(d)] }) + new CustomEvent(D, { detail: [d, p.getAttribute(d)] }) ); }); return N(e.signal, () => o.disconnect()) && o.observe(r, { attributes: !0 }), r; @@ -449,24 +449,24 @@ S.attributeChanged = function(t, e) { }; globalThis.dde= { - assign: D, + assign: O, assignAttribute: z, chainableAppend: Q, classListDeclarative: Y, createElement: k, - createElementNS: vt, - customElementRender: Ot, + createElementNS: gt, + customElementRender: Dt, customElementWithDDE: ct, dispatchEvent: _t, el: k, - elNS: vt, + elNS: gt, elementAttribute: tt, - empty: gt, + empty: vt, lifecyclesToEvents: ct, observedAttributes: it, - on: S, + on: R, registerReactivity: V, - scope: R, + scope: S, simulateSlots: bt }; diff --git a/dist/esm-with-observables.d.ts b/dist/esm-with-signals.d.ts similarity index 92% rename from dist/esm-with-observables.d.ts rename to dist/esm-with-signals.d.ts index fbe39ce..672e3fa 100644 --- a/dist/esm-with-observables.d.ts +++ b/dist/esm-with-signals.d.ts @@ -1,17 +1,17 @@ -export type Observable= (set?: V)=> V & A; -type Action= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof observable._ | void; -//type SymbolObservable= Symbol; +export type Signal= (set?: V)=> V & A; +type Action= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void; +//type SymbolSignal= Symbol; type SymbolOnclear= symbol; type Actions= Record>; type OnListenerOptions= Pick & { first_time?: boolean }; -interface observable{ +interface signal{ _: Symbol /** * Simple example: * ```js - * const hello= S("Hello Observable"); + * const hello= S("Hello Signal"); * ``` - * …simple todo observable: + * …simple todo signal: * ```js * const todos= S([], { * add(v){ this.value.push(S(v)); }, @@ -19,48 +19,48 @@ interface observable{ * [S.symbols.onclear](){ S.clear(...this.value); }, * }); * ``` - * …computed observable: + * …computed signal: * ```js * const name= S("Jan"); * const surname= S("Andrle"); * const fullname= S(()=> name()+" "+surname()); * ``` - * @param value Initial observable value. Or function computing value from other observables. - * @param actions Use to define actions on the observable. Such as add item to the array. - * There is also a reserved function `S.symbol.onclear` which is called when the observable is cleared + * @param value Initial signal value. Or function computing value from other signals. + * @param actions Use to define actions on the signal. Such as add item to the array. + * There is also a reserved function `S.symbol.onclear` which is called when the signal is cleared * by `S.clear`. * */ - >(value: V, actions?: A): Observable; + >(value: V, actions?: A): Signal; /** - * Computations observable. This creates a observable which is computed from other observables. + * Computations signal. This creates a signal which is computed from other signals. * */ - (computation: ()=> V): Observable - action>, A extends (S extends Observable ? A : never), N extends keyof A>( - observable: S, + (computation: ()=> V): Signal + action>, A extends (S extends Signal ? A : never), N extends keyof A>( + signal: S, name: N, ...params: A[N] extends (...args: infer P)=> any ? P : never ): void; - clear(...observables: Observable[]): void; - on(observable: Observable, onchange: (a: T)=> void, options?: OnListenerOptions): void; + clear(...signals: Signal[]): void; + on(signal: Signal, onchange: (a: T)=> void, options?: OnListenerOptions): void; symbols: { - //observable: SymbolObservable; + //signal: SymbolSignal; onclear: SymbolOnclear; } /** - * Reactive element, which is rendered based on the given observable. + * Reactive element, which is rendered based on the given signal. * ```js - * S.el(observable, value=> value ? el("b", "True") : el("i", "False")); + * S.el(signal, value=> value ? el("b", "True") : el("i", "False")); * S.el(listS, list=> list.map(li=> el("li", li))); * ``` * */ - el(observable: Observable, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment; + el(signal: Signal, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment; - observedAttributes(custom_element: HTMLElement): Record>; + observedAttributes(custom_element: HTMLElement): Record>; } -export const observable: observable; -export const O: observable; +export const signal: signal; +export const S: signal; declare global { - type ddeObservable= Observable; + type ddeSignal= Signal; type ddeAction= Action type ddeActions= Actions } @@ -80,20 +80,20 @@ type AttrsModified= { /** * Use string like in HTML (internally uses `*.setAttribute("style", *)`), or object representation (like DOM API). */ - style: string | Partial | Observable | Partial<{ [K in keyof CSSStyleDeclaration]: Observable }> + style: string | Partial | Signal | Partial<{ [K in keyof CSSStyleDeclaration]: Signal }> /** * Provide option to add/remove/toggle CSS clasess (index of object) using 1/0/-1. In fact `el.classList.toggle(class_name)` for `-1` and `el.classList.toggle(class_name, Boolean(...))` for others. */ - classList: Record>, + classList: Record>, /** * By default simiral to `className`, but also supports `string[]` * */ - className: string | (string|boolean|undefined|Observable)[]; + className: string | (string|boolean|undefined|Signal)[]; /** * Sets `aria-*` simiraly to `dataset` * */ - ariaset: Record>, -} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, string|Observable> & Record<`.${string}`, any> + ariaset: Record>, +} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, string|Signal> & Record<`.${string}`, any> type _fromElsInterfaces= Omit; /** * Just element attributtes @@ -103,13 +103,13 @@ type _fromElsInterfaces= Omit= Partial<_fromElsInterfaces & { [K in keyof _fromElsInterfaces]: Observable<_fromElsInterfaces[K], any> } & AttrsModified> & Record; +type ElementAttributes= Partial<_fromElsInterfaces & { [K in keyof _fromElsInterfaces]: Signal<_fromElsInterfaces[K], any> } & AttrsModified> & Record; export function classListDeclarative(element: El, classList: AttrsModified["classList"]): El export function assign(element: El, ...attrs_array: ElementAttributes[]): El export function assignAttribute>(element: El, attr: ATT, value: ElementAttributes[ATT]): ElementAttributes[ATT] type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap; -type textContent= string | ( (set?: string)=> string ); // TODO: for some reason `Observable` leads to `attrs?: any` +type textContent= string | ( (set?: string)=> string ); // TODO: for some reason `Signal` leads to `attrs?: any` export function el< TAG extends keyof ExtendedHTMLElementTagNameMap & string, EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : HTMLElement) @@ -148,7 +148,7 @@ export function elNS( EL extends ( TAG extends keyof MathMLElementTagNameMap ? MathMLElementTagNameMap[TAG] : MathMLElement ), >( tag_name: TAG, - attrs?: string | textContent | Partial<{ [key in keyof EL]: EL[key] | Observable | string | number | boolean }>, + attrs?: string | textContent | Partial<{ [key in keyof EL]: EL[key] | Signal | string | number | boolean }>, ...addons: ddeElementAddon[] )=> ddeMathMLElement export function elNS( diff --git a/dist/esm-with-observables.js b/dist/esm-with-signals.js similarity index 64% rename from dist/esm-with-observables.js rename to dist/esm-with-signals.js index a165f83..e441a79 100644 --- a/dist/esm-with-observables.js +++ b/dist/esm-with-signals.js @@ -1,6 +1,6 @@ -// src/observables-common.js +// src/signals-common.js var k = { - isObservable(t) { + isSignal(t) { return !1; }, processReactiveAttribute(t, e, n, r) { @@ -16,7 +16,7 @@ function W(t) { // src/helpers.js var T = (...t) => Object.prototype.hasOwnProperty.call(...t); -function A(t) { +function S(t) { return typeof t > "u"; } function X(t) { @@ -52,17 +52,17 @@ var d = { M: globalThis.MutationObserver }; function lt(t, e, n) { - if (Reflect.set(t, e, n), !!A(n)) { + if (Reflect.set(t, e, n), !!S(n)) { if (Reflect.deleteProperty(t, e), t instanceof d.H && t.getAttribute(e) === "undefined") return t.removeAttribute(e); if (Reflect.get(t, e) === "undefined") return Reflect.set(t, e, ""); } } -var C = "__dde_lifecyclesToEvents", y = "dde:connected", S = "dde:disconnected", P = "dde:attributeChanged"; +var C = "__dde_lifecyclesToEvents", _ = "dde:connected", O = "dde:disconnected", M = "dde:attributeChanged"; // src/dom.js -var _ = [{ +var A = [{ get scope() { return d.D.body; }, @@ -70,7 +70,7 @@ var _ = [{ prevent: !0 }], m = { get current() { - return _[_.length - 1]; + return A[A.length - 1]; }, get host() { return this.current.host; @@ -80,17 +80,17 @@ var _ = [{ return t.prevent = !0, t; }, get state() { - return [..._]; + return [...A]; }, push(t = {}) { - return _.push(Object.assign({}, this.current, { prevent: !1 }, t)); + return A.push(Object.assign({}, this.current, { prevent: !1 }, t)); }, pushRoot() { - return _.push(_[0]); + return A.push(A[0]); }, pop() { - if (_.length !== 1) - return _.pop(); + if (A.length !== 1) + return A.pop(); } }; function Y(...t) { @@ -100,68 +100,68 @@ function ht(t) { return t.append === Y || (t.appendOriginal = t.append, t.append = Y), t; } var $; -function M(t, e, ...n) { - let r = W(this), o = 0, c, s; - switch ((Object(e) !== e || r.isObservable(e)) && (e = { textContent: e }), !0) { +function j(t, e, ...n) { + let r = W(this), o = 0, c, i; + switch ((Object(e) !== e || r.isSignal(e)) && (e = { textContent: e }), !0) { case typeof t == "function": { - o = 1, m.push({ scope: t, host: (...g) => g.length ? (o === 1 ? n.unshift(...g) : g.forEach((l) => l(s)), void 0) : s }), c = t(e || void 0); + o = 1, m.push({ scope: t, host: (...v) => v.length ? (o === 1 ? n.unshift(...v) : v.forEach((l) => l(i)), void 0) : i }), c = t(e || void 0); let a = c instanceof d.F; if (c.nodeName === "#comment") break; - let h = M.mark({ + let h = j.mark({ type: "component", name: t.name, host: a ? "this" : "parentElement" }); - c.prepend(h), a && (s = h); + c.prepend(h), a && (i = h); break; } case t === "#text": - c = j.call(this, d.D.createTextNode(""), e); + c = P.call(this, d.D.createTextNode(""), e); break; case (t === "<>" || !t): - c = j.call(this, d.D.createDocumentFragment(), e); + c = P.call(this, d.D.createDocumentFragment(), e); break; case !!$: - c = j.call(this, d.D.createElementNS($, t), e); + c = P.call(this, d.D.createElementNS($, t), e); break; case !c: - c = j.call(this, d.D.createElement(t), e); + c = P.call(this, d.D.createElement(t), e); } - return ht(c), s || (s = c), n.forEach((a) => a(s)), o && m.pop(), o = 2, c; + return ht(c), i || (i = c), n.forEach((a) => a(i)), o && m.pop(), o = 2, c; } function Wt(t, e = t, n = void 0) { - let r = Symbol.for("default"), o = Array.from(e.querySelectorAll("slot")).reduce((s, a) => Reflect.set(s, a.name || r, a) && s, {}), c = T(o, r); + let r = Symbol.for("default"), o = Array.from(e.querySelectorAll("slot")).reduce((i, a) => Reflect.set(i, a.name || r, a) && i, {}), c = T(o, r); if (t.append = new Proxy(t.append, { - apply(s, a, h) { + apply(i, a, h) { if (!h.length) return t; - let g = d.D.createDocumentFragment(); + let v = d.D.createDocumentFragment(); for (let l of h) { if (!l || !l.slot) { - c && g.appendChild(l); + c && v.appendChild(l); continue; } - let x = l.slot, w = o[x]; - gt(l, "remove", "slot"), w && (bt(w, l, n), Reflect.deleteProperty(o, x)); + let x = l.slot, y = o[x]; + vt(l, "remove", "slot"), y && (bt(y, l, n), Reflect.deleteProperty(o, x)); } - return c && (o[r].replaceWith(g), Reflect.deleteProperty(o, r)), t.append = s, t; + return c && (o[r].replaceWith(v), Reflect.deleteProperty(o, r)), t.append = i, t; } }), t !== e) { - let s = Array.from(t.childNodes); - s.forEach((a) => a.remove()), t.append(...s); + let i = Array.from(t.childNodes); + i.forEach((a) => a.remove()), t.append(...i); } return e; } function bt(t, e, n) { n && n(t, e); try { - t.replaceWith(j(e, { className: [e.className, t.className], dataset: { ...t.dataset } })); + t.replaceWith(P(e, { className: [e.className, t.className], dataset: { ...t.dataset } })); } catch { t.replaceWith(e); } } -M.mark = function(t, e = !1) { +j.mark = function(t, e = !1) { t = Object.entries(t).map(([o, c]) => o + `="${c}"`).join(" "); let n = e ? "" : "/", r = d.D.createComment(``); return e && (r.end = d.D.createComment("")), r; @@ -170,12 +170,12 @@ function qt(t) { let e = this; return function(...r) { $ = t; - let o = M.call(e, ...r); + let o = j.call(e, ...r); return $ = void 0, o; }; } var U = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: tt } = d; -function j(t, ...e) { +function P(t, ...e) { if (!e.length) return t; U.set(t, rt(t, this)); @@ -191,10 +191,10 @@ function nt(t, e, n) { n, (a, h) => nt.call(c, t, a, h) ); - let [s] = e; - if (s === "=") + let [i] = e; + if (i === "=") return r(e.slice(1), n); - if (s === ".") + if (i === ".") return et(t, e.slice(1), n); if (/(aria|data)([A-Z])/.test(e)) return e = e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), r(e, n); @@ -211,7 +211,7 @@ function nt(t, e, n) { case "ariaset": return I(o, n, (a, h) => r("aria-" + a, h)); case "classList": - return vt.call(c, t, n); + return gt.call(c, t, n); } return Et(t, e) ? tt(t, e, n) : r(e, n); } @@ -221,7 +221,7 @@ function rt(t, e) { let r = (t instanceof d.S ? xt : mt).bind(null, t, "Attribute"), o = W(e); return { setRemoveAttr: r, s: o }; } -function vt(t, e) { +function gt(t, e) { let n = W(this); return I( n, @@ -232,14 +232,14 @@ function vt(t, e) { function Ft(t) { return Array.from(t.children).forEach((e) => e.remove()), t; } -function gt(t, e, n, r) { +function vt(t, e, n, r) { return t instanceof d.H ? t[e + "Attribute"](n, r) : t[e + "AttributeNS"](null, n, r); } function Et(t, e) { if (!(e in t)) return !1; let n = ot(t, e); - return !A(n.set); + return !S(n.set); } function ot(t, e) { if (t = Object.getPrototypeOf(t), !t) @@ -257,141 +257,141 @@ function ct(t) { return Array.isArray(t) ? t.filter(Boolean).join(" ") : t; } function mt(t, e, n, r) { - return t[(A(r) ? "remove" : "set") + e](n, ct(r)); + return t[(S(r) ? "remove" : "set") + e](n, ct(r)); } function xt(t, e, n, r, o = null) { - return t[(A(r) ? "remove" : "set") + e + "NS"](o, n, ct(r)); + return t[(S(r) ? "remove" : "set") + e + "NS"](o, n, ct(r)); } function et(t, e, n) { - if (Reflect.set(t, e, n), !!A(n)) + if (Reflect.set(t, e, n), !!S(n)) return Reflect.deleteProperty(t, e); } // src/events-observer.js -var D = d.M ? Ot() : new Proxy({}, { +var D = d.M ? wt() : new Proxy({}, { get() { return () => { }; } }); -function Ot() { - let t = /* @__PURE__ */ new Map(), e = !1, n = (i) => function(u) { +function wt() { + let t = /* @__PURE__ */ new Map(), e = !1, n = (s) => function(u) { for (let f of u) if (f.type === "childList") { if (l(f.addedNodes, !0)) { - i(); + s(); continue; } - x(f.removedNodes, !0) && i(); + x(f.removedNodes, !0) && s(); } }, r = new d.M(n(a)); return { - observe(i) { + observe(s) { let u = new d.M(n(() => { })); - return u.observe(i, { childList: !0, subtree: !0 }), () => u.disconnect(); + return u.observe(s, { childList: !0, subtree: !0 }), () => u.disconnect(); }, - onConnected(i, u) { - s(); - let f = c(i); + onConnected(s, u) { + i(); + let f = c(s); f.connected.has(u) || (f.connected.add(u), f.length_c += 1); }, - offConnected(i, u) { - if (!t.has(i)) + offConnected(s, u) { + if (!t.has(s)) return; - let f = t.get(i); - f.connected.has(u) && (f.connected.delete(u), f.length_c -= 1, o(i, f)); + let f = t.get(s); + f.connected.has(u) && (f.connected.delete(u), f.length_c -= 1, o(s, f)); }, - onDisconnected(i, u) { - s(); - let f = c(i); + onDisconnected(s, u) { + i(); + let f = c(s); f.disconnected.has(u) || (f.disconnected.add(u), f.length_d += 1); }, - offDisconnected(i, u) { - if (!t.has(i)) + offDisconnected(s, u) { + if (!t.has(s)) return; - let f = t.get(i); - f.disconnected.has(u) && (f.disconnected.delete(u), f.length_d -= 1, o(i, f)); + let f = t.get(s); + f.disconnected.has(u) && (f.disconnected.delete(u), f.length_d -= 1, o(s, f)); } }; - function o(i, u) { - u.length_c || u.length_d || (t.delete(i), a()); + function o(s, u) { + u.length_c || u.length_d || (t.delete(s), a()); } - function c(i) { - if (t.has(i)) - return t.get(i); + function c(s) { + if (t.has(s)) + return t.get(s); let u = { connected: /* @__PURE__ */ new WeakSet(), length_c: 0, disconnected: /* @__PURE__ */ new WeakSet(), length_d: 0 }; - return t.set(i, u), u; + return t.set(s, u), u; } - function s() { + function i() { e || (e = !0, r.observe(d.D.body, { childList: !0, subtree: !0 })); } function a() { !e || t.size || (e = !1, r.disconnect()); } function h() { - return new Promise(function(i) { - (requestIdleCallback || requestAnimationFrame)(i); + return new Promise(function(s) { + (requestIdleCallback || requestAnimationFrame)(s); }); } - async function g(i) { + async function v(s) { t.size > 30 && await h(); let u = []; - if (!(i instanceof Node)) + if (!(s instanceof Node)) return u; for (let f of t.keys()) - f === i || !(f instanceof Node) || i.contains(f) && u.push(f); + f === s || !(f instanceof Node) || s.contains(f) && u.push(f); return u; } - function l(i, u) { + function l(s, u) { let f = !1; - for (let b of i) { - if (u && g(b).then(l), !t.has(b)) + for (let b of s) { + if (u && v(b).then(l), !t.has(b)) continue; let N = t.get(b); - N.length_c && (b.dispatchEvent(new Event(y)), N.connected = /* @__PURE__ */ new WeakSet(), N.length_c = 0, N.length_d || t.delete(b), f = !0); + N.length_c && (b.dispatchEvent(new Event(_)), N.connected = /* @__PURE__ */ new WeakSet(), N.length_c = 0, N.length_d || t.delete(b), f = !0); } return f; } - function x(i, u) { + function x(s, u) { let f = !1; - for (let b of i) - u && g(b).then(x), !(!t.has(b) || !t.get(b).length_d) && ((globalThis.queueMicrotask || setTimeout)(w(b)), f = !0); + for (let b of s) + u && v(b).then(x), !(!t.has(b) || !t.get(b).length_d) && ((globalThis.queueMicrotask || setTimeout)(y(b)), f = !0); return f; } - function w(i) { + function y(s) { return () => { - i.isConnected || (i.dispatchEvent(new Event(S)), t.delete(i)); + s.isConnected || (s.dispatchEvent(new Event(O)), t.delete(s)); }; } } // src/customElement.js -function Zt(t, e, n, r = yt) { +function Zt(t, e, n, r = _t) { m.push({ scope: t, - host: (...s) => s.length ? s.forEach((a) => a(t)) : t + host: (...i) => i.length ? i.forEach((a) => a(t)) : t }), typeof r == "function" && (r = r.call(t, t)); let o = t[C]; - o || wt(t); + o || yt(t); let c = n.call(t, r); - return o || t.dispatchEvent(new Event(y)), e.nodeType === 11 && typeof e.mode == "string" && t.addEventListener(S, D.observe(e), { once: !0 }), m.pop(), e.append(c); + return o || t.dispatchEvent(new Event(_)), e.nodeType === 11 && typeof e.mode == "string" && t.addEventListener(O, D.observe(e), { once: !0 }), m.pop(), e.append(c); } -function wt(t) { +function yt(t) { return J(t.prototype, "connectedCallback", function(e, n, r) { - e.apply(n, r), n.dispatchEvent(new Event(y)); + e.apply(n, r), n.dispatchEvent(new Event(_)); }), J(t.prototype, "disconnectedCallback", function(e, n, r) { e.apply(n, r), (globalThis.queueMicrotask || setTimeout)( - () => !n.isConnected && n.dispatchEvent(new Event(S)) + () => !n.isConnected && n.dispatchEvent(new Event(O)) ); }), J(t.prototype, "attributeChangedCallback", function(e, n, r) { let [o, , c] = r; - n.dispatchEvent(new CustomEvent(P, { + n.dispatchEvent(new CustomEvent(M, { detail: [o, c] })), e.apply(n, r); }), t.prototype[C] = !0, t; @@ -400,7 +400,7 @@ function J(t, e, n) { t[e] = new Proxy(t[e] || (() => { }), { apply: n }); } -function yt(t) { +function _t(t) { return F(t, (e, n) => e.getAttribute(n)); } @@ -408,50 +408,50 @@ function yt(t) { function Qt(t, e, n) { return e || (e = {}), function(o, ...c) { n && (c.unshift(o), o = typeof n == "function" ? n() : n); - let s = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e); - return o.dispatchEvent(s); + let i = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e); + return o.dispatchEvent(i); }; } -function O(t, e, n) { +function w(t, e, n) { return function(o) { return o.addEventListener(t, e, n), o; }; } -var st = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 }); -O.connected = function(t, e) { - return e = st(e), function(r) { - return r.addEventListener(y, t, e), r[C] ? r : r.isConnected ? (r.dispatchEvent(new Event(y)), r) : (q(e.signal, () => D.offConnected(r, t)) && D.onConnected(r, t), r); +var it = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 }); +w.connected = function(t, e) { + return e = it(e), function(r) { + return r.addEventListener(_, t, e), r[C] ? r : r.isConnected ? (r.dispatchEvent(new Event(_)), r) : (q(e.signal, () => D.offConnected(r, t)) && D.onConnected(r, t), r); }; }; -O.disconnected = function(t, e) { - return e = st(e), function(r) { - return r.addEventListener(S, t, e), r[C] || q(e.signal, () => D.offDisconnected(r, t)) && D.onDisconnected(r, t), r; +w.disconnected = function(t, e) { + return e = it(e), function(r) { + return r.addEventListener(O, t, e), r[C] || q(e.signal, () => D.offDisconnected(r, t)) && D.onDisconnected(r, t), r; }; }; var Z = /* @__PURE__ */ new WeakMap(); -O.disconnectedAsAbort = function(t) { +w.disconnectedAsAbort = function(t) { if (Z.has(t)) return Z.get(t); let e = new AbortController(); - return Z.set(t, e), t(O.disconnected(() => e.abort())), e; + return Z.set(t, e), t(w.disconnected(() => e.abort())), e; }; -var _t = /* @__PURE__ */ new WeakSet(); -O.attributeChanged = function(t, e) { +var At = /* @__PURE__ */ new WeakSet(); +w.attributeChanged = function(t, e) { return typeof e != "object" && (e = {}), function(r) { - if (r.addEventListener(P, t, e), r[C] || _t.has(r) || !d.M) + if (r.addEventListener(M, t, e), r[C] || At.has(r) || !d.M) return r; - let o = new d.M(function(s) { - for (let { attributeName: a, target: h } of s) + let o = new d.M(function(i) { + for (let { attributeName: a, target: h } of i) h.dispatchEvent( - new CustomEvent(P, { detail: [a, h.getAttribute(a)] }) + new CustomEvent(M, { detail: [a, h.getAttribute(a)] }) ); }); return q(e.signal, () => o.disconnect()) && o.observe(r, { attributes: !0 }), r; }; }; -// src/observables-lib.js -var p = "__dde_observable"; +// src/signals-lib.js +var p = "__dde_signal"; function z(t) { try { return T(t, p); @@ -459,21 +459,21 @@ function z(t) { return !1; } } -var H = [], v = /* @__PURE__ */ new WeakMap(); +var H = [], g = /* @__PURE__ */ new WeakMap(); function E(t, e) { if (typeof t != "function") - return it(!1, t, e); + return st(!1, t, e); if (z(t)) return t; - let n = it(!0), r = function() { - let [o, ...c] = v.get(r); - if (v.set(r, /* @__PURE__ */ new Set([o])), H.push(r), dt(n, t()), H.pop(), !c.length) + let n = st(!0), r = function() { + let [o, ...c] = g.get(r); + if (g.set(r, /* @__PURE__ */ new Set([o])), H.push(r), dt(n, t()), H.pop(), !c.length) return; - let s = v.get(r); + let i = g.get(r); for (let a of c) - s.has(a) || L(a, r); + i.has(a) || L(a, r); }; - return v.set(n[p], r), v.set(r, /* @__PURE__ */ new Set([n])), r(), n; + return g.set(n[p], r), g.set(r, /* @__PURE__ */ new Set([n])), r(), n; } E.action = function(t, e, ...n) { let r = t[p], { actions: o } = r; @@ -492,8 +492,8 @@ E.on = function t(e, n, r = {}) { } }; E.symbols = { - //observable: mark, - onclear: Symbol.for("Observable.onclear") + //signal: mark, + onclear: Symbol.for("Signal.onclear") }; E.clear = function(...t) { for (let n of t) { @@ -502,37 +502,37 @@ E.clear = function(...t) { } function e(n, r) { r.listeners.forEach((o) => { - if (r.listeners.delete(o), !v.has(o)) + if (r.listeners.delete(o), !g.has(o)) return; - let c = v.get(o); - c.delete(n), !(c.size > 1) && (n.clear(...c), v.delete(o)); + let c = g.get(o); + c.delete(n), !(c.size > 1) && (n.clear(...c), g.delete(o)); }); } }; var R = "__dde_reactive"; E.el = function(t, e) { - let n = M.mark({ type: "reactive" }, !0), r = n.end, o = d.D.createDocumentFragment(); + let n = j.mark({ type: "reactive" }, !0), r = n.end, o = d.D.createDocumentFragment(); o.append(n, r); - let { current: c } = m, s = {}, a = (h) => { + let { current: c } = m, i = {}, a = (h) => { if (!n.parentNode || !r.parentNode) return L(t, a); - let g = s; - s = {}, m.push(c); + let v = i; + i = {}, m.push(c); let l = e(h, function(u, f) { let b; - return T(g, u) ? (b = g[u], delete g[u]) : b = f(), s[u] = b, b; + return T(v, u) ? (b = v[u], delete v[u]) : b = f(), i[u] = b, b; }); m.pop(), Array.isArray(l) || (l = [l]); let x = document.createComment(""); l.push(x), n.after(...l); - let w; - for (; (w = x.nextSibling) && w !== r; ) - w.remove(); - x.remove(), n.isConnected && At(c.host()); + let y; + for (; (y = x.nextSibling) && y !== r; ) + y.remove(); + x.remove(), n.isConnected && St(c.host()); }; return Q(t, a), ft(t, a, n, e), a(t()), o; }; -function At(t) { +function St(t) { !t || !t[R] || (requestIdleCallback || setTimeout)(function() { t[R] = t[R].filter(([e, n]) => n.isConnected ? !0 : (L(...e), !1)); }); @@ -542,7 +542,7 @@ var Ct = { this.value = t; } }; -function St(t) { +function Ot(t) { return function(e, n) { let r = (...c) => c.length ? e.setAttribute(n, ...c) : K(r), o = at(r, e.getAttribute(n), Ct); return t[n] = o, o; @@ -550,21 +550,21 @@ function St(t) { } var G = "__dde_attributes"; E.observedAttributes = function(t) { - let e = t[G] = {}, n = F(t, St(e)); - return O.attributeChanged(function({ detail: o }) { - /*! This maps attributes to observables (`O.observedAttributes`). + let e = t[G] = {}, n = F(t, Ot(e)); + return w.attributeChanged(function({ detail: o }) { + /*! This maps attributes to signals (`S.observedAttributes`). * Investigate `__dde_attributes` key of the element.*/ - let [c, s] = o, a = this[G][c]; + let [c, i] = o, a = this[G][c]; if (a) - return E.action(a, "_set", s); - })(t), O.disconnected(function() { - /*! This removes all observables mapped to attributes (`O.observedAttributes`). + return E.action(a, "_set", i); + })(t), w.disconnected(function() { + /*! This removes all signals mapped to attributes (`S.observedAttributes`). * Investigate `__dde_attributes` key of the element.*/ E.clear(...Object.values(this[G])); })(t), n; }; var ut = { - isObservable: z, + isSignal: z, processReactiveAttribute(t, e, n, r) { if (!z(n)) return n; @@ -579,18 +579,18 @@ var ut = { function ft(t, e, ...n) { let { current: r } = m; r.prevent || r.host(function(o) { - o[R] || (o[R] = [], O.disconnected( + o[R] || (o[R] = [], w.disconnected( () => ( /*! - * Clears all Observables listeners added in the current scope/host (`O.el`, `assign`, …?). + * Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?). * You can investigate the `__dde_reactive` key of the element. * */ - o[R].forEach(([[c, s]]) => L(c, s, c[p] && c[p].host && c[p].host() === o)) + o[R].forEach(([[c, i]]) => L(c, i, c[p] && c[p].host && c[p].host() === o)) ) )(o)), o[R].push([[t, e], ...n]); }); } -function it(t, e, n) { +function st(t, e, n) { let r = t ? () => K(r) : (...o) => o.length ? dt(r, ...o) : K(r); return at(r, e, n, t); } @@ -611,13 +611,13 @@ function at(t, e, n, r = !1) { X(n) !== "[object Object]" && (n = {}); let { onclear: c } = E.symbols; n[c] && (o.push(n[c]), delete n[c]); - let { host: s } = m; + let { host: i } = m; return Reflect.defineProperty(t, p, { value: { value: e, actions: n, onclear: o, - host: s, + host: i, listeners: /* @__PURE__ */ new Set(), defined: new V().stack, readonly: r @@ -634,7 +634,7 @@ function K(t) { if (!t[p]) return; let { value: e, listeners: n } = t[p], r = Rt(); - return r && n.add(r), v.has(r) && v.get(r).add(t), e; + return r && n.add(r), g.has(r) && g.get(r).add(t), e; } function dt(t, e, n) { if (!t[p]) @@ -653,39 +653,39 @@ function L(t, e, n) { return; let o = r.listeners.delete(e); if (n && !r.listeners.size) { - if (E.clear(t), !v.has(r)) + if (E.clear(t), !g.has(r)) return o; - let c = v.get(r); - if (!v.has(c)) + let c = g.get(r); + if (!g.has(c)) return o; - v.get(c).forEach((s) => L(s, c, !0)); + g.get(c).forEach((i) => L(i, c, !0)); } return o; } -// observables.js +// signals.js B(ut); export { - E as O, - j as assign, + E as S, + P as assign, nt as assignAttribute, ht as chainableAppend, - vt as classListDeclarative, - M as createElement, + gt as classListDeclarative, + j as createElement, qt as createElementNS, Zt as customElementRender, - wt as customElementWithDDE, + yt as customElementWithDDE, Qt as dispatchEvent, - M as el, + j as el, qt as elNS, - gt as elementAttribute, + vt as elementAttribute, Ft as empty, - z as isObservable, - wt as lifecyclesToEvents, - E as observable, - yt as observedAttributes, - O as on, + z as isSignal, + yt as lifecyclesToEvents, + _t as observedAttributes, + w as on, B as registerReactivity, m as scope, + E as signal, Wt as simulateSlots }; diff --git a/dist/esm.d.ts b/dist/esm.d.ts index fbe39ce..672e3fa 100644 --- a/dist/esm.d.ts +++ b/dist/esm.d.ts @@ -1,17 +1,17 @@ -export type Observable= (set?: V)=> V & A; -type Action= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof observable._ | void; -//type SymbolObservable= Symbol; +export type Signal= (set?: V)=> V & A; +type Action= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void; +//type SymbolSignal= Symbol; type SymbolOnclear= symbol; type Actions= Record>; type OnListenerOptions= Pick & { first_time?: boolean }; -interface observable{ +interface signal{ _: Symbol /** * Simple example: * ```js - * const hello= S("Hello Observable"); + * const hello= S("Hello Signal"); * ``` - * …simple todo observable: + * …simple todo signal: * ```js * const todos= S([], { * add(v){ this.value.push(S(v)); }, @@ -19,48 +19,48 @@ interface observable{ * [S.symbols.onclear](){ S.clear(...this.value); }, * }); * ``` - * …computed observable: + * …computed signal: * ```js * const name= S("Jan"); * const surname= S("Andrle"); * const fullname= S(()=> name()+" "+surname()); * ``` - * @param value Initial observable value. Or function computing value from other observables. - * @param actions Use to define actions on the observable. Such as add item to the array. - * There is also a reserved function `S.symbol.onclear` which is called when the observable is cleared + * @param value Initial signal value. Or function computing value from other signals. + * @param actions Use to define actions on the signal. Such as add item to the array. + * There is also a reserved function `S.symbol.onclear` which is called when the signal is cleared * by `S.clear`. * */ - >(value: V, actions?: A): Observable; + >(value: V, actions?: A): Signal; /** - * Computations observable. This creates a observable which is computed from other observables. + * Computations signal. This creates a signal which is computed from other signals. * */ - (computation: ()=> V): Observable - action>, A extends (S extends Observable ? A : never), N extends keyof A>( - observable: S, + (computation: ()=> V): Signal + action>, A extends (S extends Signal ? A : never), N extends keyof A>( + signal: S, name: N, ...params: A[N] extends (...args: infer P)=> any ? P : never ): void; - clear(...observables: Observable[]): void; - on(observable: Observable, onchange: (a: T)=> void, options?: OnListenerOptions): void; + clear(...signals: Signal[]): void; + on(signal: Signal, onchange: (a: T)=> void, options?: OnListenerOptions): void; symbols: { - //observable: SymbolObservable; + //signal: SymbolSignal; onclear: SymbolOnclear; } /** - * Reactive element, which is rendered based on the given observable. + * Reactive element, which is rendered based on the given signal. * ```js - * S.el(observable, value=> value ? el("b", "True") : el("i", "False")); + * S.el(signal, value=> value ? el("b", "True") : el("i", "False")); * S.el(listS, list=> list.map(li=> el("li", li))); * ``` * */ - el(observable: Observable, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment; + el(signal: Signal, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment; - observedAttributes(custom_element: HTMLElement): Record>; + observedAttributes(custom_element: HTMLElement): Record>; } -export const observable: observable; -export const O: observable; +export const signal: signal; +export const S: signal; declare global { - type ddeObservable= Observable; + type ddeSignal= Signal; type ddeAction= Action type ddeActions= Actions } @@ -80,20 +80,20 @@ type AttrsModified= { /** * Use string like in HTML (internally uses `*.setAttribute("style", *)`), or object representation (like DOM API). */ - style: string | Partial | Observable | Partial<{ [K in keyof CSSStyleDeclaration]: Observable }> + style: string | Partial | Signal | Partial<{ [K in keyof CSSStyleDeclaration]: Signal }> /** * Provide option to add/remove/toggle CSS clasess (index of object) using 1/0/-1. In fact `el.classList.toggle(class_name)` for `-1` and `el.classList.toggle(class_name, Boolean(...))` for others. */ - classList: Record>, + classList: Record>, /** * By default simiral to `className`, but also supports `string[]` * */ - className: string | (string|boolean|undefined|Observable)[]; + className: string | (string|boolean|undefined|Signal)[]; /** * Sets `aria-*` simiraly to `dataset` * */ - ariaset: Record>, -} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, string|Observable> & Record<`.${string}`, any> + ariaset: Record>, +} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, string|Signal> & Record<`.${string}`, any> type _fromElsInterfaces= Omit; /** * Just element attributtes @@ -103,13 +103,13 @@ type _fromElsInterfaces= Omit= Partial<_fromElsInterfaces & { [K in keyof _fromElsInterfaces]: Observable<_fromElsInterfaces[K], any> } & AttrsModified> & Record; +type ElementAttributes= Partial<_fromElsInterfaces & { [K in keyof _fromElsInterfaces]: Signal<_fromElsInterfaces[K], any> } & AttrsModified> & Record; export function classListDeclarative(element: El, classList: AttrsModified["classList"]): El export function assign(element: El, ...attrs_array: ElementAttributes[]): El export function assignAttribute>(element: El, attr: ATT, value: ElementAttributes[ATT]): ElementAttributes[ATT] type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap; -type textContent= string | ( (set?: string)=> string ); // TODO: for some reason `Observable` leads to `attrs?: any` +type textContent= string | ( (set?: string)=> string ); // TODO: for some reason `Signal` leads to `attrs?: any` export function el< TAG extends keyof ExtendedHTMLElementTagNameMap & string, EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : HTMLElement) @@ -148,7 +148,7 @@ export function elNS( EL extends ( TAG extends keyof MathMLElementTagNameMap ? MathMLElementTagNameMap[TAG] : MathMLElement ), >( tag_name: TAG, - attrs?: string | textContent | Partial<{ [key in keyof EL]: EL[key] | Observable | string | number | boolean }>, + attrs?: string | textContent | Partial<{ [key in keyof EL]: EL[key] | Signal | string | number | boolean }>, ...addons: ddeElementAddon[] )=> ddeMathMLElement export function elNS( diff --git a/dist/esm.js b/dist/esm.js index eb3699a..2df90a5 100644 --- a/dist/esm.js +++ b/dist/esm.js @@ -1,6 +1,6 @@ -// src/observables-common.js +// src/signals-common.js var C = { - isObservable(t) { + isSignal(t) { return !1; }, processReactiveAttribute(t, e, n, r) { @@ -55,18 +55,18 @@ function K(t, e, n) { return Reflect.set(t, e, ""); } } -var x = "__dde_lifecyclesToEvents", v = "dde:connected", y = "dde:disconnected", O = "dde:attributeChanged"; +var x = "__dde_lifecyclesToEvents", g = "dde:connected", y = "dde:disconnected", D = "dde:attributeChanged"; // src/dom.js -var g = [{ +var v = [{ get scope() { return a.D.body; }, host: (t) => t ? t(a.D.body) : a.D.body, prevent: !0 -}], R = { +}], S = { get current() { - return g[g.length - 1]; + return v[v.length - 1]; }, get host() { return this.current.host; @@ -76,17 +76,17 @@ var g = [{ return t.prevent = !0, t; }, get state() { - return [...g]; + return [...v]; }, push(t = {}) { - return g.push(Object.assign({}, this.current, { prevent: !1 }, t)); + return v.push(Object.assign({}, this.current, { prevent: !1 }, t)); }, pushRoot() { - return g.push(g[0]); + return v.push(v[0]); }, pop() { - if (g.length !== 1) - return g.pop(); + if (v.length !== 1) + return v.pop(); } }; function $(...t) { @@ -98,9 +98,9 @@ function Q(t) { var T; function k(t, e, ...n) { let r = L(this), o = 0, c, f; - switch ((Object(e) !== e || r.isObservable(e)) && (e = { textContent: e }), !0) { + switch ((Object(e) !== e || r.isSignal(e)) && (e = { textContent: e }), !0) { case typeof t == "function": { - o = 1, R.push({ scope: t, host: (...b) => b.length ? (o === 1 ? n.unshift(...b) : b.forEach((l) => l(f)), void 0) : f }), c = t(e || void 0); + o = 1, S.push({ scope: t, host: (...b) => b.length ? (o === 1 ? n.unshift(...b) : b.forEach((l) => l(f)), void 0) : f }), c = t(e || void 0); let d = c instanceof a.F; if (c.nodeName === "#comment") break; @@ -113,18 +113,18 @@ function k(t, e, ...n) { break; } case t === "#text": - c = D.call(this, a.D.createTextNode(""), e); + c = O.call(this, a.D.createTextNode(""), e); break; case (t === "<>" || !t): - c = D.call(this, a.D.createDocumentFragment(), e); + c = O.call(this, a.D.createDocumentFragment(), e); break; case !!T: - c = D.call(this, a.D.createElementNS(T, t), e); + c = O.call(this, a.D.createElementNS(T, t), e); break; case !c: - c = D.call(this, a.D.createElement(t), e); + c = O.call(this, a.D.createElement(t), e); } - return Q(c), f || (f = c), n.forEach((d) => d(f)), o && R.pop(), o = 2, c; + return Q(c), f || (f = c), n.forEach((d) => d(f)), o && S.pop(), o = 2, c; } function bt(t, e = t, n = void 0) { let r = Symbol.for("default"), o = Array.from(e.querySelectorAll("slot")).reduce((f, d) => Reflect.set(f, d.name || r, d) && f, {}), c = q(o, r); @@ -152,7 +152,7 @@ function bt(t, e = t, n = void 0) { function X(t, e, n) { n && n(t, e); try { - t.replaceWith(D(e, { className: [e.className, t.className], dataset: { ...t.dataset } })); + t.replaceWith(O(e, { className: [e.className, t.className], dataset: { ...t.dataset } })); } catch { t.replaceWith(e); } @@ -162,7 +162,7 @@ k.mark = function(t, e = !1) { let n = e ? "" : "/", r = a.D.createComment(``); return e && (r.end = a.D.createComment("")), r; }; -function vt(t) { +function gt(t) { let e = this; return function(...r) { T = t; @@ -171,7 +171,7 @@ function vt(t) { }; } var P = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: U } = a; -function D(t, ...e) { +function O(t, ...e) { if (!e.length) return t; P.set(t, B(t, this)); @@ -225,7 +225,7 @@ function Y(t, e) { (r, o) => t.classList.toggle(r, o === -1 ? void 0 : !!o) ), t; } -function gt(t) { +function vt(t) { return Array.from(t.children).forEach((e) => e.remove()), t; } function tt(t, e, n, r) { @@ -350,7 +350,7 @@ function ot() { if (u && b(h).then(l), !t.has(h)) continue; let m = t.get(h); - m.length_c && (h.dispatchEvent(new Event(v)), m.connected = /* @__PURE__ */ new WeakSet(), m.length_c = 0, m.length_d || t.delete(h), s = !0); + m.length_c && (h.dispatchEvent(new Event(g)), m.connected = /* @__PURE__ */ new WeakSet(), m.length_c = 0, m.length_d || t.delete(h), s = !0); } return s; } @@ -368,26 +368,26 @@ function ot() { } // src/customElement.js -function Ot(t, e, n, r = it) { - R.push({ +function Dt(t, e, n, r = it) { + S.push({ scope: t, host: (...f) => f.length ? f.forEach((d) => d(t)) : t }), typeof r == "function" && (r = r.call(t, t)); let o = t[x]; o || ct(t); let c = n.call(t, r); - return o || t.dispatchEvent(new Event(v)), e.nodeType === 11 && typeof e.mode == "string" && t.addEventListener(y, w.observe(e), { once: !0 }), R.pop(), e.append(c); + return o || t.dispatchEvent(new Event(g)), e.nodeType === 11 && typeof e.mode == "string" && t.addEventListener(y, w.observe(e), { once: !0 }), S.pop(), e.append(c); } function ct(t) { return W(t.prototype, "connectedCallback", function(e, n, r) { - e.apply(n, r), n.dispatchEvent(new Event(v)); + e.apply(n, r), n.dispatchEvent(new Event(g)); }), W(t.prototype, "disconnectedCallback", function(e, n, r) { e.apply(n, r), (globalThis.queueMicrotask || setTimeout)( () => !n.isConnected && n.dispatchEvent(new Event(y)) ); }), W(t.prototype, "attributeChangedCallback", function(e, n, r) { let [o, , c] = r; - n.dispatchEvent(new CustomEvent(O, { + n.dispatchEvent(new CustomEvent(D, { detail: [o, c] })), e.apply(n, r); }), t.prototype[x] = !0, t; @@ -408,61 +408,61 @@ function _t(t, e, n) { return o.dispatchEvent(f); }; } -function S(t, e, n) { +function R(t, e, n) { return function(o) { return o.addEventListener(t, e, n), o; }; } var G = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 }); -S.connected = function(t, e) { +R.connected = function(t, e) { return e = G(e), function(r) { - return r.addEventListener(v, t, e), r[x] ? r : r.isConnected ? (r.dispatchEvent(new Event(v)), r) : (N(e.signal, () => w.offConnected(r, t)) && w.onConnected(r, t), r); + return r.addEventListener(g, t, e), r[x] ? r : r.isConnected ? (r.dispatchEvent(new Event(g)), r) : (N(e.signal, () => w.offConnected(r, t)) && w.onConnected(r, t), r); }; }; -S.disconnected = function(t, e) { +R.disconnected = function(t, e) { return e = G(e), function(r) { return r.addEventListener(y, t, e), r[x] || N(e.signal, () => w.offDisconnected(r, t)) && w.onDisconnected(r, t), r; }; }; var j = /* @__PURE__ */ new WeakMap(); -S.disconnectedAsAbort = function(t) { +R.disconnectedAsAbort = function(t) { if (j.has(t)) return j.get(t); let e = new AbortController(); - return j.set(t, e), t(S.disconnected(() => e.abort())), e; + return j.set(t, e), t(R.disconnected(() => e.abort())), e; }; var st = /* @__PURE__ */ new WeakSet(); -S.attributeChanged = function(t, e) { +R.attributeChanged = function(t, e) { return typeof e != "object" && (e = {}), function(r) { - if (r.addEventListener(O, t, e), r[x] || st.has(r) || !a.M) + if (r.addEventListener(D, t, e), r[x] || st.has(r) || !a.M) return r; let o = new a.M(function(f) { for (let { attributeName: d, target: p } of f) p.dispatchEvent( - new CustomEvent(O, { detail: [d, p.getAttribute(d)] }) + new CustomEvent(D, { detail: [d, p.getAttribute(d)] }) ); }); return N(e.signal, () => o.disconnect()) && o.observe(r, { attributes: !0 }), r; }; }; export { - D as assign, + O as assign, z as assignAttribute, Q as chainableAppend, Y as classListDeclarative, k as createElement, - vt as createElementNS, - Ot as customElementRender, + gt as createElementNS, + Dt as customElementRender, ct as customElementWithDDE, _t as dispatchEvent, k as el, - vt as elNS, + gt as elNS, tt as elementAttribute, - gt as empty, + vt as empty, ct as lifecyclesToEvents, it as observedAttributes, - S as on, + R as on, V as registerReactivity, - R as scope, + S as scope, bt as simulateSlots }; diff --git a/docs/index.html b/docs/index.html index cb944a9..d8792bd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,9 +1,9 @@ -`deka-dom-el` — Introduction

`deka-dom-el` — Introduction

Introducing a library.

The library tries to provide pure JavaScript tool(s) to create reactive interfaces.

We start with creating and modifying a static elements and end up with UI templates. From document.createElement to el. Then we go through the native events system and the way to include it declaratively in UI templates. From element.addEventListener to on.

Next step is providing interactivity not only for our UI templates. We introduce observables (O) and how them incorporate to UI templates.

Now we will clarify how the observables are incorporated into our templates with regard to application performance. This is not the only reason the library uses scopes. We will look at how they work in components represented in JavaScript by functions.

import { el } from "./esm-with-observables.js"; -import { O } from "./esm-with-observables.js"; -const clicks= O(0); +`deka-dom-el` — Introduction

`deka-dom-el` — Introduction

Introducing a library.

The library tries to provide pure JavaScript tool(s) to create reactive interfaces.

We start with creating and modifying a static elements and end up with UI templates. From document.createElement to el. Then we go through the native events system and the way to include it declaratively in UI templates. From element.addEventListener to on.

Next step is providing interactivity not only for our UI templates. We introduce signals (O) and how them incorporate to UI templates.

Now we will clarify how the signals are incorporated into our templates with regard to application performance. This is not the only reason the library uses scopes. We will look at how they work in components represented in JavaScript by functions.

import { el } from "./esm-with-signals.js"; +import { S } from "./esm-with-signals.js"; +const clicks= S(0); document.body.append( el().append( - el("p", O(()=> + el("p", S(()=> "Hello World "+"🎉".repeat(clicks()) )), el("button", { @@ -13,4 +13,4 @@ document.body.append( }) ) ); -
\ No newline at end of file +
\ No newline at end of file diff --git a/docs/p02-elements.html b/docs/p02-elements.html index 34ce738..6dd2412 100644 --- a/docs/p02-elements.html +++ b/docs/p02-elements.html @@ -1,4 +1,4 @@ -`deka-dom-el` — Elements

`deka-dom-el` — Elements

Basic concepts of elements modifications and creations.

Native JavaScript DOM elements creations

Let’s go through all patterns we would like to use and what needs to be improved for better experience.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js +`deka-dom-el` — Elements

`deka-dom-el` — Elements

Basic concepts of elements modifications and creations.

Native JavaScript DOM elements creations

Let’s go through all patterns we would like to use and what needs to be improved for better experience.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js import { assign, el, createElement, @@ -26,7 +26,7 @@ document.body.append( { textContent: "Element’s text content.", style: "color: coral;" } ) ); -

To make this easier, you can use the el function. Internally in basic examples, it is wrapper around assign(document.createElement(…), { … }).

import { el, assign } from "./esm-with-observables.js"; +

To make this easier, you can use the el function. Internally in basic examples, it is wrapper around assign(document.createElement(…), { … }).

import { el, assign } from "./esm-with-signals.js"; const color= "lightcoral"; document.body.append( el("p", { textContent: "Hello (first time)", style: { color } }) @@ -37,7 +37,7 @@ document.body.append( { textContent: "Hello (second time)", style: { color } } ) ); -

The assign function provides improved behaviour of Object.assign(). You can declaratively sets any IDL and attribute of the given element. Function prefers IDL and fallback to the element.setAttribute if there is no writable property in the element prototype.

You can study all JavaScript elements interfaces to the corresponding HTML elements. All HTML elements inherits from HTMLElement. To see all available IDLs for example for paragraphs, see HTMLParagraphElement. Moreover, the assign provides a way to sets declaratively some convenient properties:

  • It is possible to sets data-*/aria-* attributes using object notation.
  • In opposite, it is also possible to sets data-*/aria-* attribute using camelCase notation.
  • You can use string or object as a value for style property.
  • className (IDL – preffered)/class are ways to add CSS class to the element. You can use string (similarly to class="…" syntax in HTML) or array of strings. This is handy to concat conditional classes.
  • Use classList to toggle specific classes. This will be handy later when the reactivity via observables is beeing introduced.
  • The assign also accepts the undefined as a value for any property to remove it from the element declaratively. Also for some IDL the corresponding attribute is removed as it can be confusing. For example, natievly the element’s id is removed by setting the IDL to an empty string.
  • You can use = or . to force processing given key as attribute/property of the element.

For processing, the assign internally uses assignAttribute and classListDeclarative.

import { assign, assignAttribute, classListDeclarative } from "./esm-with-observables.js"; +

The assign function provides improved behaviour of Object.assign(). You can declaratively sets any IDL and attribute of the given element. Function prefers IDL and fallback to the element.setAttribute if there is no writable property in the element prototype.

You can study all JavaScript elements interfaces to the corresponding HTML elements. All HTML elements inherits from HTMLElement. To see all available IDLs for example for paragraphs, see HTMLParagraphElement. Moreover, the assign provides a way to sets declaratively some convenient properties:

  • It is possible to sets data-*/aria-* attributes using object notation.
  • In opposite, it is also possible to sets data-*/aria-* attribute using camelCase notation.
  • You can use string or object as a value for style property.
  • className (IDL – preffered)/class are ways to add CSS class to the element. You can use string (similarly to class="…" syntax in HTML) or array of strings. This is handy to concat conditional classes.
  • Use classList to toggle specific classes. This will be handy later when the reactivity via signals is beeing introduced.
  • The assign also accepts the undefined as a value for any property to remove it from the element declaratively. Also for some IDL the corresponding attribute is removed as it can be confusing. For example, natievly the element’s id is removed by setting the IDL to an empty string.
  • You can use = or . to force processing given key as attribute/property of the element.

For processing, the assign internally uses assignAttribute and classListDeclarative.

import { assign, assignAttribute, classListDeclarative } from "./esm-with-signals.js"; const paragraph= document.createElement("p"); assignAttribute(paragraph, "textContent", "Hello, world!"); @@ -70,7 +70,7 @@ console.log("paragraph.something=", paragraph.something); document.body.append( paragraph ); -

# Native JavaScript templating

By default, the native JS has no good way to define HTML template using DOM API:

document.body.append( +

# Native JavaScript templating

By default, the native JS has no good way to define HTML template using DOM API:

document.body.append( document.createElement("div"), document.createElement("span"), document.createElement("main") @@ -81,7 +81,7 @@ const template= document.createElement("main").append( document.createElement("span"), ); console.log(typeof template==="undefined"); -

This library therefore overwrites the append method of created elements to always return parent element.

import { el } from "./esm-with-observables.js"; +

This library therefore overwrites the append method of created elements to always return parent element.

import { el } from "./esm-with-signals.js"; document.head.append( el("style").append( "tr, td{ border: 1px solid red; padding: 1em; }", @@ -106,7 +106,7 @@ document.body.append( ) ); -import { chainableAppend } from "./esm-with-observables.js"; +import { chainableAppend } from "./esm-with-signals.js"; /** @param {keyof HTMLElementTagNameMap} tag */ const createElement= tag=> chainableAppend(document.createElement(tag)); document.body.append( @@ -116,7 +116,7 @@ document.body.append( ) ) ); -

# Basic (state-less) components

You can use functions for encapsulation (repeating) logic. The el accepts function as first argument. In that case, the function should return dom elements and the second argument for el is argument for given element.

import { el } from "./esm-with-observables.js"; +

# Basic (state-less) components

You can use functions for encapsulation (repeating) logic. The el accepts function as first argument. In that case, the function should return dom elements and the second argument for el is argument for given element.

import { el } from "./esm-with-signals.js"; document.head.append( el("style").append( ".class1{ font-weight: bold; }", @@ -133,7 +133,7 @@ function component({ className, textContent }){ el("p", textContent) ); } -

As you can see, in case of state-less/basic components there is no difference between calling component function directly or using el function.

It is nice to use similar naming convention as native DOM API. This allows us to use the destructuring assignment syntax and keep track of the native API (things are best remembered through regular use).

# Creating non-HTML elements

Similarly to the native DOM API (document.createElementNS) for non-HTML elements we need to tell JavaScript which kind of the element to create. We can use the elNS function:

import { elNS, assign } from "./esm-with-observables.js"; +

As you can see, in case of state-less/basic components there is no difference between calling component function directly or using el function.

It is nice to use similar naming convention as native DOM API. This allows us to use the destructuring assignment syntax and keep track of the native API (things are best remembered through regular use).

# Creating non-HTML elements

Similarly to the native DOM API (document.createElementNS) for non-HTML elements we need to tell JavaScript which kind of the element to create. We can use the elNS function:

import { elNS, assign } from "./esm-with-signals.js"; const elSVG= elNS("http://www.w3.org/2000/svg"); const elMath= elNS("http://www.w3.org/1998/Math/MathML"); document.body.append( @@ -144,4 +144,4 @@ document.body.append( console.log( document.body.innerHTML.includes("<svg></svg><math></math>") ) -

# Mnemonic

  • assign(<element>, ...<idl-objects>): <element> — assign properties to the element
  • el(<tag-name>, <primitive>)[.append(...)]: <element-from-tag-name> — simple element containing only text
  • el(<tag-name>, <idl-object>)[.append(...)]: <element-from-tag-name> — element with more properties
  • el(<function>, <function-argument(s)>)[.append(...)]: <element-returned-by-function> — using component represented by function
  • el(<...>, <...>, ...<addons>) — see following page
  • elNS(<namespace>)(<as-el-see-above>)[.append(...)]: <element-based-on-arguments> — typically SVG elements
\ No newline at end of file +

# Mnemonic

  • assign(<element>, ...<idl-objects>): <element> — assign properties to the element
  • el(<tag-name>, <primitive>)[.append(...)]: <element-from-tag-name> — simple element containing only text
  • el(<tag-name>, <idl-object>)[.append(...)]: <element-from-tag-name> — element with more properties
  • el(<function>, <function-argument(s)>)[.append(...)]: <element-returned-by-function> — using component represented by function
  • el(<...>, <...>, ...<addons>) — see following page
  • elNS(<namespace>)(<as-el-see-above>)[.append(...)]: <element-based-on-arguments> — typically SVG elements
\ No newline at end of file diff --git a/docs/p03-events.html b/docs/p03-events.html index 2a0b20e..83e894e 100644 --- a/docs/p03-events.html +++ b/docs/p03-events.html @@ -1,8 +1,8 @@ -`deka-dom-el` — Events and Addons

`deka-dom-el` — Events and Addons

Using not only events in UI declaratively.

Listenning to the native DOM events and other Addons

We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called Addon to incorporate not only this in UI templates declaratively.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js +`deka-dom-el` — Events and Addons

`deka-dom-el` — Events and Addons

Using not only events in UI declaratively.

Listenning to the native DOM events and other Addons

We quickly introduce helper to listening to the native DOM events. And library syntax/pattern so-called Addon to incorporate not only this in UI templates declaratively.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm.js import { on, dispatchEvent } from "deka-dom-el"; /** @type {ddeElementAddon} */ -

# Events and listenners

In JavaScript you can listen to the native DOM events of the given element by using element.addEventListener(type, listener, options). The library provides an alternative (on) accepting the differen order of the arguments:

import { el, on } from "./esm-with-observables.js"; +

# Events and listenners

In JavaScript you can listen to the native DOM events of the given element by using element.addEventListener(type, listener, options). The library provides an alternative (on) accepting the differen order of the arguments:

import { el, on } from "./esm-with-signals.js"; const log= mark=> console.log.bind(console, mark); const button= el("button", "Test click"); @@ -12,7 +12,7 @@ on("click", log("`on`"), { once: true })(button); document.body.append( button ); -

…this is actually one of the two differences. The another one is that on accepts only object as the options (but it is still optional).

The other difference is that there is no off function. You can remove listener declaratively using AbortSignal:

import { el, on } from "./esm-with-observables.js"; +

…this is actually one of the two differences. The another one is that on accepts only object as the options (but it is still optional).

The other difference is that there is no off function. You can remove listener declaratively using AbortSignal:

import { el, on } from "./esm-with-signals.js"; const log= mark=> console.log.bind(console, mark); const abort_controller= new AbortController(); @@ -25,7 +25,7 @@ on("click", log("`on`"), { signal })(button); document.body.append( button, " ", el("button", { textContent: "Off", onclick: ()=> abort_controller.abort() }) ); -

So, there are (typically) three ways to handle events. You can use:

  • el("button", { textContent: "click me", "=onclick": "console.log(event)" })
  • el("button", { textContent: "click me", onclick: console.log })
  • el("button", { textContent: "click me" }, on("click", console.log))

In the first example we force to use HTML attribute (it corresponds to <button onclick="console.log(event)">click me</button>). Side note: this can be useful in case of SSR. To study difference, you can read a nice summary here: GIST @WebReflection/web_events.md.

# Addons

From practical point of view, Addons are just functions that accept any html element as their first parameter. You can see that the on(…) fullfills this requirement.

You can use Addons as ≥3rd argument of el function. This way is possible to extends you templates by additional (3rd party) functionalities. But for now mainly, you can add events listeners:

import { el, on } from "./esm-with-observables.js"; +

So, there are (typically) three ways to handle events. You can use:

  • el("button", { textContent: "click me", "=onclick": "console.log(event)" })
  • el("button", { textContent: "click me", onclick: console.log })
  • el("button", { textContent: "click me" }, on("click", console.log))

In the first example we force to use HTML attribute (it corresponds to <button onclick="console.log(event)">click me</button>). Side note: this can be useful in case of SSR. To study difference, you can read a nice summary here: GIST @WebReflection/web_events.md.

# Addons

From practical point of view, Addons are just functions that accept any html element as their first parameter. You can see that the on(…) fullfills this requirement.

You can use Addons as ≥3rd argument of el function. This way is possible to extends you templates by additional (3rd party) functionalities. But for now mainly, you can add events listeners:

import { el, on } from "./esm-with-signals.js"; const abort_controller= new AbortController(); const { signal }= abort_controller; /** @type {ddeElementAddon<HTMLButtonElement>} */ @@ -49,7 +49,7 @@ function update(event){ "\n" ); } -

As the example shows, you can also provide types in JSDoc+TypeScript by using global type ddeElementAddon. Also notice, you can use Addons to get element reference.

# Life-cycle events

Addons are called immediately when the element is created, even it is not connected to live DOM yet. Therefore, you can understand the Addon to be “oncreate” event.

The library provide three additional live-cycle events corresponding to how they are named in a case of custom elements: on.connected, on.disconnected and on.attributeChanged.

import { el, on } from "./esm-with-observables.js"; +

As the example shows, you can also provide types in JSDoc+TypeScript by using global type ddeElementAddon. Also notice, you can use Addons to get element reference.

# Life-cycle events

Addons are called immediately when the element is created, even it is not connected to live DOM yet. Therefore, you can understand the Addon to be “oncreate” event.

The library provide three additional live-cycle events corresponding to how they are named in a case of custom elements: on.connected, on.disconnected and on.attributeChanged.

import { el, on } from "./esm-with-signals.js"; const paragraph= el("p", "See live-cycle events in console.", el=> log({ type: "dde:created", detail: el }), on.connected(log), @@ -67,7 +67,7 @@ document.body.append( function log({ type, detail }){ console.log({ _this: this, type, detail }); } -

For Custom elements, we will later introduce a way to replace *Callback syntax with dde:* events. The on.* functions then listen to the appropriate Custom Elements events (see Custom element lifecycle callbacks | MDN).

But, in case of regular elemnets the MutationObserver | MDN is internaly used to track these events. Therefore, there are some drawbacks:

  • To proper listener registration, you need to use on.* not `on("dde:*", …)`!
  • Use sparingly! Internally, library must loop of all registered events and fires event properly. It is good practice to use the fact that if an element is removed, its children are also removed! In this spirit, we will introduce later the host syntax to register clean up procedures when the component is removed from the app.

To provide intuitive behaviour, similar also to how the life-cycle events works in other frameworks/libraries, deka-dom-el library ensures that on.connected and on.disconnected are called only once and only when the element is (dis)connected to live DOM. The solution is inspired by Vue. For using native behaviour re-(dis)connecting element, use:

  • custom MutationObserver or logic in (dis)
    or…
  • re-add on.connected or on.disconnected listeners.

# Final notes

The library also provides a method to dispatch the events.

import { el, on, dispatchEvent } from "./esm-with-observables.js"; +

For Custom elements, we will later introduce a way to replace *Callback syntax with dde:* events. The on.* functions then listen to the appropriate Custom Elements events (see Custom element lifecycle callbacks | MDN).

But, in case of regular elemnets the MutationObserver | MDN is internaly used to track these events. Therefore, there are some drawbacks:

  • To proper listener registration, you need to use on.* not `on("dde:*", …)`!
  • Use sparingly! Internally, library must loop of all registered events and fires event properly. It is good practice to use the fact that if an element is removed, its children are also removed! In this spirit, we will introduce later the host syntax to register clean up procedures when the component is removed from the app.

To provide intuitive behaviour, similar also to how the life-cycle events works in other frameworks/libraries, deka-dom-el library ensures that on.connected and on.disconnected are called only once and only when the element is (dis)connected to live DOM. The solution is inspired by Vue. For using native behaviour re-(dis)connecting element, use:

  • custom MutationObserver or logic in (dis)
    or…
  • re-add on.connected or on.disconnected listeners.

# Final notes

The library also provides a method to dispatch the events.

import { el, on, dispatchEvent } from "./esm-with-signals.js"; document.body.append( el("p", "Listenning to `test` event.", on("test", console.log)).append( el("br"), @@ -91,4 +91,4 @@ function dde(){ function ddeOptions(){ dispatchEvent("test", { bubbles: true })(this, "hi"); } -

# Mnemonic

  • on(<event>, <listener>[, <options>])(<element>) — just <element>.addEventListener(<event>, <listener>[, <options>])
  • on.<live-cycle>(<event>, <listener>[, <options>])(<element>) — corresponds to custom elemnets callbacks <live-cycle>Callback(...){...}. To connect to custom element see following page, else it is simulated by MutationObserver.
  • dispatchEvent(<event>[, <options>])(element) — just <element>.dispatchEvent(new Event(<event>[, <options>]))
  • dispatchEvent(<event>[, <options>])(element, detail) — just <element>.dispatchEvent(new CustomEvent(<event>, { detail, ...<options> }))
\ No newline at end of file +

# Mnemonic

  • on(<event>, <listener>[, <options>])(<element>) — just <element>.addEventListener(<event>, <listener>[, <options>])
  • on.<live-cycle>(<event>, <listener>[, <options>])(<element>) — corresponds to custom elemnets callbacks <live-cycle>Callback(...){...}. To connect to custom element see following page, else it is simulated by MutationObserver.
  • dispatchEvent(<event>[, <options>])(element) — just <element>.dispatchEvent(new Event(<event>[, <options>]))
  • dispatchEvent(<event>[, <options>])(element, detail) — just <element>.dispatchEvent(new CustomEvent(<event>, { detail, ...<options> }))
\ No newline at end of file diff --git a/docs/p04-signals.html b/docs/p04-signals.html index 69973a3..217de81 100644 --- a/docs/p04-signals.html +++ b/docs/p04-signals.html @@ -1,22 +1,22 @@ -`deka-dom-el` — Signals and reactivity

`deka-dom-el` — Signals and reactivity

Handling reactivity in UI via signals.

Using signals to manage reactivity

How a program responds to variable data or user interactions is one of the fundamental problems of programming. If we desire to solve the issue in a declarative manner, signals may be a viable approach.

// when NPM -import { S } from "deka-dom-el/signals"; -// https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js - -/** - * @type {ddeSignal} - * */ -/** - * @type {ddeActions} - * */ -

# Introducing signals

Using signals, we split program logic into the three parts. Firstly (α), we create a variable (constant) representing reactive value. Somewhere later, we can register (β) a logic reacting to the signal value changes. Similarly, in a remaining part (γ), we can update the signal value.

import { S } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js"; +`deka-dom-el` — Signals and reactivity

`deka-dom-el` — Signals and reactivity

Handling reactivity in UI via signals.

Using signals to manage reactivity

How a program responds to variable data or user interactions is one of the fundamental problems of programming. If we desire to solve the issue in a declarative manner, signals may be a viable approach.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js +import { S, signal } from "deka-dom-el/signals"; +S===signal +/** @type {ddeSignal} */ +/** @type {ddeAction} */ +/** @type {ddeActions} */ +

# Introducing signals

Using signals, we split program logic into the three parts. Firstly (α), we create a variable (constant) representing reactive value. Somewhere later, we can register (β) a logic reacting to the signal value changes. Similarly, in a remaining part (γ), we can update the signal value.

import { S } from "./esm-with-signals.js"; // α — `signal` represents a reactive value const signal= S(0); // β — just reacts on signal changes S.on(signal, console.log); // γ — just updates the value -signal(signal()+1); -setInterval(()=> signal(signal()+1), 5000); -

All this is just an example of Event-driven programming and Publish–subscribe pattern (compare for example with fpubsub library). All three parts can be in some manner independent and still connected to the same reactive entity.

Signals are implemented in the library as functions. To see current value of signal, just call it without any arguments console.log(signal()). To update the signal value, pass any argument signal('a new value'). For listenning the signal value changes, use S.on(signal, console.log).

Similarly to the on function to register DOM events listener. You can use AbortController/AbortSignal to off/stop listenning. For representing “live” piece of code computation pattern:

import { S } from "https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js"; +const update= ()=> signal(signal()+1); + +update(); +const interval= 5*1000; +setTimeout(clearInterval, 10*interval, + setInterval(update, interval)); +

All this is just an example of Event-driven programming and Publish–subscribe pattern (compare for example with fpubsub library). All three parts can be in some manner independent and still connected to the same reactive entity.

Signals are implemented in the library as functions. To see current value of signal, just call it without any arguments console.log(signal()). To update the signal value, pass any argument signal('a new value'). For listenning the signal value changes, use S.on(signal, console.log).

Similarly to the on function to register DOM events listener. You can use AbortController/AbortSignal to off/stop listenning. In example, you also found the way for representing “live” piece of code computation pattern (derived signal):

import { S } from "./esm-with-signals.js"; const signal= S(0); // computation pattern const double= S(()=> 2*signal()); @@ -26,10 +26,125 @@ S.on(signal, v=> console.log("signal", v), { signal: ac.signal }); S.on(double, v=> console.log("double", v), { signal: ac.signal }); signal(signal()+1); -const interval= 5000; +const interval= 5 * 1000; const id= setInterval(()=> signal(signal()+1), interval); ac.signal.addEventListener("abort", ()=> setTimeout(()=> clearInterval(id), 2*interval)); setTimeout(()=> ac.abort(), 3*interval) -

# Mnemonic

  • S(<value>) — signal: reactive value
  • S(()=> <computation>) — signal: reactive value dependent on calculation using other signals
  • S.on(<signal>, <listener>[, <options>]) — listen to the signal value changes
  • S.clear(...<signals>) — off and clear signals
  • S(<value>, <actions>) — signal: pattern to create complex reactive objects/arrays
  • S.action(<signal>, <action-name>, ...<action-arguments>) — invoke an action for given signal
\ No newline at end of file +

# Signals and actions

S(/* primitive */) allows you to declare simple reactive variables, typically around immutable primitive types. However, it may also be necessary to use reactive arrays, objects, or other complex reactive structures.

import { S } from "./esm-with-signals.js"; +const signal= S(0, { + increaseOnlyOdd(add){ + console.info(add); + if(add%2 === 0) return this.stopPropagation(); + this.value+= add; + } +}); +S.on(signal, console.log); +const oninterval= ()=> + S.action(signal, "increaseOnlyOdd", Math.floor(Math.random()*100)); + +const interval= 5*1000; +setTimeout( + clearInterval, + 10*interval, + setInterval(oninterval, interval) +); +

…but typical user-case is object/array (maps, sets and other mutable objects):

import { S } from "./esm-with-signals.js"; +const todos= S([], { + push(item){ + this.value.push(S(item)); + }, + pop(){ + const removed= this.value.pop(); + if(removed) S.clear(removed); + }, + [S.symbols.onclear](){ // this covers `O.clear(todos)` + S.clear(...this.value); + } +}); + +import { el, on } from "./esm-with-signals.js"; +/** @type {ddeElementAddon<HTMLFormElement>} */ +const onsubmit= on("submit", function(event){ + event.preventDefault(); + const data= new FormData(this); + switch (data.get("op")){ + case "A"/*dd*/: + S.action(todos, "push", data.get("todo")); + break; + case "E"/*dit*/: { + const last= todos().at(-1); + if(!last) break; + last(data.get("todo")); + break; + } + case "R"/*emove*/: + S.action(todos, "pop"); + break; + } +}); +document.body.append( + el("ul").append( + S.el(todos, todos=> + todos.map(textContent=> el("li", textContent))) + ), + el("form", null, onsubmit).append( + el("input", { type: "text", name: "todo", placeholder: "Todo’s text" }), + el(radio, { textContent: "Add", checked: true }), + el(radio, { textContent: "Edit last" }), + el(radio, { textContent: "Remove" }), + el("button", "Submit") + ) +); +document.head.append( + el("style", "form{ display: flex; flex-flow: column nowrap; }") +); +function radio({ textContent, checked= false }){ + return el("label").append( + el("input", { type: "radio", name: "op", value: textContent[0], checked }), + " ",textContent + ) +} +

In some way, you can compare it with useReducer hook from React. So, the S(<data>, <actions>) pattern creates a store “machine”. We can then invoke (dispatch) registered action by calling S.action(<signal>, <name>, ...<args>) after the action call the signal calls all its listeners. This can be stopped by calling this.stopPropagation() in the method representing the given action. As it can be seen in examples, the “store” value is available also in the function for given action (this.value).

# Reactive DOM attributes and elements

There are on basic level two distinc situation to mirror dynamic value into the DOM/UI

  1. to change some attribute(s) of existing element(s)
  2. to generate elements itself dynamically – this covers conditions and loops
import { S } from "./esm-with-signals.js"; +const count= S(0); + +import { el } from "./esm-with-signals.js"; +document.body.append( + el("p", S(()=> "Currently: "+count())), + el("p", { classList: { red: S(()=> count()%2) }, dataset: { count }, textContent: "Attributes example" }) +); +document.head.append( + el("style", ".red { color: red; }") +); + +const interval= 5 * 1000; +setTimeout(clearInterval, 10*interval, + setInterval(()=> count(count()+1), interval)); +

To derived attribute based on value of signal variable just use the signal as a value of the attribute (assign(element, { attribute: S('value') })). assign/el provides ways to glue reactive attributes/classes more granularly into the DOM. Just use dedicated build-in attributes dataset, ariaset and classList.

For computation, you can use the “derived signal” (see above) like assign(element, { textContent: S(()=> 'Hello '+WorldSignal()) }). This is read-only signal its value is computed based on given function and updated when any signal used in the function changes.

To represent part of the template filled dynamically based on the signal value use S.el(signal, DOMgenerator). This was already used in the todo example above or see:

import { S } from "./esm-with-signals.js"; +const count= S(0, { + add(){ this.value= this.value + Math.round(Math.random()*10); } +}); +const numbers= S([ count() ], { + push(next){ this.value.push(next); } +}); + +import { el } from "./esm-with-signals.js"; +document.body.append( + S.el(count, count=> count%2 + ? el("p", "Last number is odd.") + : el() + ), + el("p", "Lucky numbers:"), + el("ul").append( + S.el(numbers, numbers=> numbers.toReversed() + .map(n=> el("li", n))) + ) +); + +const interval= 5*1000; +setTimeout(clearInterval, 10*interval, setInterval(function(){ + S.action(count, "add"); + S.action(numbers, "push", count()); +}, interval)); +

# Mnemonic

  • S(<value>) — signal: reactive value
  • S(()=> <computation>) — read-only signal: reactive value dependent on calculation using other signals
  • S.on(<signal>, <listener>[, <options>]) — listen to the signal value changes
  • S.clear(...<signals>) — off and clear signals
  • S(<value>, <actions>) — signal: pattern to create complex reactive objects/arrays
  • S.action(<signal>, <action-name>, ...<action-arguments>) — invoke an action for given signal
  • S.el(<signal>, <function-returning-dom>) — render partial dom structure (template) based on the current signal value
\ No newline at end of file diff --git a/docs/p05-scopes.html b/docs/p05-scopes.html index 8af76fd..70bb807 100644 --- a/docs/p05-scopes.html +++ b/docs/p05-scopes.html @@ -1,7 +1,7 @@ -`deka-dom-el` — Scopes and components

`deka-dom-el` — Scopes and components

Organizing UI into components

Using functions as UI components

For state-less components we can use functions as UI components (see “Elements” page). But in real life, we may need to handle the component live-cycle and provide JavaScript the way to properly use the Garbage collection.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js +`deka-dom-el` — Scopes and components

`deka-dom-el` — Scopes and components

Organizing UI into components

Using functions as UI components

For state-less components we can use functions as UI components (see “Elements” page). But in real life, we may need to handle the component live-cycle and provide JavaScript the way to properly use the Garbage collection.

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js import { scope, el } from "deka-dom-el"; /** @type {ddeElementAddon} */ -

The library therefore use scopes to provide these functionalities.

# Scopes and hosts

The host is the name for the element representing the component. This is typically element returned by function. To get reference, you can use scope.host() to applly addons just use scope.host(...<addons>).

import { el, on, scope } from "./esm-with-observables.js"; +

The library therefore use scopes to provide these functionalities.

# Scopes and hosts

The host is the name for the element representing the component. This is typically element returned by function. To get reference, you can use scope.host() to applly addons just use scope.host(...<addons>).

import { el, on, scope } from "./esm-with-signals.js"; const { host }= scope; host( element=> console.log( @@ -34,7 +34,7 @@ function component(){ el("strong", "Component") ); } -

To better understanding we implement function elClass helping to create component as class instances.

import { el } from "./esm-with-observables.js"; +

To better understanding we implement function elClass helping to create component as class instances.

import { el } from "./esm-with-signals.js"; class Test { constructor(params){ this._params= params; @@ -49,7 +49,7 @@ document.body.append( elClass(Test, { textContent: "Hello World" }) ); -import { chainableAppend, scope } from "./esm-with-observables.js"; +import { chainableAppend, scope } from "./esm-with-signals.js"; function elClass(_class, attributes, ...addons){ let element, element_host; scope.push({ @@ -78,7 +78,7 @@ function elClass(_class, attributes, ...addons){ scope.pop(); return element; } -

As you can see, the scope.host() is stored temporarily and synchronously. Therefore, at least in the beginning of using library, it is the good practise to store host in the root of your component. As it may be changed, typically when there is asynchronous code in the component.

import { el, scope, on, dispatchEvent } from "deka-dom-el"; +

As you can see, the scope.host() is stored temporarily and synchronously. Therefore, at least in the beginning of using library, it is the good practise to store host in the root of your component. As it may be changed, typically when there is asynchronous code in the component.

import { el, scope, on, dispatchEvent } from "deka-dom-el"; document.body.append( el(component) ); @@ -96,7 +96,7 @@ function component(){ setTimeout(()=> dispatchEvent("timeout")(host()), 750) return el("p", "Clickable paragraph!"); } -

# Scopes, observables and cleaning magic

The host is internally used to register the cleaning procedure, when the component (host element) is removed from the DOM.

import { el, empty } from "./esm-with-observables.js"; +

# Scopes, signals and cleaning magic

The host is internally used to register the cleaning procedure, when the component (host element) is removed from the DOM.

import { el, empty, on } from "./esm-with-signals.js"; document.body.append( el(component), el("button", { @@ -105,23 +105,22 @@ document.body.append( type: "button" }) ); -import { on } from "./esm-with-observables.js"; -import { O } from "./esm-with-observables.js"; +import { S } from "./esm-with-signals.js"; function component(){ - const textContent= O("Click to change text."); + const textContent= S("Click to change text."); const onclickChange= on("click", function redispatch(){ textContent("Text changed! "+(new Date()).toString()) }); return el("p", textContent, onclickChange); } -

The text content of the paragraph is changing when the value of the observable textContent is changed. Internally, there is association between textContent and the paragraph similar to using S.on(textContent, /* update the paragraph */).

This listener must be removed when the component is removed from the DOM. To do it, the library assign internally on.disconnected(/* remove the listener */)(host()) to the host element.

The library DOM API and observables works ideally when used declaratively. It means, you split your app logic into three parts as it was itroduced in Observables.

/* PSEUDO-CODE!!! */ +

The text content of the paragraph is changing when the value of the signal textContent is changed. Internally, there is association between textContent and the paragraph similar to using S.on(textContent, /* update the paragraph */).

This listener must be removed when the component is removed from the DOM. To do it, the library assign internally on.disconnected(/* remove the listener */)(host()) to the host element.

The library DOM API and signals works ideally when used declaratively. It means, you split your app logic into three parts as it was itroduced in Signals.

/* PSEUDO-CODE!!! */ import { el } from "deka-dom-el"; -import { O } from "deka-dom-el/observables"; +import { S } from "deka-dom-el/signals"; function component(){ /* prepare changeable data */ - const dataA= O("data"); - const dataB= O("data"); + const dataA= S("data"); + const dataB= S("data"); /* define data flow (can be asynchronous) */ fetchAPI().then(data_new=> dataA(data_new)); setTimeout(()=> dataB("DATA")); @@ -134,23 +133,23 @@ function component(){ }), el("ul").append( /* declarative element(s) */ - O.el(dataA, data=> data.map(d=> el("li", d))) + S.el(dataA, data=> data.map(d=> el("li", d))) ), el("ul").append( /* declarative component(s) */ - O.el(dataA, data=> data.map(d=> el(subcomponent, d))) + S.el(dataA, data=> data.map(d=> el(subcomponent, d))) ) ); } function subcomponent({ id }){ /* prepare changeable data */ - const textContent= O("…"); + const textContent= S("…"); /* define data flow (can be asynchronous) */ fetchAPI(id).then(text=> textContent(text)); /* declarative UI */ return el("li", { textContent, dataId: id }); } -

Strictly speaking, the imperative way of using the library is not prohibited. Just be careful (rather avoid) mixing declarative approach (using observables) and imperative manipulation of elements.

/* PSEUDO-CODE!!! */ +

Strictly speaking, the imperative way of using the library is not prohibited. Just be careful (rather avoid) mixing declarative approach (using signals) and imperative manipulation of elements.

/* PSEUDO-CODE!!! */ import { el, on, scope } from "deka-dom-el"; function component(){ const { host }= scope; @@ -181,4 +180,4 @@ function component(){ * // UNEXPECTEDLY REMOVED!!! * */ } -

# Mnemonic

  • el(<function>, <function-argument(s)>)[.append(...)]: <element-returned-by-function> — using component represented by function
  • scope.host() — get current component reference
  • scope.host(...<addons>) — use addons to current component
\ No newline at end of file +

# Mnemonic

  • el(<function>, <function-argument(s)>)[.append(...)]: <element-returned-by-function> — using component represented by function
  • scope.host() — get current component reference
  • scope.host(...<addons>) — use addons to current component
\ No newline at end of file diff --git a/docs/p06-customElement.html b/docs/p06-customElement.html index 2e9d27c..810744b 100644 --- a/docs/p06-customElement.html +++ b/docs/p06-customElement.html @@ -1,12 +1,12 @@ -`deka-dom-el` — Custom elements

`deka-dom-el` — Custom elements

Using custom elements in combinantion with DDE

Using custom elements in combinantion with DDE

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js +`deka-dom-el` — Custom elements

`deka-dom-el` — Custom elements

Using custom elements in combinantion with DDE

Using custom elements in combinantion with DDE

// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js import { customElementRender, customElementWithDDE, observedAttributes, } from "deka-dom-el"; /** @type {ddePublicElementTagNameMap} */ -import { O } from "deka-dom-el/observables"; -O.observedAttributes; +import { O as S } from "deka-dom-el/signals"; +S.observedAttributes; // “internal” utils import { lifecyclesToEvents } from "deka-dom-el"; @@ -31,4 +31,4 @@ import { lifecyclesToEvents } from "deka-dom-el"; set customAttribute(value){ this.setAttribute("custom-attribute", value); } } customElements.define(CustomHTMLElement.tagName, CustomHTMLElement); -

Handy Custom Elements' Patterns

# Mnemonic

  • customElementRender(<custom-element>, <render-function>[, <properties>]) — use function to render DOM structure for given <custom-element>
  • customElementWithDDE(<custom-element>) — register <custom-element> to DDE library, see also `lifecyclesToEvents`, can be also used as decorator
  • observedAttributes(<custom-element>) — returns record of observed attributes (keys uses camelCase)
  • O.observedAttributes(<custom-element>) — returns record of observed attributes (keys uses camelCase and values are observables)
  • lifecyclesToEvents(<class-declaration>) — convert lifecycle methods to events, can be also used as decorator
\ No newline at end of file +

Handy Custom Elements' Patterns

# Mnemonic

  • customElementRender(<custom-element>, <render-function>[, <properties>]) — use function to render DOM structure for given <custom-element>
  • customElementWithDDE(<custom-element>) — register <custom-element> to DDE library, see also `lifecyclesToEvents`, can be also used as decorator
  • observedAttributes(<custom-element>) — returns record of observed attributes (keys uses camelCase)
  • S.observedAttributes(<custom-element>) — returns record of observed attributes (keys uses camelCase and values are signals)
  • lifecyclesToEvents(<class-declaration>) — convert lifecycle methods to events, can be also used as decorator
\ No newline at end of file diff --git a/docs_src/components/example.html.js b/docs_src/components/example.html.js index 1c9f7d7..c049edc 100644 --- a/docs_src/components/example.html.js +++ b/docs_src/components/example.html.js @@ -14,7 +14,7 @@ ${host}{ } `; -const dde_content= s.cat(new URL("../../dist/esm-with-observables.js", import.meta.url)).toString(); +const dde_content= s.cat(new URL("../../dist/esm-with-signals.js", import.meta.url)).toString(); import { el } from "deka-dom-el"; import { code } from "./code.html.js"; @@ -29,7 +29,7 @@ import { relative } from "node:path"; export function example({ src, language= "js", page_id }){ registerClientPart(page_id); const content= s.cat(src).toString() - .replaceAll(/ from "deka-dom-el(\/observables)?";/g, ' from "./esm-with-observables.js";'); + .replaceAll(/ from "deka-dom-el(\/signals)?";/g, ' from "./esm-with-signals.js";'); const id= "code-example-"+generateCodeId(src); return el().append( el(code, { id, content, language, className: example.name }), @@ -38,7 +38,7 @@ export function example({ src, language= "js", page_id }){ } function elCode({ id, content, extension: name }){ const options= JSON.stringify({ - files: [{ name, content }, { name: "esm-with-observables.js", content: dde_content }], + files: [{ name, content }, { name: "esm-with-signals.js", content: dde_content }], toolbar: false }); return el("script", `Flems(document.getElementById("${id}"), JSON.parse(${JSON.stringify(options)}));`); diff --git a/docs_src/components/examples/customElement/intro.js b/docs_src/components/examples/customElement/intro.js index f716b83..9b7833e 100644 --- a/docs_src/components/examples/customElement/intro.js +++ b/docs_src/components/examples/customElement/intro.js @@ -1,12 +1,12 @@ -// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js +// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js import { customElementRender, customElementWithDDE, observedAttributes, } from "deka-dom-el"; /** @type {ddePublicElementTagNameMap} */ -import { O } from "deka-dom-el/observables"; -O.observedAttributes; +import { S } from "deka-dom-el/signals"; +S.observedAttributes; // “internal” utils import { lifecyclesToEvents } from "deka-dom-el"; diff --git a/docs_src/components/examples/helloWorld.js b/docs_src/components/examples/helloWorld.js index a3cb2a3..37d13b1 100644 --- a/docs_src/components/examples/helloWorld.js +++ b/docs_src/components/examples/helloWorld.js @@ -1,9 +1,9 @@ import { el } from "deka-dom-el"; -import { O } from "deka-dom-el/observables"; -const clicks= O(0); +import { S } from "deka-dom-el/signals"; +const clicks= S(0); document.body.append( el().append( - el("p", O(()=> + el("p", S(()=> "Hello World "+"🎉".repeat(clicks()) )), el("button", { diff --git a/docs_src/components/examples/observables/computations-abort.js b/docs_src/components/examples/observables/computations-abort.js deleted file mode 100644 index dcf7386..0000000 --- a/docs_src/components/examples/observables/computations-abort.js +++ /dev/null @@ -1,16 +0,0 @@ -import { O } from "deka-dom-el/observables"; -const observable= O(0); -// computation pattern -const double= O(()=> 2*observable()); - -const ac= new AbortController(); -O.on(observable, v=> console.log("observable", v), { signal: ac.signal }); -O.on(double, v=> console.log("double", v), { signal: ac.signal }); - -observable(observable()+1); -const interval= 5 * 1000; -const id= setInterval(()=> observable(observable()+1), interval); -ac.signal.addEventListener("abort", - ()=> setTimeout(()=> clearInterval(id), 2*interval)); - -setTimeout(()=> ac.abort(), 3*interval) diff --git a/docs_src/components/examples/observables/intro.js b/docs_src/components/examples/observables/intro.js deleted file mode 100644 index fc0c53b..0000000 --- a/docs_src/components/examples/observables/intro.js +++ /dev/null @@ -1,6 +0,0 @@ -// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js -import { O, observable } from "deka-dom-el/observables"; -O===observable -/** @type {ddeObservable} */ -/** @type {ddeAction} */ -/** @type {ddeActions} */ diff --git a/docs_src/components/examples/observables/observables.js b/docs_src/components/examples/observables/observables.js deleted file mode 100644 index d154991..0000000 --- a/docs_src/components/examples/observables/observables.js +++ /dev/null @@ -1,12 +0,0 @@ -import { O } from "deka-dom-el/observables"; -// α — `observable` represents a reactive value -const observable= O(0); -// β — just reacts on observable changes -O.on(observable, console.log); -// γ — just updates the value -const update= ()=> observable(observable()+1); - -update(); -const interval= 5*1000; -setTimeout(clearInterval, 10*interval, - setInterval(update, interval)); diff --git a/docs_src/components/examples/scopes/cleaning.js b/docs_src/components/examples/scopes/cleaning.js index 8447f03..923b331 100644 --- a/docs_src/components/examples/scopes/cleaning.js +++ b/docs_src/components/examples/scopes/cleaning.js @@ -7,9 +7,9 @@ document.body.append( type: "button" }) ); -import { O } from "deka-dom-el/observables"; +import { S } from "deka-dom-el/signals"; function component(){ - const textContent= O("Click to change text."); + const textContent= S("Click to change text."); const onclickChange= on("click", function redispatch(){ textContent("Text changed! "+(new Date()).toString()) diff --git a/docs_src/components/examples/scopes/declarative.js b/docs_src/components/examples/scopes/declarative.js index 46716ad..e87c7a2 100644 --- a/docs_src/components/examples/scopes/declarative.js +++ b/docs_src/components/examples/scopes/declarative.js @@ -1,10 +1,10 @@ /* PSEUDO-CODE!!! */ import { el } from "deka-dom-el"; -import { O } from "deka-dom-el/observables"; +import { S } from "deka-dom-el/signals"; function component(){ /* prepare changeable data */ - const dataA= O("data"); - const dataB= O("data"); + const dataA= S("data"); + const dataB= S("data"); /* define data flow (can be asynchronous) */ fetchAPI().then(data_new=> dataA(data_new)); setTimeout(()=> dataB("DATA")); @@ -17,17 +17,17 @@ function component(){ }), el("ul").append( /* declarative element(s) */ - O.el(dataA, data=> data.map(d=> el("li", d))) + S.el(dataA, data=> data.map(d=> el("li", d))) ), el("ul").append( /* declarative component(s) */ - O.el(dataA, data=> data.map(d=> el(subcomponent, d))) + S.el(dataA, data=> data.map(d=> el(subcomponent, d))) ) ); } function subcomponent({ id }){ /* prepare changeable data */ - const textContent= O("…"); + const textContent= S("…"); /* define data flow (can be asynchronous) */ fetchAPI(id).then(text=> textContent(text)); /* declarative UI */ diff --git a/docs_src/components/examples/scopes/intro.js b/docs_src/components/examples/scopes/intro.js index 2706af3..7b50595 100644 --- a/docs_src/components/examples/scopes/intro.js +++ b/docs_src/components/examples/scopes/intro.js @@ -1,3 +1,3 @@ -// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-observables.js +// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js import { scope, el } from "deka-dom-el"; /** @type {ddeElementAddon} */ diff --git a/docs_src/components/examples/observables/actions-demo.js b/docs_src/components/examples/signals/actions-demo.js similarity index 58% rename from docs_src/components/examples/observables/actions-demo.js rename to docs_src/components/examples/signals/actions-demo.js index 97b1904..f21c9a4 100644 --- a/docs_src/components/examples/observables/actions-demo.js +++ b/docs_src/components/examples/signals/actions-demo.js @@ -1,14 +1,14 @@ -import { O } from "deka-dom-el/observables"; -const observable= O(0, { +import { S } from "deka-dom-el/signals"; +const signal= S(0, { increaseOnlyOdd(add){ console.info(add); if(add%2 === 0) return this.stopPropagation(); this.value+= add; } }); -O.on(observable, console.log); +S.on(signal, console.log); const oninterval= ()=> - O.action(observable, "increaseOnlyOdd", Math.floor(Math.random()*100)); + S.action(signal, "increaseOnlyOdd", Math.floor(Math.random()*100)); const interval= 5*1000; setTimeout( diff --git a/docs_src/components/examples/observables/actions-todos.js b/docs_src/components/examples/signals/actions-todos.js similarity index 78% rename from docs_src/components/examples/observables/actions-todos.js rename to docs_src/components/examples/signals/actions-todos.js index c0a64d9..24d2cda 100644 --- a/docs_src/components/examples/observables/actions-todos.js +++ b/docs_src/components/examples/signals/actions-todos.js @@ -1,14 +1,14 @@ -import { O } from "deka-dom-el/observables"; -const todos= O([], { +import { S } from "deka-dom-el/signals"; +const todos= S([], { push(item){ - this.value.push(O(item)); + this.value.push(S(item)); }, pop(){ const removed= this.value.pop(); - if(removed) O.clear(removed); + if(removed) S.clear(removed); }, - [O.symbols.onclear](){ // this covers `O.clear(todos)` - O.clear(...this.value); + [S.symbols.onclear](){ // this covers `O.clear(todos)` + S.clear(...this.value); } }); @@ -19,7 +19,7 @@ const onsubmit= on("submit", function(event){ const data= new FormData(this); switch (data.get("op")){ case "A"/*dd*/: - O.action(todos, "push", data.get("todo")); + S.action(todos, "push", data.get("todo")); break; case "E"/*dit*/: { const last= todos().at(-1); @@ -28,13 +28,13 @@ const onsubmit= on("submit", function(event){ break; } case "R"/*emove*/: - O.action(todos, "pop"); + S.action(todos, "pop"); break; } }); document.body.append( el("ul").append( - O.el(todos, todos=> + S.el(todos, todos=> todos.map(textContent=> el("li", textContent))) ), el("form", null, onsubmit).append( diff --git a/docs_src/components/examples/signals/computations-abort.js b/docs_src/components/examples/signals/computations-abort.js new file mode 100644 index 0000000..9573973 --- /dev/null +++ b/docs_src/components/examples/signals/computations-abort.js @@ -0,0 +1,16 @@ +import { S } from "deka-dom-el/signals"; +const signal= S(0); +// computation pattern +const double= S(()=> 2*signal()); + +const ac= new AbortController(); +S.on(signal, v=> console.log("signal", v), { signal: ac.signal }); +S.on(double, v=> console.log("double", v), { signal: ac.signal }); + +signal(signal()+1); +const interval= 5 * 1000; +const id= setInterval(()=> signal(signal()+1), interval); +ac.signal.addEventListener("abort", + ()=> setTimeout(()=> clearInterval(id), 2*interval)); + +setTimeout(()=> ac.abort(), 3*interval) diff --git a/docs_src/components/examples/observables/dom-attrs.js b/docs_src/components/examples/signals/dom-attrs.js similarity index 62% rename from docs_src/components/examples/observables/dom-attrs.js rename to docs_src/components/examples/signals/dom-attrs.js index 4d7a336..a26219e 100644 --- a/docs_src/components/examples/observables/dom-attrs.js +++ b/docs_src/components/examples/signals/dom-attrs.js @@ -1,10 +1,10 @@ -import { O } from "deka-dom-el/observables"; -const count= O(0); +import { S } from "deka-dom-el/signals"; +const count= S(0); import { el } from "deka-dom-el"; document.body.append( - el("p", O(()=> "Currently: "+count())), - el("p", { classList: { red: O(()=> count()%2) }, dataset: { count }, textContent: "Attributes example" }) + el("p", S(()=> "Currently: "+count())), + el("p", { classList: { red: S(()=> count()%2) }, dataset: { count }, textContent: "Attributes example" }) ); document.head.append( el("style", ".red { color: red; }") diff --git a/docs_src/components/examples/observables/dom-el.js b/docs_src/components/examples/signals/dom-el.js similarity index 62% rename from docs_src/components/examples/observables/dom-el.js rename to docs_src/components/examples/signals/dom-el.js index e473df7..5620081 100644 --- a/docs_src/components/examples/observables/dom-el.js +++ b/docs_src/components/examples/signals/dom-el.js @@ -1,26 +1,26 @@ -import { O } from "deka-dom-el/observables"; -const count= O(0, { +import { S } from "deka-dom-el/signals"; +const count= S(0, { add(){ this.value= this.value + Math.round(Math.random()*10); } }); -const numbers= O([ count() ], { +const numbers= S([ count() ], { push(next){ this.value.push(next); } }); import { el } from "deka-dom-el"; document.body.append( - O.el(count, count=> count%2 + S.el(count, count=> count%2 ? el("p", "Last number is odd.") : el() ), el("p", "Lucky numbers:"), el("ul").append( - O.el(numbers, numbers=> numbers.toReversed() + S.el(numbers, numbers=> numbers.toReversed() .map(n=> el("li", n))) ) ); const interval= 5*1000; setTimeout(clearInterval, 10*interval, setInterval(function(){ - O.action(count, "add"); - O.action(numbers, "push", count()); + S.action(count, "add"); + S.action(numbers, "push", count()); }, interval)); diff --git a/docs_src/components/examples/signals/intro.js b/docs_src/components/examples/signals/intro.js new file mode 100644 index 0000000..513f0cd --- /dev/null +++ b/docs_src/components/examples/signals/intro.js @@ -0,0 +1,6 @@ +// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js +import { S, signal } from "deka-dom-el/signals"; +S===signal +/** @type {ddeSignal} */ +/** @type {ddeAction} */ +/** @type {ddeActions} */ diff --git a/docs_src/components/examples/signals/signals.js b/docs_src/components/examples/signals/signals.js new file mode 100644 index 0000000..8178a41 --- /dev/null +++ b/docs_src/components/examples/signals/signals.js @@ -0,0 +1,12 @@ +import { S } from "deka-dom-el/signals"; +// α — `signal` represents a reactive value +const signal= S(0); +// β — just reacts on signal changes +S.on(signal, console.log); +// γ — just updates the value +const update= ()=> signal(signal()+1); + +update(); +const interval= 5*1000; +setTimeout(clearInterval, 10*interval, + setInterval(update, interval)); diff --git a/docs_src/components/mnemonic/customElement-init.js b/docs_src/components/mnemonic/customElement-init.js index 92500ce..c256893 100644 --- a/docs_src/components/mnemonic/customElement-init.js +++ b/docs_src/components/mnemonic/customElement-init.js @@ -13,7 +13,7 @@ export function mnemonic(){ el("code", "observedAttributes()"), " — returns record of observed attributes (keys uses camelCase)", ), el("li").append( - el("code", "O.observedAttributes()"), " — returns record of observed attributes (keys uses camelCase and values are observables)", + el("code", "S.observedAttributes()"), " — returns record of observed attributes (keys uses camelCase and values are signals)", ), el("li").append( el("code", "lifecyclesToEvents()"), " — convert lifecycle methods to events, can be also used as decorator", diff --git a/docs_src/components/mnemonic/observables-init.js b/docs_src/components/mnemonic/observables-init.js deleted file mode 100644 index 2dea569..0000000 --- a/docs_src/components/mnemonic/observables-init.js +++ /dev/null @@ -1,28 +0,0 @@ -import { el } from "deka-dom-el"; -import { mnemonicUl } from "../mnemonicUl.html.js"; - -export function mnemonic(){ - return mnemonicUl().append( - el("li").append( - el("code", "O()"), " — observable: reactive value", - ), - el("li").append( - el("code", "O(()=> )"), " — read-only observable: reactive value dependent on calculation using other observables", - ), - el("li").append( - el("code", "O.on(, [, ])"), " — listen to the observable value changes", - ), - el("li").append( - el("code", "O.clear(...)"), " — off and clear observables", - ), - el("li").append( - el("code", "O(, )"), " — observable: pattern to create complex reactive objects/arrays", - ), - el("li").append( - el("code", "O.action(, , ...)"), " — invoke an action for given observable" - ), - el("li").append( - el("code", "O.el(, )"), " — render partial dom structure (template) based on the current observable value", - ) - ); -} diff --git a/docs_src/components/mnemonic/signals-init.js b/docs_src/components/mnemonic/signals-init.js new file mode 100644 index 0000000..7a6d0a6 --- /dev/null +++ b/docs_src/components/mnemonic/signals-init.js @@ -0,0 +1,28 @@ +import { el } from "deka-dom-el"; +import { mnemonicUl } from "../mnemonicUl.html.js"; + +export function mnemonic(){ + return mnemonicUl().append( + el("li").append( + el("code", "S()"), " — signal: reactive value", + ), + el("li").append( + el("code", "S(()=> )"), " — read-only signal: reactive value dependent on calculation using other signals", + ), + el("li").append( + el("code", "S.on(, [, ])"), " — listen to the signal value changes", + ), + el("li").append( + el("code", "S.clear(...)"), " — off and clear signals", + ), + el("li").append( + el("code", "S(, )"), " — signal: pattern to create complex reactive objects/arrays", + ), + el("li").append( + el("code", "S.action(, , ...)"), " — invoke an action for given signal" + ), + el("li").append( + el("code", "S.el(, )"), " — render partial dom structure (template) based on the current signal value", + ) + ); +} diff --git a/docs_src/index.html.js b/docs_src/index.html.js index d3dd2e9..dc98244 100644 --- a/docs_src/index.html.js +++ b/docs_src/index.html.js @@ -1,6 +1,11 @@ -import { simplePage } from "./layout/simplePage.html.js"; +export const info= { + href: "./", + title: "Introduction", + description: "Introducing a library.", +}; import { el } from "deka-dom-el"; +import { simplePage } from "./layout/simplePage.html.js"; import { example } from "./components/example.html.js"; /** @param {import("./types.d.ts").PageAttrs} attrs */ export function page({ pkg, info }){ @@ -23,10 +28,10 @@ export function page({ pkg, info }){ el("p").append( "Next step is providing interactivity not only for our UI templates.", " ", - "We introduce observables (", el("code", "O"), ") and how them incorporate to UI templates.", + "We introduce signals (", el("code", "O"), ") and how them incorporate to UI templates.", ), el("p").append( - "Now we will clarify how the observables are incorporated into our templates with regard ", + "Now we will clarify how the signals are incorporated into our templates with regard ", "to application performance. This is not the only reason the library uses ", el("code", "scope"), "s. We will look at how they work in components represented ", "in JavaScript by functions." diff --git a/docs_src/p02-elements.html.js b/docs_src/p02-elements.html.js index 81c2699..5c20766 100644 --- a/docs_src/p02-elements.html.js +++ b/docs_src/p02-elements.html.js @@ -1,6 +1,10 @@ -import { simplePage } from "./layout/simplePage.html.js"; +export const info= { + title: "Elements", + description: "Basic concepts of elements modifications and creations.", +}; import { el } from "deka-dom-el"; +import { simplePage } from "./layout/simplePage.html.js"; import { example } from "./components/example.html.js"; import { h3 } from "./components/pageUtils.html.js"; import { mnemonic } from "./components/mnemonic/elements-init.js"; @@ -64,7 +68,7 @@ export function page({ pkg, info }){ "This is handy to concat conditional classes." ), el("li").append( - "Use ", el("code", "classList"), " to toggle specific classes. This will be handy later when the reactivity via observables is beeing introduced.", + "Use ", el("code", "classList"), " to toggle specific classes. This will be handy later when the reactivity via signals is beeing introduced.", ), el("li").append( "The ", el("code", "assign"), " also accepts the ", el("code", "undefined"), " as a value for any property to remove it from the element declaratively. ", diff --git a/docs_src/p03-events.html.js b/docs_src/p03-events.html.js index 451617f..70ce51e 100644 --- a/docs_src/p03-events.html.js +++ b/docs_src/p03-events.html.js @@ -1,6 +1,10 @@ -import { simplePage } from "./layout/simplePage.html.js"; +export const info= { + title: "Events and Addons", + description: "Using not only events in UI declaratively.", +}; import { el } from "deka-dom-el"; +import { simplePage } from "./layout/simplePage.html.js"; import { example } from "./components/example.html.js"; import { h3 } from "./components/pageUtils.html.js"; import { mnemonic } from "./components/mnemonic/events-init.js"; diff --git a/docs_src/p04-observables.html.js b/docs_src/p04-signals.html.js similarity index 60% rename from docs_src/p04-observables.html.js rename to docs_src/p04-signals.html.js index d6c0753..d7d480c 100644 --- a/docs_src/p04-observables.html.js +++ b/docs_src/p04-signals.html.js @@ -1,9 +1,13 @@ -import { simplePage } from "./layout/simplePage.html.js"; +export const info= { + title: "Signals and reactivity", + description: "Handling reactivity in UI via signals.", +}; import { el } from "deka-dom-el"; +import { simplePage } from "./layout/simplePage.html.js"; import { example } from "./components/example.html.js"; import { h3 } from "./components/pageUtils.html.js"; -import { mnemonic } from "./components/mnemonic/observables-init.js"; +import { mnemonic } from "./components/mnemonic/signals-init.js"; import { code } from "./components/code.html.js"; /** @param {string} url */ const fileURL= url=> new URL(url, import.meta.url); @@ -12,24 +16,24 @@ const fileURL= url=> new URL(url, import.meta.url); export function page({ pkg, info }){ const page_id= info.id; return el(simplePage, { info, pkg }).append( - el("h2", "Using observables to manage reactivity"), + el("h2", "Using signals to manage reactivity"), el("p").append( "How a program responds to variable data or user", " interactions is one of the fundamental problems of programming.", " If we desire to solve the issue in a declarative manner,", - " observables may be a viable approach.", + " signals may be a viable approach.", ), - el(code, { src: fileURL("./components/examples/observables/intro.js"), page_id }), + el(code, { src: fileURL("./components/examples/signals/intro.js"), page_id }), - el(h3, "Introducing observables"), + el(h3, "Introducing signals"), el("p").append( - "Using observables, we split program logic into the three parts.", + "Using signals, we split program logic into the three parts.", " Firstly (α), we create a variable (constant) representing reactive", " value. Somewhere later, we can register (β) a logic reacting", - " to the observable value changes. Similarly, in a remaining part (γ), we", - " can update the observable value." + " to the signal value changes. Similarly, in a remaining part (γ), we", + " can update the signal value." ), - el(example, { src: fileURL("./components/examples/observables/observables.js"), page_id }), + el(example, { src: fileURL("./components/examples/signals/signals.js"), page_id }), el("p").append( "All this is just an example of ", el("a", { textContent: "Event-driven programming", href: "https://en.wikipedia.org/wiki/Event-driven_programming", title: "Wikipedia: Event-driven programming" }), @@ -40,35 +44,35 @@ export function page({ pkg, info }){ " to the same reactive entity." ), el("p").append( - "Observables are implemented in the library as functions. To see current value", - " of observable, just call it without any arguments ", el("code", "console.log(observable())"), ".", - " To update the observable value, pass any argument ", el("code", "observable('a new value')"), ".", - " For listenning the observable value changes, use ", el("code", "O.on(observable, console.log)"), "." + "Signals are implemented in the library as functions. To see current value", + " of signal, just call it without any arguments ", el("code", "console.log(signal())"), ".", + " To update the signal value, pass any argument ", el("code", "signal('a new value')"), ".", + " For listenning the signal value changes, use ", el("code", "S.on(signal, console.log)"), "." ), el("p").append( "Similarly to the ", el("code", "on"), " function to register DOM events listener.", " You can use ", el("code", "AbortController"), "/", el("code", "AbortSignal"), " to", " ", el("em", "off"), "/stop listenning. In example, you also found the way for representing", - " “live” piece of code computation pattern (derived observable):" + " “live” piece of code computation pattern (derived signal):" ), - el(example, { src: fileURL("./components/examples/observables/computations-abort.js"), page_id }), + el(example, { src: fileURL("./components/examples/signals/computations-abort.js"), page_id }), - el(h3, "Observables and actions"), + el(h3, "Signals and actions"), el("p").append( - el("code", "O(/* primitive */)"), " allows you to declare simple reactive variables, typically", + el("code", "S(/* primitive */)"), " allows you to declare simple reactive variables, typically", " around ", el("em", "immutable"), " ", el("a", { textContent: "primitive types", title: "Primitive | MDN", href: "https://developer.mozilla.org/en-US/docs/Glossary/Primitive" }), ".", " ", "However, it may also be necessary to use reactive arrays, objects, or other complex reactive structures." ), - el(example, { src: fileURL("./components/examples/observables/actions-demo.js"), page_id }), + el(example, { src: fileURL("./components/examples/signals/actions-demo.js"), page_id }), el("p", "…but typical user-case is object/array (maps, sets and other mutable objects):"), - el(example, { src: fileURL("./components/examples/observables/actions-todos.js"), page_id }), + el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), page_id }), el("p").append( "In some way, you can compare it with ", el("a", { textContent: "useReducer", href: "https://react.dev/reference/react/useReducer", title: "useReducer hook | React docs" }), - " hook from React. So, the ", el("code", "O(, )"), " pattern creates", + " hook from React. So, the ", el("code", "S(, )"), " pattern creates", " a store “machine”. We can then invoke (dispatch) registered action by calling", - " ", el("code", "O.action(, , ...)"), " after the action call", - " the observable calls all its listeners. This can be stopped by calling ", el("code", "this.stopPropagation()"), + " ", el("code", "S.action(, , ...)"), " after the action call", + " the signal calls all its listeners. This can be stopped by calling ", el("code", "this.stopPropagation()"), " in the method representing the given action. As it can be seen in examples, the “store” value is", " available also in the function for given action (", el("code", "this.value"), ")." ), @@ -79,24 +83,24 @@ export function page({ pkg, info }){ el("li", "to change some attribute(s) of existing element(s)"), el("li", "to generate elements itself dynamically – this covers conditions and loops") ), - el(example, { src: fileURL("./components/examples/observables/dom-attrs.js"), page_id }), + el(example, { src: fileURL("./components/examples/signals/dom-attrs.js"), page_id }), el("p").append( - "To derived attribute based on value of observable variable just use the observable as", - " a value of the attribute (", el("code", "assign(element, { attribute: O('value') })"), ").", + "To derived attribute based on value of signal variable just use the signal as", + " a value of the attribute (", el("code", "assign(element, { attribute: S('value') })"), ").", " ", el("code", "assign"), "/", el("code", "el"), " provides ways to glue reactive attributes/classes", " more granularly into the DOM. Just use dedicated build-in attributes ", el("code", "dataset"), ", ", el("code", "ariaset"), " and ", el("code", "classList"), "." ), el("p").append( - "For computation, you can use the “derived observable” (see above) like ", el("code", "assign(element, { textContent: O(()=> 'Hello '+WorldObservable()) })"), ".", + "For computation, you can use the “derived signal” (see above) like ", el("code", "assign(element, { textContent: S(()=> 'Hello '+WorldSignal()) })"), ".", " ", - "This is read-only observable its value is computed based on given function and updated when any observable used in the function changes." + "This is read-only signal its value is computed based on given function and updated when any signal used in the function changes." ), el("p").append( - "To represent part of the template filled dynamically based on the observable value use ", el("code", "O.el(observable, DOMgenerator)"), ".", + "To represent part of the template filled dynamically based on the signal value use ", el("code", "S.el(signal, DOMgenerator)"), ".", " This was already used in the todo example above or see:" ), - el(example, { src: fileURL("./components/examples/observables/dom-el.js"), page_id }), + el(example, { src: fileURL("./components/examples/signals/dom-el.js"), page_id }), el(mnemonic) ); diff --git a/docs_src/p05-scopes.html.js b/docs_src/p05-scopes.html.js index 56aa15d..78e6cbe 100644 --- a/docs_src/p05-scopes.html.js +++ b/docs_src/p05-scopes.html.js @@ -1,6 +1,10 @@ -import { simplePage } from "./layout/simplePage.html.js"; +export const info= { + title: "Scopes and components", + description: "Organizing UI into components", +}; import { el } from "deka-dom-el"; +import { simplePage } from "./layout/simplePage.html.js"; import { example } from "./components/example.html.js"; import { h3 } from "./components/pageUtils.html.js"; import { mnemonic } from "./components/mnemonic/scopes-init.js"; @@ -43,14 +47,14 @@ export function page({ pkg, info }){ ), el(code, { src: fileURL("./components/examples/scopes/good-practise.js"), page_id }), - el(h3, "Scopes, observables and cleaning magic"), + el(h3, "Scopes, signals and cleaning magic"), el("p").append( "The ", el("code", "host"), " is internally used to register the cleaning procedure,", " when the component (", el("code", "host"), " element) is removed from the DOM." ), el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }), el("p").append( - "The text content of the paragraph is changing when the value of the observable ", el("code", "textContent"), + "The text content of the paragraph is changing when the value of the signal ", el("code", "textContent"), " is changed. Internally, there is association between ", el("code", "textContent"), " and the paragraph", " similar to using ", el("code", "S.on(textContent, /* update the paragraph */)"), "." ), @@ -59,13 +63,13 @@ export function page({ pkg, info }){ " assign internally ", el("code", "on.disconnected(/* remove the listener */)(host())"), " to the host element." ), el("p", { className: "notice" }).append( - "The library DOM API and observables works ideally when used declaratively.", - " It means, you split your app logic into three parts as it was itroduced in ", el("a", { textContent: "Observables", href: "http://localhost:40911/docs/p04-observables#h-introducing-observables" }), "." + "The library DOM API and signals works ideally when used declaratively.", + " It means, you split your app logic into three parts as it was itroduced in ", el("a", { textContent: "Signals", href: "http://localhost:40911/docs/p04-signals#h-introducing-signals" }), "." ), el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id }), el("p").append( "Strictly speaking, the imperative way of using the library is not prohibited.", - " Just be careful (rather avoid) mixing declarative approach (using observables)", + " Just be careful (rather avoid) mixing declarative approach (using signals)", " and imperative manipulation of elements.", ), el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id }), diff --git a/docs_src/p06-customElement.html.js b/docs_src/p06-customElement.html.js index b36f51c..605e507 100644 --- a/docs_src/p06-customElement.html.js +++ b/docs_src/p06-customElement.html.js @@ -1,6 +1,10 @@ -import { simplePage } from "./layout/simplePage.html.js"; +export const info= { + title: "Custom elements", + description: "Using custom elements in combinantion with DDE", +}; import { el } from "deka-dom-el"; +import { simplePage } from "./layout/simplePage.html.js"; import { example } from "./components/example.html.js"; import { h3 } from "./components/pageUtils.html.js"; import { mnemonic } from "./components/mnemonic/customElement-init.js"; diff --git a/docs_src/ssr.js b/docs_src/ssr.js index 9c430a8..f6e4466 100644 --- a/docs_src/ssr.js +++ b/docs_src/ssr.js @@ -2,14 +2,11 @@ export const path_target= { root: "docs/", css: "docs/" }; -export const pages= [ - { id: "index", href: "./", title: "Introduction", description: "Introducing a library." }, - { id: "p02-elements", href: "p02-elements", title: "Elements", description: "Basic concepts of elements modifications and creations." }, - { id: "p03-events", href: "p03-events", title: "Events and Addons", description: "Using not only events in UI declaratively." }, - { id: "p04-observables", href: "p04-observables", title: "Observables and reactivity", description: "Handling reactivity in UI via observables." }, - { id: "p05-scopes", href: "p05-scopes", title: "Scopes and components", description: "Organizing UI into components" }, - { id: "p06-customElement", href: "p06-customElement", title: "Custom elements", description: "Using custom elements in combinantion with DDE" }, -]; +/** + * This variable will be filled with the list of pages during the build process (see `bs/docs.js`). + * @type {import("./types.d.ts").Info[]} + * */ +export let pages= []; /** * @typedef registerClientFile * @type {function} diff --git a/examples/components/3rd-party.js b/examples/components/3rd-party.js index 332ba35..21db48c 100644 --- a/examples/components/3rd-party.js +++ b/examples/components/3rd-party.js @@ -1,4 +1,4 @@ -import { style, el, O, isObservable } from '../exports.js'; +import { style, el, S, isSignal } from '../exports.js'; const className= style.host(thirdParty).css` :host { color: green; @@ -10,22 +10,22 @@ const store_adapter= { write(data){ console.log(data); history.replaceState("", "", "?"+(new URLSearchParams(data)).toString()); } }; export function thirdParty(){ - const store= O({ - value: O("initial") + const store= S({ + value: S("initial") }, { set(key, value){ - const p= this.value[key] || O(); + const p= this.value[key] || S(); p(value); this.value[key]= p; } }); // Array.from((new URL(location)).searchParams.entries()) - // .forEach(([ key, value ])=> O.action(store, "set", key, value)); - // O.on(store, data=> history.replaceState("", "", "?"+(new URLSearchParams(JSON.parse(JSON.stringify(data)))).toString())); + // .forEach(([ key, value ])=> S.action(store, "set", key, value)); + // S.on(store, data=> history.replaceState("", "", "?"+(new URLSearchParams(JSON.parse(JSON.stringify(data)))).toString())); useStore(store_adapter, { onread(data){ Array.from(data.entries()) - .forEach(([ key, value ])=> O.action(store, "set", key, value)); + .forEach(([ key, value ])=> S.action(store, "set", key, value)); return store; } })(); @@ -33,18 +33,18 @@ export function thirdParty(){ className, value: store().value(), type: "text", - onchange: ev=> O.action(store, "set", "value", ev.target.value) + onchange: ev=> S.action(store, "set", "value", ev.target.value) }); } function useStore(adapter_in, { onread, onbeforewrite }= {}){ const adapter= typeof adapter_in === "function" ? { read: adapter_in } : adapter_in; - if(!onread) onread= O; + if(!onread) onread= S; if(!onbeforewrite) onbeforewrite= data=> JSON.parse(JSON.stringify(data)); return function useStoreInner(data_read){ - const observable= onread(adapter.read(data_read)); //TODO OK as synchronous - if(adapter.write && isObservable(observable)) - O.on(observable, data=> adapter.write(onbeforewrite(data))); - return observable; + const signal= onread(adapter.read(data_read)); //TODO OK as synchronous + if(adapter.write && isSignal(signal)) + S.on(signal, data=> adapter.write(onbeforewrite(data))); + return signal; }; } diff --git a/examples/components/fullNameComponent.js b/examples/components/fullNameComponent.js index 479b65a..7dd28b1 100644 --- a/examples/components/fullNameComponent.js +++ b/examples/components/fullNameComponent.js @@ -1,4 +1,4 @@ -import { style, el, elNS, on, O, scope } from '../exports.js'; +import { style, el, elNS, on, S, scope } from '../exports.js'; const className= style.host(fullNameComponent).css` :host form{ display: flex; @@ -7,8 +7,8 @@ const className= style.host(fullNameComponent).css` `; export function fullNameComponent(){ const labels= [ "Name", "Surname" ]; - const name= labels.map(_=> O("")); - const full_name= O(()=> + const name= labels.map(_=> S("")); + const full_name= S(()=> name.map(l=> l()).filter(Boolean).join(" ") || "-"); scope.host( on.connected(()=> console.log(fullNameComponent)), diff --git a/examples/components/todosComponent.js b/examples/components/todosComponent.js index f5ce2c0..65a750c 100644 --- a/examples/components/todosComponent.js +++ b/examples/components/todosComponent.js @@ -1,4 +1,4 @@ -import { style, el, dispatchEvent, on, O, scope } from '../exports.js'; +import { style, el, dispatchEvent, on, S, scope } from '../exports.js'; const className= style.host(todosComponent).css` :host{ display: flex; @@ -17,27 +17,27 @@ const className= style.host(todosComponent).css` /** @param {{ todos: string[] }} */ export function todosComponent({ todos= [ "Task A" ] }= {}){ let key= 0; - const todosO= O(new Map(), { - add(v){ this.value.set(key++, O(v)); }, - remove(key){ O.clear(this.value.get(key)); this.value.delete(key); } + const todosO= S(new Map(), { + add(v){ this.value.set(key++, S(v)); }, + remove(key){ S.clear(this.value.get(key)); this.value.delete(key); } }); - todos.forEach(text=> O.action(todosO, "add", text)); + todos.forEach(text=> S.action(todosO, "add", text)); const name= "todoName"; const onsubmitAdd= on("submit", event=> { const el= event.target.elements[name]; event.preventDefault(); - O.action(todosO, "add", el.value); + S.action(todosO, "add", el.value); el.value= ""; }); const onremove= on("remove", event=> - O.action(todosO, "remove", event.detail)); + S.action(todosO, "remove", event.detail)); return el("div", { className }).append( el("div").append( el("h2", "Todos:"), el("h3", "List of todos:"), - O.el(todosO, (ts, memo)=> !ts.size + S.el(todosO, (ts, memo)=> !ts.size ? el("p", "No todos yet") : el("ul").append( ...Array.from(ts).map(([ value, textContent ])=> @@ -55,7 +55,7 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){ ), el("div").append( el("h3", "Output (JSON):"), - el("output", O(()=> JSON.stringify(Array.from(todosO()), null, "\t"))) + el("output", S(()=> JSON.stringify(Array.from(todosO()), null, "\t"))) ) ) } @@ -70,13 +70,13 @@ function todoComponent({ textContent, value }){ event.stopPropagation(); dispatchEvent("remove")(host(), value); }); - const is_editable= O(false); + const is_editable= S(false); const onedited= on("change", ev=> { textContent(ev.target.value); is_editable(false); }); return el("li").append( - O.el(is_editable, is=> is + S.el(is_editable, is=> is ? el("input", { value: textContent(), type: "text" }, onedited) : el("span", { textContent, onclick: is_editable.bind(null, true) }) ), diff --git a/examples/components/webComponent.js b/examples/components/webComponent.js index 4af23bd..e225dd4 100644 --- a/examples/components/webComponent.js +++ b/examples/components/webComponent.js @@ -1,5 +1,5 @@ import { el, on, customElementRender, customElementWithDDE, scope, simulateSlots } from "../../index.js"; -import { O } from "../../observables.js"; +import { S } from "../../signals.js"; /** * Compatible with `npx -y web-component-analyzer examples/components/webComponent.js` @@ -16,7 +16,7 @@ export class CustomHTMLTestElement extends HTMLElement{ } attributes(element){ - const observed= O.observedAttributes(element); + const observed= S.observedAttributes(element); return Object.assign({ test: element.test }, observed); } render({ name, preName, test }){ diff --git a/examples/exports.js b/examples/exports.js index a05eeae..8c4928f 100644 --- a/examples/exports.js +++ b/examples/exports.js @@ -1,10 +1,10 @@ import * as dde_dom from "../index.js"; export * from "../index.js"; -import * as dde_s from "../observables.js"; -export * from "../observables.js"; +import * as dde_s from "../signals.js"; +export * from "../signals.js"; Object.assign(globalThis, dde_dom, dde_s); -//import * as dde_dom from "../dist/esm-with-observables.js"; -//export * from "../dist/esm-with-observables.js"; +//import * as dde_dom from "../dist/esm-with-signals.js"; +//export * from "../dist/esm-with-signals.js"; //Object.assign(globalThis, dde_dom); export const style= createStyle(); diff --git a/index-with-observables.d.ts b/index-with-observables.d.ts deleted file mode 100644 index e563543..0000000 --- a/index-with-observables.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./index"; -export * from "./observables"; diff --git a/index-with-observables.js b/index-with-observables.js deleted file mode 100644 index c64e169..0000000 --- a/index-with-observables.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./index.js"; -export * from "./observables.js"; diff --git a/index-with-signals.d.ts b/index-with-signals.d.ts new file mode 100644 index 0000000..eefb192 --- /dev/null +++ b/index-with-signals.d.ts @@ -0,0 +1,2 @@ +export * from "./index"; +export * from "./signals"; diff --git a/index-with-signals.js b/index-with-signals.js new file mode 100644 index 0000000..e969ab0 --- /dev/null +++ b/index-with-signals.js @@ -0,0 +1,2 @@ +export * from "./index.js"; +export * from "./signals.js"; diff --git a/index.d.ts b/index.d.ts index f4a4536..42c559d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -import { Observable } from "./observables"; +import { Signal } from "./signals"; type CustomElementTagNameMap= { '#text': Text, '#comment': Comment } type SupportedElement= @@ -15,20 +15,20 @@ type AttrsModified= { /** * Use string like in HTML (internally uses `*.setAttribute("style", *)`), or object representation (like DOM API). */ - style: string | Partial | Observable | Partial<{ [K in keyof CSSStyleDeclaration]: Observable }> + style: string | Partial | Signal | Partial<{ [K in keyof CSSStyleDeclaration]: Signal }> /** * Provide option to add/remove/toggle CSS clasess (index of object) using 1/0/-1. In fact `el.classList.toggle(class_name)` for `-1` and `el.classList.toggle(class_name, Boolean(...))` for others. */ - classList: Record>, + classList: Record>, /** * By default simiral to `className`, but also supports `string[]` * */ - className: string | (string|boolean|undefined|Observable)[]; + className: string | (string|boolean|undefined|Signal)[]; /** * Sets `aria-*` simiraly to `dataset` * */ - ariaset: Record>, -} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, string|Observable> & Record<`.${string}`, any> + ariaset: Record>, +} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, string|Signal> & Record<`.${string}`, any> type _fromElsInterfaces= Omit; /** * Just element attributtes @@ -38,13 +38,13 @@ type _fromElsInterfaces= Omit= Partial<_fromElsInterfaces & { [K in keyof _fromElsInterfaces]: Observable<_fromElsInterfaces[K], any> } & AttrsModified> & Record; +type ElementAttributes= Partial<_fromElsInterfaces & { [K in keyof _fromElsInterfaces]: Signal<_fromElsInterfaces[K], any> } & AttrsModified> & Record; export function classListDeclarative(element: El, classList: AttrsModified["classList"]): El export function assign(element: El, ...attrs_array: ElementAttributes[]): El export function assignAttribute>(element: El, attr: ATT, value: ElementAttributes[ATT]): ElementAttributes[ATT] type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap; -type textContent= string | ( (set?: string)=> string ); // TODO: for some reason `Observable` leads to `attrs?: any` +type textContent= string | ( (set?: string)=> string ); // TODO: for some reason `Signal` leads to `attrs?: any` export function el< TAG extends keyof ExtendedHTMLElementTagNameMap & string, EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : HTMLElement) @@ -83,7 +83,7 @@ export function elNS( EL extends ( TAG extends keyof MathMLElementTagNameMap ? MathMLElementTagNameMap[TAG] : MathMLElement ), >( tag_name: TAG, - attrs?: string | textContent | Partial<{ [key in keyof EL]: EL[key] | Observable | string | number | boolean }>, + attrs?: string | textContent | Partial<{ [key in keyof EL]: EL[key] | Signal | string | number | boolean }>, ...addons: ddeElementAddon[] )=> ddeMathMLElement export function elNS( diff --git a/observables.d.ts b/observables.d.ts deleted file mode 100644 index 745f46b..0000000 --- a/observables.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -export type Observable= (set?: V)=> V & A; -type Action= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof observable._ | void; -//type SymbolObservable= Symbol; -type SymbolOnclear= symbol; -type Actions= Record>; -type OnListenerOptions= Pick & { first_time?: boolean }; -interface observable{ - _: Symbol - /** - * Simple example: - * ```js - * const hello= S("Hello Observable"); - * ``` - * …simple todo observable: - * ```js - * const todos= S([], { - * add(v){ this.value.push(S(v)); }, - * remove(i){ this.value.splice(i, 1); }, - * [S.symbols.onclear](){ S.clear(...this.value); }, - * }); - * ``` - * …computed observable: - * ```js - * const name= S("Jan"); - * const surname= S("Andrle"); - * const fullname= S(()=> name()+" "+surname()); - * ``` - * @param value Initial observable value. Or function computing value from other observables. - * @param actions Use to define actions on the observable. Such as add item to the array. - * There is also a reserved function `S.symbol.onclear` which is called when the observable is cleared - * by `S.clear`. - * */ - >(value: V, actions?: A): Observable; - /** - * Computations observable. This creates a observable which is computed from other observables. - * */ - (computation: ()=> V): Observable - action>, A extends (S extends Observable ? A : never), N extends keyof A>( - observable: S, - name: N, - ...params: A[N] extends (...args: infer P)=> any ? P : never - ): void; - clear(...observables: Observable[]): void; - on(observable: Observable, onchange: (a: T)=> void, options?: OnListenerOptions): void; - symbols: { - //observable: SymbolObservable; - onclear: SymbolOnclear; - } - /** - * Reactive element, which is rendered based on the given observable. - * ```js - * S.el(observable, value=> value ? el("b", "True") : el("i", "False")); - * S.el(listS, list=> list.map(li=> el("li", li))); - * ``` - * */ - el(observable: Observable, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment; - - observedAttributes(custom_element: HTMLElement): Record>; -} -export const observable: observable; -export const O: observable; -declare global { - type ddeObservable= Observable; - type ddeAction= Action - type ddeActions= Actions -} diff --git a/observables.js b/observables.js deleted file mode 100644 index a17edaa..0000000 --- a/observables.js +++ /dev/null @@ -1,4 +0,0 @@ -export { observable, O, isObservable } from "./src/observables-lib.js"; -import { observables_config } from "./src/observables-lib.js"; -import { registerReactivity } from "./src/observables-common.js"; -registerReactivity(observables_config); diff --git a/package-lock.json b/package-lock.json index e4d338b..e5f8e38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "deka-dom-el", - "version": "0.7.7", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "deka-dom-el", - "version": "0.7.7", + "version": "0.8.0", "license": "MIT", "devDependencies": { "@size-limit/preset-small-lib": "^11.0.1", diff --git a/package.json b/package.json index 394a7e9..4c9980c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "deka-dom-el", - "version": "0.7.8", + "version": "0.8.0", "description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.", "author": "Jan Andrle ", "license": "MIT", @@ -20,17 +20,17 @@ "import": "./index.js", "types": "./index.d.ts" }, - "./observables": { - "import": "./observables.js", - "types": "./observables.d.ts" + "./signals": { + "import": "./signals.js", + "types": "./signals.d.ts" }, "./jsdom": { "import": "./jsdom.js", "types": "./jsdom.d.ts" }, - "./src/observables-lib": { - "import": "./src/observables-lib.js", - "types": "./src/observables-lib.d.ts" + "./src/signals-lib": { + "import": "./src/signals-lib.js", + "types": "./src/signals-lib.d.ts" } }, "files": [ @@ -65,20 +65,20 @@ }, { - "path": "./observables.js", + "path": "./signals.js", "limit": "12 kB", "gzip": false, "brotli": false }, { - "path": "./index-with-observables.js", + "path": "./index-with-signals.js", "limit": "15 kB", "gzip": false, "brotli": false }, { - "path": "./index-with-observables.js", + "path": "./index-with-signals.js", "limit": "5.25 kB" } ], diff --git a/signals.d.ts b/signals.d.ts new file mode 100644 index 0000000..753ec07 --- /dev/null +++ b/signals.d.ts @@ -0,0 +1,66 @@ +export type Signal= (set?: V)=> V & A; +type Action= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void; +//type SymbolSignal= Symbol; +type SymbolOnclear= symbol; +type Actions= Record>; +type OnListenerOptions= Pick & { first_time?: boolean }; +interface signal{ + _: Symbol + /** + * Simple example: + * ```js + * const hello= S("Hello Signal"); + * ``` + * …simple todo signal: + * ```js + * const todos= S([], { + * add(v){ this.value.push(S(v)); }, + * remove(i){ this.value.splice(i, 1); }, + * [S.symbols.onclear](){ S.clear(...this.value); }, + * }); + * ``` + * …computed signal: + * ```js + * const name= S("Jan"); + * const surname= S("Andrle"); + * const fullname= S(()=> name()+" "+surname()); + * ``` + * @param value Initial signal value. Or function computing value from other signals. + * @param actions Use to define actions on the signal. Such as add item to the array. + * There is also a reserved function `S.symbol.onclear` which is called when the signal is cleared + * by `S.clear`. + * */ + >(value: V, actions?: A): Signal; + /** + * Computations signal. This creates a signal which is computed from other signals. + * */ + (computation: ()=> V): Signal + action>, A extends (S extends Signal ? A : never), N extends keyof A>( + signal: S, + name: N, + ...params: A[N] extends (...args: infer P)=> any ? P : never + ): void; + clear(...signals: Signal[]): void; + on(signal: Signal, onchange: (a: T)=> void, options?: OnListenerOptions): void; + symbols: { + //signal: SymbolSignal; + onclear: SymbolOnclear; + } + /** + * Reactive element, which is rendered based on the given signal. + * ```js + * S.el(signal, value=> value ? el("b", "True") : el("i", "False")); + * S.el(listS, list=> list.map(li=> el("li", li))); + * ``` + * */ + el(signal: Signal, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment; + + observedAttributes(custom_element: HTMLElement): Record>; +} +export const signal: signal; +export const S: signal; +declare global { + type ddeSignal= Signal; + type ddeAction= Action + type ddeActions= Actions +} diff --git a/signals.js b/signals.js new file mode 100644 index 0000000..5e456f4 --- /dev/null +++ b/signals.js @@ -0,0 +1,4 @@ +export { signal, S, isSignal } from "./src/signals-lib.js"; +import { signals_config } from "./src/signals-lib.js"; +import { registerReactivity } from "./src/signals-common.js"; +registerReactivity(signals_config); diff --git a/src/dom.js b/src/dom.js index aef87b8..fc56e1a 100644 --- a/src/dom.js +++ b/src/dom.js @@ -1,4 +1,4 @@ -import { observables } from "./observables-common.js"; +import { signals } from "./signals-common.js"; import { enviroment as env } from './dom-common.js'; /** @type {{ scope: object, prevent: boolean, host: function }[]} */ @@ -31,11 +31,11 @@ export function chainableAppend(el){ if(el.append===append) return el; el.append let namespace; export function createElement(tag, attributes, ...addons){ /* jshint maxcomplexity: 15 */ - const s= observables(this); + const s= signals(this); let scoped= 0; let el, el_host; //TODO Array.isArray(tag) ⇒ set key (cache els) - if(Object(attributes)!==attributes || s.isObservable(attributes)) + if(Object(attributes)!==attributes || s.isSignal(attributes)) attributes= { textContent: attributes }; switch(true){ case typeof tag==="function": { @@ -177,11 +177,11 @@ function assignContext(element, _this){ if(assign_context.has(element)) return assign_context.get(element); const is_svg= element instanceof env.S; const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute"); - const s= observables(_this); + const s= signals(_this); return { setRemoveAttr, s }; } export function classListDeclarative(element, toggle){ - const s= observables(this); + const s= signals(this); forEachEntries(s, toggle, (class_name, val)=> element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val))); diff --git a/src/events.js b/src/events.js index bd07237..8f08d0e 100644 --- a/src/events.js +++ b/src/events.js @@ -1,4 +1,4 @@ -export { registerReactivity } from './observables-common.js'; +export { registerReactivity } from './signals-common.js'; import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js'; export function dispatchEvent(name, options, host){ diff --git a/src/observables-common.js b/src/observables-common.js deleted file mode 100644 index e9b5260..0000000 --- a/src/observables-common.js +++ /dev/null @@ -1,13 +0,0 @@ -export const observables_global= { - isObservable(attributes){ return false; }, - processReactiveAttribute(obj, key, attr, set){ return attr; }, -}; -export function registerReactivity(def, global= true){ - if(global) return Object.assign(observables_global, def); - Object.setPrototypeOf(def, observables_global); - return def; -} -/** @param {unknown} _this @returns {typeof observables_global} */ -export function observables(_this){ - return observables_global.isPrototypeOf(_this) && _this!==observables_global ? _this : observables_global; -} diff --git a/src/observables-lib.d.ts b/src/observables-lib.d.ts deleted file mode 100644 index 5d7c148..0000000 --- a/src/observables-lib.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { Action, Actions, observable as o, Observable, SymbolOnclear } from "../observables.d.ts"; -export { Action, Actions, Observable, SymbolOnclear }; -export const O: o; -export const observable: o; diff --git a/src/observables-lib.js b/src/observables-lib.js index 3ffa831..c86e51a 100644 --- a/src/observables-lib.js +++ b/src/observables-lib.js @@ -1,26 +1,26 @@ -export const mark= "__dde_observable"; +export const mark= "__dde_signal"; import { hasOwn } from "./helpers.js"; -export function isObservable(candidate){ +export function isSignal(candidate){ try{ return hasOwn(candidate, mark); } catch(e){ return false; } } /** @type {function[]} */ const stack_watch= []; /** - * ### `WeakMap>>` - * The `Set` is in the form of `[ source, ...depended observables (DSs) ]`. - * When the DS is cleaned (`O.clear`) it is removed from DSs, + * ### `WeakMap>>` + * The `Set` is in the form of `[ source, ...depended signals (DSs) ]`. + * When the DS is cleaned (`S.clear`) it is removed from DSs, * if remains only one (`source`) it is cleared too. * ### `WeakMap` * This is used for revesed deps, the `function` is also key for `deps`. - * @type {WeakMap>|function>} + * @type {WeakMap>|function>} * */ const deps= new WeakMap(); -export function observable(value, actions){ +export function signal(value, actions){ if(typeof value!=="function") return create(false, value, actions); - if(isObservable(value)) return value; + if(isSignal(value)) return value; const out= create(true); const contextReWatch= function(){ @@ -33,9 +33,9 @@ export function observable(value, actions){ if(!deps_old.length) return; const deps_curr= deps.get(contextReWatch); - for (const dep_observable of deps_old){ - if(deps_curr.has(dep_observable)) continue; - removeObservableListener(dep_observable, contextReWatch); + for (const dep_signal of deps_old){ + if(deps_curr.has(dep_signal)) continue; + removeSignalListener(dep_signal, contextReWatch); } }; deps.set(out[mark], contextReWatch); @@ -43,46 +43,46 @@ export function observable(value, actions){ contextReWatch(); return out; } -export { observable as O }; -observable.action= function(o, name, ...a){ - const s= o[mark], { actions }= s; +export { signal as S }; +signal.action= function(s, name, ...a){ + const M= s[mark], { actions }= M; if(!actions || !(name in actions)) - throw new Error(`'${o}' has no action with name '${name}'!`); - actions[name].apply(s, a); - if(s.skip) return (delete s.skip); - s.listeners.forEach(l=> l(s.value)); + throw new Error(`'${s}' has no action with name '${name}'!`); + actions[name].apply(M, a); + if(M.skip) return (delete M.skip); + M.listeners.forEach(l=> l(M.value)); }; -observable.on= function on(o, listener, options= {}){ +signal.on= function on(s, listener, options= {}){ const { signal: as }= options; if(as && as.aborted) return; - if(Array.isArray(o)) return o.forEach(s=> on(s, listener, options)); - addObservableListener(o, listener); - if(as) as.addEventListener("abort", ()=> removeObservableListener(o, listener)); - //TODO cleanup when observable removed + if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options)); + addSignalListener(s, listener); + if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener)); + //TODO cleanup when signal removed }; -observable.symbols= { - //observable: mark, - onclear: Symbol.for("Observable.onclear") +signal.symbols= { + //signal: mark, + onclear: Symbol.for("Signal.onclear") }; -observable.clear= function(...observables){ - for(const o of observables){ - const s= o[mark]; - if(!s) continue; - delete o.toJSON; - s.onclear.forEach(f=> f.call(s)); - clearListDeps(o, s); - delete o[mark]; +signal.clear= function(...signals){ + for(const s of signals){ + const M= s[mark]; + if(!M) continue; + delete s.toJSON; + M.onclear.forEach(f=> f.call(M)); + clearListDeps(s, M); + delete s[mark]; } - function clearListDeps(o, s){ - s.listeners.forEach(l=> { - s.listeners.delete(l); + function clearListDeps(s, o){ + o.listeners.forEach(l=> { + o.listeners.delete(l); if(!deps.has(l)) return; const ls= deps.get(l); - ls.delete(o); + ls.delete(s); if(ls.size>1) return; - o.clear(...ls); + s.clear(...ls); deps.delete(l); }); } @@ -92,7 +92,7 @@ import { enviroment as env } from "./dom-common.js"; import { el } from "./dom.js"; import { scope } from "./dom.js"; // TODO: third argument for handle `cache_tmp` in re-render -observable.el= function(o, map){ +signal.el= function(s, map){ const mark_start= el.mark({ type: "reactive" }, true); const mark_end= mark_start.end; const out= env.D.createDocumentFragment(); @@ -101,7 +101,7 @@ observable.el= function(o, map){ let cache= {}; const reRenderReactiveElement= v=> { if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasn’t yet rendered - return removeObservableListener(o, reRenderReactiveElement); + return removeSignalListener(s, reRenderReactiveElement); const cache_tmp= cache; // will be reused in the useCache or removed in the while loop on the end cache= {}; scope.push(current); @@ -128,16 +128,16 @@ observable.el= function(o, map){ if(mark_start.isConnected) requestCleanUpReactives(current.host()); }; - addObservableListener(o, reRenderReactiveElement); - removeObservablesFromElements(o, reRenderReactiveElement, mark_start, map); - reRenderReactiveElement(o()); + addSignalListener(s, reRenderReactiveElement); + removeSignalsFromElements(s, reRenderReactiveElement, mark_start, map); + reRenderReactiveElement(s()); return out; }; function requestCleanUpReactives(host){ if(!host || !host[key_reactive]) return; (requestIdleCallback || setTimeout)(function(){ host[key_reactive]= host[key_reactive] - .filter(([ o, el ])=> el.isConnected ? true : (removeObservableListener(...o), false)); + .filter(([ s, el ])=> el.isConnected ? true : (removeSignalListener(...s), false)); }); } import { on } from "./events.js"; @@ -147,49 +147,49 @@ const observedAttributeActions= { }; function observedAttribute(store){ return function(instance, name){ - const varO= (...args)=> !args.length - ? read(varO) + const varS= (...args)=> !args.length + ? read(varS) : instance.setAttribute(name, ...args); - const out= toObservable(varO, instance.getAttribute(name), observedAttributeActions); + const out= toSignal(varS, instance.getAttribute(name), observedAttributeActions); store[name]= out; return out; }; } const key_attributes= "__dde_attributes"; -observable.observedAttributes= function(element){ +signal.observedAttributes= function(element){ const store= element[key_attributes]= {}; const attrs= observedAttributes(element, observedAttribute(store)); - on.attributeChanged(function attributeChangeToObservable({ detail }){ - /*! This maps attributes to observables (`O.observedAttributes`). + on.attributeChanged(function attributeChangeToSignal({ detail }){ + /*! This maps attributes to signals (`S.observedAttributes`). * Investigate `__dde_attributes` key of the element.*/ const [ name, value ]= detail; const curr= this[key_attributes][name]; - if(curr) return observable.action(curr, "_set", value); + if(curr) return signal.action(curr, "_set", value); })(element); on.disconnected(function(){ - /*! This removes all observables mapped to attributes (`O.observedAttributes`). + /*! This removes all signals mapped to attributes (`S.observedAttributes`). * Investigate `__dde_attributes` key of the element.*/ - observable.clear(...Object.values(this[key_attributes])); + signal.clear(...Object.values(this[key_attributes])); })(element); return attrs; }; import { typeOf } from './helpers.js'; -export const observables_config= { - isObservable, +export const signals_config= { + isSignal, processReactiveAttribute(element, key, attrs, set){ - if(!isObservable(attrs)) return attrs; + if(!isSignal(attrs)) return attrs; const l= attr=> { if(!element.isConnected) - return removeObservableListener(attrs, l); + return removeSignalListener(attrs, l); set(key, attr); }; - addObservableListener(attrs, l); - removeObservablesFromElements(attrs, l, element, key); + addSignalListener(attrs, l); + removeSignalsFromElements(attrs, l, element, key); return attrs(); } }; -function removeObservablesFromElements(o, listener, ...notes){ +function removeSignalsFromElements(s, listener, ...notes){ const { current }= scope; if(current.prevent) return; current.host(function(element){ @@ -197,29 +197,29 @@ function removeObservablesFromElements(o, listener, ...notes){ element[key_reactive]= []; on.disconnected(()=> /*! - * Clears all Observables listeners added in the current scope/host (`O.el`, `assign`, …?). + * Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?). * You can investigate the `__dde_reactive` key of the element. * */ - element[key_reactive].forEach(([ [ o, listener ] ])=> - removeObservableListener(o, listener, o[mark] && o[mark].host && o[mark].host() === element)) + element[key_reactive].forEach(([ [ s, listener ] ])=> + removeSignalListener(s, listener, s[mark] && s[mark].host && s[mark].host() === element)) )(element); } - element[key_reactive].push([ [ o, listener ], ...notes ]); + element[key_reactive].push([ [ s, listener ], ...notes ]); }); } function create(is_readonly, value, actions){ - const varO= is_readonly - ? ()=> read(varO) - : (...value)=> value.length ? write(varO, ...value) : read(varO); - return toObservable(varO, value, actions, is_readonly); + const varS= is_readonly + ? ()=> read(varS) + : (...value)=> value.length ? write(varS, ...value) : read(varS); + return toSignal(varS, value, actions, is_readonly); } const protoSigal= Object.assign(Object.create(null), { stopPropagation(){ this.skip= true; } }); -class ObservableDefined extends Error{ +class SignalDefined extends Error{ constructor(){ super(); const [ curr, ...rest ]= this.stack.split("\n"); @@ -227,66 +227,66 @@ class ObservableDefined extends Error{ this.stack= rest.find(l=> !l.includes(curr_file)); } } -function toObservable(o, value, actions, readonly= false){ +function toSignal(s, value, actions, readonly= false){ const onclear= []; if(typeOf(actions)!=="[object Object]") actions= {}; - const { onclear: ocs }= observable.symbols; + const { onclear: ocs }= signal.symbols; if(actions[ocs]){ onclear.push(actions[ocs]); delete actions[ocs]; } const { host }= scope; - Reflect.defineProperty(o, mark, { + Reflect.defineProperty(s, mark, { value: { value, actions, onclear, host, listeners: new Set(), - defined: (new ObservableDefined()).stack, + defined: (new SignalDefined()).stack, readonly }, enumerable: false, writable: false, configurable: true }); - o.toJSON= ()=> o(); - o.valueOf= ()=> o[mark] && o[mark].value; - Object.setPrototypeOf(o[mark], protoSigal); - return o; + s.toJSON= ()=> s(); + s.valueOf= ()=> s[mark] && s[mark].value; + Object.setPrototypeOf(s[mark], protoSigal); + return s; } function currentContext(){ return stack_watch[stack_watch.length - 1]; } -function read(o){ - if(!o[mark]) return; - const { value, listeners }= o[mark]; +function read(s){ + if(!s[mark]) return; + const { value, listeners }= s[mark]; const context= currentContext(); if(context) listeners.add(context); - if(deps.has(context)) deps.get(context).add(o); + if(deps.has(context)) deps.get(context).add(s); return value; } -function write(o, value, force){ - if(!o[mark]) return; - const s= o[mark]; - if(!force && s.value===value) return; - s.value= value; - s.listeners.forEach(l=> l(value)); +function write(s, value, force){ + if(!s[mark]) return; + const M= s[mark]; + if(!force && M.value===value) return; + M.value= value; + M.listeners.forEach(l=> l(value)); return value; } -function addObservableListener(o, listener){ - if(!o[mark]) return; - return o[mark].listeners.add(listener); +function addSignalListener(s, listener){ + if(!s[mark]) return; + return s[mark].listeners.add(listener); } -function removeObservableListener(o, listener, clear_when_empty){ - const s= o[mark]; - if(!s) return; - const out= s.listeners.delete(listener); - if(clear_when_empty && !s.listeners.size){ - observable.clear(o); - if(!deps.has(s)) return out; - const c= deps.get(s); +function removeSignalListener(s, listener, clear_when_empty){ + const M= s[mark]; + if(!M) return; + const out= M.listeners.delete(listener); + if(clear_when_empty && !M.listeners.size){ + signal.clear(s); + if(!deps.has(M)) return out; + const c= deps.get(M); if(!deps.has(c)) return out; - deps.get(c).forEach(sig=> removeObservableListener(sig, c, true)); + deps.get(c).forEach(sig=> removeSignalListener(sig, c, true)); } return out; } diff --git a/src/signals-common.js b/src/signals-common.js new file mode 100644 index 0000000..abe281a --- /dev/null +++ b/src/signals-common.js @@ -0,0 +1,13 @@ +export const signals_global= { + isSignal(attributes){ return false; }, + processReactiveAttribute(obj, key, attr, set){ return attr; }, +}; +export function registerReactivity(def, global= true){ + if(global) return Object.assign(signals_global, def); + Object.setPrototypeOf(def, signals_global); + return def; +} +/** @param {unknown} _this @returns {typeof signals_global} */ +export function signals(_this){ + return signals_global.isPrototypeOf(_this) && _this!==signals_global ? _this : signals_global; +} diff --git a/src/signals-lib.d.ts b/src/signals-lib.d.ts new file mode 100644 index 0000000..9e02e46 --- /dev/null +++ b/src/signals-lib.d.ts @@ -0,0 +1,4 @@ +import type { Action, Actions, signal as s, Signal, SymbolOnclear } from "../signals.d.ts"; +export { Action, Actions, Signal, SymbolOnclear }; +export const S: s; +export const signal: s; diff --git a/src/signals-lib.js b/src/signals-lib.js new file mode 100644 index 0000000..c86e51a --- /dev/null +++ b/src/signals-lib.js @@ -0,0 +1,292 @@ +export const mark= "__dde_signal"; +import { hasOwn } from "./helpers.js"; + +export function isSignal(candidate){ + try{ return hasOwn(candidate, mark); } + catch(e){ return false; } +} +/** @type {function[]} */ +const stack_watch= []; +/** + * ### `WeakMap>>` + * The `Set` is in the form of `[ source, ...depended signals (DSs) ]`. + * When the DS is cleaned (`S.clear`) it is removed from DSs, + * if remains only one (`source`) it is cleared too. + * ### `WeakMap` + * This is used for revesed deps, the `function` is also key for `deps`. + * @type {WeakMap>|function>} + * */ +const deps= new WeakMap(); +export function signal(value, actions){ + if(typeof value!=="function") + return create(false, value, actions); + if(isSignal(value)) return value; + + const out= create(true); + const contextReWatch= function(){ + const [ origin, ...deps_old ]= deps.get(contextReWatch); + deps.set(contextReWatch, new Set([ origin ])); + + stack_watch.push(contextReWatch); + write(out, value()); + stack_watch.pop(); + + if(!deps_old.length) return; + const deps_curr= deps.get(contextReWatch); + for (const dep_signal of deps_old){ + if(deps_curr.has(dep_signal)) continue; + removeSignalListener(dep_signal, contextReWatch); + } + }; + deps.set(out[mark], contextReWatch); + deps.set(contextReWatch, new Set([ out ])); + contextReWatch(); + return out; +} +export { signal as S }; +signal.action= function(s, name, ...a){ + const M= s[mark], { actions }= M; + if(!actions || !(name in actions)) + throw new Error(`'${s}' has no action with name '${name}'!`); + actions[name].apply(M, a); + if(M.skip) return (delete M.skip); + M.listeners.forEach(l=> l(M.value)); +}; +signal.on= function on(s, listener, options= {}){ + const { signal: as }= options; + if(as && as.aborted) return; + if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options)); + addSignalListener(s, listener); + if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener)); + //TODO cleanup when signal removed +}; +signal.symbols= { + //signal: mark, + onclear: Symbol.for("Signal.onclear") +}; +signal.clear= function(...signals){ + for(const s of signals){ + const M= s[mark]; + if(!M) continue; + delete s.toJSON; + M.onclear.forEach(f=> f.call(M)); + clearListDeps(s, M); + delete s[mark]; + } + function clearListDeps(s, o){ + o.listeners.forEach(l=> { + o.listeners.delete(l); + if(!deps.has(l)) return; + + const ls= deps.get(l); + ls.delete(s); + if(ls.size>1) return; + + s.clear(...ls); + deps.delete(l); + }); + } +}; +const key_reactive= "__dde_reactive"; +import { enviroment as env } from "./dom-common.js"; +import { el } from "./dom.js"; +import { scope } from "./dom.js"; +// TODO: third argument for handle `cache_tmp` in re-render +signal.el= function(s, map){ + const mark_start= el.mark({ type: "reactive" }, true); + const mark_end= mark_start.end; + const out= env.D.createDocumentFragment(); + out.append(mark_start, mark_end); + const { current }= scope; + let cache= {}; + const reRenderReactiveElement= v=> { + if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasn’t yet rendered + return removeSignalListener(s, reRenderReactiveElement); + const cache_tmp= cache; // will be reused in the useCache or removed in the while loop on the end + cache= {}; + scope.push(current); + let els= map(v, function useCache(key, fun){ + let value; + if(hasOwn(cache_tmp, key)){ + value= cache_tmp[key]; + delete cache_tmp[key]; + } else + value= fun(); + cache[key]= value; + return value; + }); + scope.pop(); + if(!Array.isArray(els)) + els= [ els ]; + const el_start_rm= document.createComment(""); + els.push(el_start_rm); + mark_start.after(...els); + let el_r; + while(( el_r= el_start_rm.nextSibling ) && el_r !== mark_end) + el_r.remove(); + el_start_rm.remove(); + if(mark_start.isConnected) + requestCleanUpReactives(current.host()); + }; + addSignalListener(s, reRenderReactiveElement); + removeSignalsFromElements(s, reRenderReactiveElement, mark_start, map); + reRenderReactiveElement(s()); + return out; +}; +function requestCleanUpReactives(host){ + if(!host || !host[key_reactive]) return; + (requestIdleCallback || setTimeout)(function(){ + host[key_reactive]= host[key_reactive] + .filter(([ s, el ])=> el.isConnected ? true : (removeSignalListener(...s), false)); + }); +} +import { on } from "./events.js"; +import { observedAttributes } from "./helpers.js"; +const observedAttributeActions= { + _set(value){ this.value= value; }, +}; +function observedAttribute(store){ + return function(instance, name){ + const varS= (...args)=> !args.length + ? read(varS) + : instance.setAttribute(name, ...args); + const out= toSignal(varS, instance.getAttribute(name), observedAttributeActions); + store[name]= out; + return out; + }; +} +const key_attributes= "__dde_attributes"; +signal.observedAttributes= function(element){ + const store= element[key_attributes]= {}; + const attrs= observedAttributes(element, observedAttribute(store)); + on.attributeChanged(function attributeChangeToSignal({ detail }){ + /*! This maps attributes to signals (`S.observedAttributes`). + * Investigate `__dde_attributes` key of the element.*/ + const [ name, value ]= detail; + const curr= this[key_attributes][name]; + if(curr) return signal.action(curr, "_set", value); + })(element); + on.disconnected(function(){ + /*! This removes all signals mapped to attributes (`S.observedAttributes`). + * Investigate `__dde_attributes` key of the element.*/ + signal.clear(...Object.values(this[key_attributes])); + })(element); + return attrs; +}; + +import { typeOf } from './helpers.js'; +export const signals_config= { + isSignal, + processReactiveAttribute(element, key, attrs, set){ + if(!isSignal(attrs)) return attrs; + const l= attr=> { + if(!element.isConnected) + return removeSignalListener(attrs, l); + set(key, attr); + }; + addSignalListener(attrs, l); + removeSignalsFromElements(attrs, l, element, key); + return attrs(); + } +}; +function removeSignalsFromElements(s, listener, ...notes){ + const { current }= scope; + if(current.prevent) return; + current.host(function(element){ + if(!element[key_reactive]){ + element[key_reactive]= []; + on.disconnected(()=> + /*! + * Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?). + * You can investigate the `__dde_reactive` key of the element. + * */ + element[key_reactive].forEach(([ [ s, listener ] ])=> + removeSignalListener(s, listener, s[mark] && s[mark].host && s[mark].host() === element)) + )(element); + } + element[key_reactive].push([ [ s, listener ], ...notes ]); + }); +} + +function create(is_readonly, value, actions){ + const varS= is_readonly + ? ()=> read(varS) + : (...value)=> value.length ? write(varS, ...value) : read(varS); + return toSignal(varS, value, actions, is_readonly); +} +const protoSigal= Object.assign(Object.create(null), { + stopPropagation(){ + this.skip= true; + } +}); +class SignalDefined extends Error{ + constructor(){ + super(); + const [ curr, ...rest ]= this.stack.split("\n"); + const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4); + this.stack= rest.find(l=> !l.includes(curr_file)); + } +} +function toSignal(s, value, actions, readonly= false){ + const onclear= []; + if(typeOf(actions)!=="[object Object]") + actions= {}; + const { onclear: ocs }= signal.symbols; + if(actions[ocs]){ + onclear.push(actions[ocs]); + delete actions[ocs]; + } + const { host }= scope; + Reflect.defineProperty(s, mark, { + value: { + value, actions, onclear, host, + listeners: new Set(), + defined: (new SignalDefined()).stack, + readonly + }, + enumerable: false, + writable: false, + configurable: true + }); + s.toJSON= ()=> s(); + s.valueOf= ()=> s[mark] && s[mark].value; + Object.setPrototypeOf(s[mark], protoSigal); + return s; +} +function currentContext(){ + return stack_watch[stack_watch.length - 1]; +} +function read(s){ + if(!s[mark]) return; + const { value, listeners }= s[mark]; + const context= currentContext(); + if(context) listeners.add(context); + if(deps.has(context)) deps.get(context).add(s); + return value; +} +function write(s, value, force){ + if(!s[mark]) return; + const M= s[mark]; + if(!force && M.value===value) return; + M.value= value; + M.listeners.forEach(l=> l(value)); + return value; +} + +function addSignalListener(s, listener){ + if(!s[mark]) return; + return s[mark].listeners.add(listener); +} +function removeSignalListener(s, listener, clear_when_empty){ + const M= s[mark]; + if(!M) return; + const out= M.listeners.delete(listener); + if(clear_when_empty && !M.listeners.size){ + signal.clear(s); + if(!deps.has(M)) return out; + const c= deps.get(M); + if(!deps.has(c)) return out; + deps.get(c).forEach(sig=> removeSignalListener(sig, c, true)); + } + return out; +}