1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-07-02 04:32:14 +02:00

18 Commits

Author SHA1 Message Date
1b0312f6bd 🔤 2025-03-04 17:52:55 +01:00
508d93bb1a bs/lint 2025-03-04 17:00:14 +01:00
f2ce23d9f7 🔤 2025-03-04 16:46:00 +01:00
b08f75bfb0 🔤 ssr 2025-03-04 16:24:34 +01:00
4edc509646 🔤 adds debugging 2025-03-04 15:42:52 +01:00
56232a9f64 (bs) (un)min 2025-03-04 14:07:00 +01:00
dcf389e28e 🔤 UI enhancements 2025-03-04 13:19:16 +01:00
bdb20ec298 🔤 Docs UI/UX 2025-03-03 19:06:23 +01:00
7ec50e1660 🐛 coumputed signal 2025-03-03 15:25:04 +01:00
6c4ddd655f 🔤 2025-03-03 15:21:43 +01:00
198f4a3777 🔤 2025-03-03 15:20:31 +01:00
3435ea6cfe 🐛 Better types for on* 2025-03-03 15:10:20 +01:00
ed7e6c7963 Refatc signals to .get/.set syntax #26 2025-03-03 14:19:41 +01:00
3168f452ae wip 2025-02-28 19:53:07 +01:00
b53f3926b3 wip 2025-02-28 17:12:40 +01:00
8f2fd5a68c 🔤 2025-02-28 14:32:51 +01:00
f53b97a89c wip 2025-02-28 13:40:56 +01:00
f8a94ab9f8 🎉 2025-02-28 13:05:46 +01:00
73 changed files with 8291 additions and 3425 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist/docs/
dist/.*
# Logs
logs
*.log

View File

@ -18,19 +18,19 @@ function HelloWorldComponent({ initial }){
/** @param {HTMLOptionElement} el */
const isSelected= el=> (el.selected= el.value===initial);
// @ts-expect-error 2339: The <select> has only two options with {@link Emoji}
const onChange= on("change", event=> emoji(event.target.value));
const onChange= on("change", event=> emoji.set(event.target.value));
return el().append(
el("p", {
textContent: S(() => `Hello World ${emoji().repeat(clicks())}`),
textContent: S(() => `Hello World ${emoji.get().repeat(clicks.get())}`),
className: "example",
ariaLive: "polite", //OR ariaset: { live: "polite" },
dataset: { example: "Example" }, //OR dataExample: "Example",
}),
el("button",
{ textContent: "Fire", type: "button" },
on("click", ()=> clicks(clicks() + 1)),
on("keyup", ()=> clicks(clicks() - 2)),
on("click", ()=> clicks.set(clicks.get() + 1)),
on("keyup", ()=> clicks.set(clicks.get() - 2)),
),
el("select", null, onChange).append(
el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" }
@ -86,10 +86,10 @@ To balance these requirements, numerous compromises have been made. To summarize
- [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)
- [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)

View File

@ -1,71 +1,37 @@
#!/usr/bin/env -S npx nodejsscript
import { bundle as bundleDTS } from "dts-bundler";
import { build } from "./dev/.build.js"
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 [ no, full, partial (default) ]")
.action(async function main({ minify= "partial" }){
for(const file_root of files){
const file= file_root+".js";
echo("Processing: "+ file);
const out= filesOut(file);
const esbuild_output= s.$().run([
"npx esbuild '::file::'",
"--platform=neutral",
"--bundle",
minifyOption(minify),
"--legal-comments=inline",
"--packages=external",
"--outfile='::out::'"
].filter(Boolean).join(" "), { file, out });
if(esbuild_output.code)
return $.exit(esbuild_output.code, echo(esbuild_output.stderr));
echoVariant(esbuild_output.stderr.split("\n")[1].trim()+ " (esbuild)");
pipe(
f=> f.replace(/^ +/gm, m=> "\t".repeat(m.length/2)),
f=> s.echo(f).to(out)
)(s.cat(out));
const file_dts= file_root+".d.ts";
const file_dts_out= filesOut(file_dts);
echoVariant(file_dts_out);
s.echo(bundleDTS(file_dts)).to(file_dts_out);
await toDDE(out, file_root);
}
$.exit(0);
async function toDDE(file, file_root){
const name= "dde";
const out= filesOut(file_root+".js", name);
echoVariant(`${out} (${file} → globalThis.${name})`)
let content= s.cat(file).toString().split(/export ?{/);
content.splice(1, 0, `\nglobalThis.${name}= {`);
content[2]= content[2]
.replace(/,(?!\n)/g, ",\n")
.replace(/(?<!\n)}/, "\n}")
.replace(/^(\t*)(.*) as ([^,\n]*)(,?)$/mg, "$1$3: $2$4");
s.echo([
`//deka-dom-el library is available via global namespace \`${name}\``,
"(()=> {",
content.join(""),
"})();"
].join("\n")).to(out);
}
$.api("")
.command("main", "Build main files", { default: true })
.action(async function main(){
const regular = await build({
files,
filesOut,
minify: "no",
});
const min = await build({
files,
filesOut(file, mark= "esm"){
const out= filesOut(file, mark);
const idx= out.lastIndexOf(".");
return out.slice(0, idx)+".min"+out.slice(idx);
},
minify: "full",
});
return $.exit(regular + min);
})
.command("signals", "Build only signals (for example for analysis)")
.action(async function signals(){
const regular = await build({
files: [ "signals" ],
filesOut(file){ return "dist/."+file; },
minify: "no",
dde: false,
});
return $.exit(regular);
})
.parse();
/** @param {"no"|"full"|"partial"} level */
function minifyOption(level= "partial"){
if("no"===level) return undefined;
if("full"===level) return "--minify";
return "--minify-syntax --minify-identifiers";
}
function echoVariant(name){
return echo("%c✓ "+name, css.info+css);
}
function filesOut(file, mark= "esm"){ return "dist/"+file.replace("index", mark); }

65
bs/dev/.build.js Normal file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env -S npx nodejsscript
import { bundle as bundleDTS } from "dts-bundler";
const css= echo.css`
.info{ color: gray; }
`;
export async function build({ files, filesOut, minify= "partiala", dde= true }){
for(const file_root of files){
const file= file_root+".js";
echo(`Processing ${file} (minified: ${minify})`);
const out= filesOut(file);
const esbuild_output= s.$().run([
"npx esbuild '::file::'",
"--platform=neutral",
"--bundle",
minifyOption(minify),
"--legal-comments=inline",
"--packages=external",
"--outfile='::out::'"
].filter(Boolean).join(" "), { file, out });
if(esbuild_output.code)
return $.exit(esbuild_output.code, echo(esbuild_output.stderr));
echoVariant(esbuild_output.stderr.split("\n")[1].trim()+ " (esbuild)");
pipe(
f=> f.replace(/^ +/gm, m=> "\t".repeat(m.length/2)),
f=> s.echo(f).to(out)
)(s.cat(out));
const file_dts= file_root+".d.ts";
const file_dts_out= filesOut(file_dts);
echoVariant(file_dts_out);
s.echo(bundleDTS(file_dts)).to(file_dts_out);
if(dde) await toDDE(out, file_root);
}
return 0;
async function toDDE(file, file_root){
const name= "dde";
const out= filesOut(file_root+".js", name);
echoVariant(`${out} (${file} → globalThis.${name})`)
let content= s.cat(file).toString().split(/export ?{/);
content.splice(1, 0, `\nglobalThis.${name}= {`);
content[2]= content[2]
.replace(/,(?!\n)/g, ",\n")
.replace(/(?<!\n)}/, "\n}")
.replace(/^(\t*)(.*) as ([^,\n]*)(,?)$/mg, "$1$3: $2$4");
s.echo([
`//deka-dom-el library is available via global namespace \`${name}\``,
"(()=> {",
content.join(""),
"})();"
].join("\n")).to(out);
}
}
/** @param {"no"|"full"|"partial"} level */
function minifyOption(level= "partial"){
if("no"===level) return undefined;
if("full"===level) return "--minify";
return "--minify-syntax --minify-identifiers";
}
function echoVariant(name){
return echo("%c✓ "+name, css.info+css);
}

1336
dist/dde-with-signals.js vendored

File diff suppressed because it is too large Load Diff

31
dist/dde-with-signals.min.js vendored Normal file

File diff suppressed because one or more lines are too long

841
dist/dde.js vendored
View File

@ -1,456 +1,663 @@
//deka-dom-el library is available via global namespace `dde`
(()=> {
// src/signals-common.js
var C = {
isSignal(t) {
return !1;
},
processReactiveAttribute(t, e, r, n) {
return r;
}
};
function Z(t, e = !0) {
return e ? Object.assign(C, t) : (Object.setPrototypeOf(t, C), t);
}
function S(t) {
return C.isPrototypeOf(t) && t !== C ? t : C;
}
// src/helpers.js
function m(t) {
return typeof t > "u";
function isUndef(value) {
return typeof value === "undefined";
}
function L(t, e) {
if (!t || !(t instanceof AbortSignal))
return !0;
if (!t.aborted)
return t.addEventListener("abort", e), function() {
t.removeEventListener("abort", e);
function isInstance(obj, cls) {
return obj instanceof cls;
}
function isProtoFrom(obj, cls) {
return Object.prototype.isPrototypeOf.call(cls, obj);
}
function oAssign(...o) {
return Object.assign(...o);
}
function onAbort(signal, listener) {
if (!signal || !isInstance(signal, AbortSignal))
return true;
if (signal.aborted)
return;
signal.addEventListener("abort", listener);
return function cleanUp() {
signal.removeEventListener("abort", listener);
};
}
function q(t, e) {
let { observedAttributes: r = [] } = t.constructor;
return r.reduce(function(n, o) {
return n[G(o)] = e(t, o), n;
function observedAttributes(instance, observedAttribute) {
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
return observedAttributes3.reduce(function(out, name) {
out[kebabToCamel(name)] = observedAttribute(instance, name);
return out;
}, {});
}
function G(t) {
return t.replace(/-./g, (e) => e[1].toUpperCase());
function kebabToCamel(name) {
return name.replace(/-./g, (x) => x[1].toUpperCase());
}
// src/signals-lib/common.js
var signals_global = {
/**
* Checks if a value is a signal
* @param {any} attributes - Value to check
* @returns {boolean} Whether the value is a signal
*/
isSignal(attributes) {
return false;
},
/**
* Processes an attribute that might be reactive
* @param {Element} obj - Element that owns the attribute
* @param {string} key - Attribute name
* @param {any} attr - Attribute value
* @param {Function} set - Function to set the attribute
* @returns {any} Processed attribute value
*/
processReactiveAttribute(obj, key, attr, set) {
return attr;
}
};
function registerReactivity(def, global = true) {
if (global) return oAssign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
function signals(_this) {
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
}
// src/dom-common.js
var a = {
setDeleteAttr: V,
var enviroment = {
setDeleteAttr,
ssr: "",
D: globalThis.document,
F: globalThis.DocumentFragment,
H: globalThis.HTMLElement,
S: globalThis.SVGElement,
M: globalThis.MutationObserver,
q: (t) => t || Promise.resolve()
q: (p) => p || Promise.resolve()
};
function V(t, e, r) {
if (Reflect.set(t, e, r), !!m(r)) {
if (Reflect.deleteProperty(t, e), t instanceof a.H && t.getAttribute(e) === "undefined")
return t.removeAttribute(e);
if (Reflect.get(t, e) === "undefined")
return Reflect.set(t, e, "");
function setDeleteAttr(obj, prop, val) {
Reflect.set(obj, prop, val);
if (!isUndef(val)) return;
Reflect.deleteProperty(obj, prop);
if (isInstance(obj, enviroment.H) && obj.getAttribute(prop) === "undefined")
return obj.removeAttribute(prop);
if (Reflect.get(obj, prop) === "undefined")
return Reflect.set(obj, prop, "");
}
}
var x = "__dde_lifecyclesToEvents", v = "dde:connected", w = "dde:disconnected", y = "dde:attributeChanged";
var keyLTE = "__dde_lifecyclesToEvents";
var evc = "dde:connected";
var evd = "dde:disconnected";
var eva = "dde:attributeChanged";
// src/dom.js
function dt(t) {
return a.q(t);
function queue(promise) {
return enviroment.q(promise);
}
var g = [{
var scopes = [{
get scope() {
return a.D.body;
return enviroment.D.body;
},
host: (t) => t ? t(a.D.body) : a.D.body,
prevent: !0
}], O = {
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true
}];
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
*/
get current() {
return g[g.length - 1];
return scopes[scopes.length - 1];
},
/**
* Gets the host element of the current scope
* @returns {Function} Host accessor function
*/
get host() {
return this.current.host;
},
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
preventDefault() {
let { current: t } = this;
return t.prevent = !0, t;
const { current } = this;
current.prevent = true;
return current;
},
/**
* Gets a copy of the current scope stack
* @returns {Array} Copy of scope stack
*/
get state() {
return [...g];
return [...scopes];
},
push(t = {}) {
return g.push(Object.assign({}, this.current, { prevent: !1 }, t));
/**
* Pushes a new scope to the stack
* @param {Object} [s={}] - Scope object to push
* @returns {number} New length of the scope stack
*/
push(s = {}) {
return scopes.push(oAssign({}, this.current, { prevent: false }, s));
},
/**
* Pushes the root scope to the stack
* @returns {number} New length of the scope stack
*/
pushRoot() {
return g.push(g[0]);
return scopes.push(scopes[0]);
},
/**
* Pops the current scope from the stack
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
*/
pop() {
if (g.length !== 1)
return g.pop();
if (scopes.length === 1) return;
return scopes.pop();
}
};
function k(...t) {
return this.appendOriginal(...t), this;
function append(...els) {
this.appendOriginal(...els);
return this;
}
function J(t) {
return t.append === k || (t.appendOriginal = t.append, t.append = k), t;
function chainableAppend(el) {
if (el.append === append) return el;
el.appendOriginal = el.append;
el.append = append;
return el;
}
var T;
function P(t, e, ...r) {
let n = S(this), o = 0, c, d;
switch ((Object(e) !== e || n.isSignal(e)) && (e = { textContent: e }), !0) {
case typeof t == "function": {
o = 1;
let f = (...l) => l.length ? (o === 1 ? r.unshift(...l) : l.forEach((E) => E(d)), void 0) : d;
O.push({ scope: t, host: f }), c = t(e || void 0);
let p = c instanceof a.F;
if (c.nodeName === "#comment") break;
let b = P.mark({
var namespace;
function createElement(tag, attributes, ...addons) {
const s = signals(this);
let scoped = 0;
let el, el_host;
if (Object(attributes) !== attributes || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host });
el = tag(attributes || void 0);
const is_fragment = isInstance(el, enviroment.F);
if (el.nodeName === "#comment") break;
const el_mark = createElement.mark({
type: "component",
name: t.name,
host: p ? "this" : "parentElement"
name: tag.name,
host: is_fragment ? "this" : "parentElement"
});
c.prepend(b), p && (d = b);
el.prepend(el_mark);
if (is_fragment) el_host = el_mark;
break;
}
case t === "#text":
c = R.call(this, a.D.createTextNode(""), e);
case tag === "#text":
el = assign.call(this, enviroment.D.createTextNode(""), attributes);
break;
case (t === "<>" || !t):
c = R.call(this, a.D.createDocumentFragment(), e);
case (tag === "<>" || !tag):
el = assign.call(this, enviroment.D.createDocumentFragment(), attributes);
break;
case !!T:
c = R.call(this, a.D.createElementNS(T, t), e);
case Boolean(namespace):
el = assign.call(this, enviroment.D.createElementNS(namespace, tag), attributes);
break;
case !c:
c = R.call(this, a.D.createElement(t), e);
case !el:
el = assign.call(this, enviroment.D.createElement(tag), attributes);
}
return J(c), d || (d = c), r.forEach((f) => f(d)), o && O.pop(), o = 2, c;
chainableAppend(el);
if (!el_host) el_host = el;
addons.forEach((c) => c(el_host));
if (scoped) scope.pop();
scoped = 2;
return el;
}
P.mark = function(t, e = !1) {
t = Object.entries(t).map(([o, c]) => o + `="${c}"`).join(" ");
let r = e ? "" : "/", n = a.D.createComment(`<dde:mark ${t}${a.ssr}${r}>`);
return e && (n.end = a.D.createComment("</dde:mark>")), n;
createElement.mark = function(attrs, is_open = false) {
attrs = Object.entries(attrs).map(([n, v]) => n + `="${v}"`).join(" ");
const end = is_open ? "" : "/";
const out = enviroment.D.createComment(`<dde:mark ${attrs}${enviroment.ssr}${end}>`);
if (is_open) out.end = enviroment.D.createComment("</dde:mark>");
return out;
};
function pt(t) {
let e = this;
return function(...n) {
T = t;
let o = P.call(e, ...n);
return T = void 0, o;
function createElementNS(ns) {
const _this = this;
return function createElementNSCurried(...rest) {
namespace = ns;
const el = createElement.call(_this, ...rest);
namespace = void 0;
return el;
};
}
function lt(t, e = t) {
let r = "\xB9\u2070", n = "\u2713", o = Object.fromEntries(
Array.from(e.querySelectorAll("slot")).filter((c) => !c.name.endsWith(r)).map((c) => [c.name += r, c])
function simulateSlots(element, root = element) {
const mark_e = "\xB9\u2070", mark_s = "\u2713";
const slots = Object.fromEntries(
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
);
if (t.append = new Proxy(t.append, {
apply(c, d, f) {
if (f[0] === e) return c.apply(t, f);
for (let p of f) {
let b = (p.slot || "") + r;
element.append = new Proxy(element.append, {
apply(orig, _, els) {
if (els[0] === root) return orig.apply(element, els);
for (const el of els) {
const name = (el.slot || "") + mark_e;
try {
Q(p, "remove", "slot");
} catch {
elementAttribute(el, "remove", "slot");
} catch (_error) {
}
let l = o[b];
if (!l) return;
l.name.startsWith(n) || (l.childNodes.forEach((E) => E.remove()), l.name = n + b), l.append(p);
const slot = slots[name];
if (!slot) return;
if (!slot.name.startsWith(mark_s)) {
slot.childNodes.forEach((c) => c.remove());
slot.name = mark_s + name;
}
return t.append = c, t;
slot.append(el);
}
}), t !== e) {
let c = Array.from(t.childNodes);
t.append(...c);
element.append = orig;
return element;
}
return e;
});
if (element !== root) {
const els = Array.from(element.childNodes);
element.append(...els);
}
var N = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: $ } = a;
function R(t, ...e) {
if (!e.length) return t;
N.set(t, H(t, this));
for (let [r, n] of Object.entries(Object.assign({}, ...e)))
U.call(this, t, r, n);
return N.delete(t), t;
return root;
}
function U(t, e, r) {
let { setRemoveAttr: n, s: o } = H(t, this), c = this;
r = o.processReactiveAttribute(
t,
e,
r,
(f, p) => U.call(c, t, f, p)
var assign_context = /* @__PURE__ */ new WeakMap();
var { setDeleteAttr: setDeleteAttr2 } = enviroment;
function assign(element, ...attributes) {
if (!attributes.length) return element;
assign_context.set(element, assignContext(element, this));
for (const [key, value] of Object.entries(oAssign({}, ...attributes)))
assignAttribute.call(this, element, key, value);
assign_context.delete(element);
return element;
}
function assignAttribute(element, key, value) {
const { setRemoveAttr, s } = assignContext(element, this);
const _this = this;
value = s.processReactiveAttribute(
element,
key,
value,
(key2, value2) => assignAttribute.call(_this, element, key2, value2)
);
let [d] = e;
if (d === "=") return n(e.slice(1), r);
if (d === ".") return F(t, e.slice(1), r);
if (/(aria|data)([A-Z])/.test(e))
return e = e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), n(e, r);
switch (e === "className" && (e = "class"), e) {
const [k] = key;
if ("=" === k) return setRemoveAttr(key.slice(1), value);
if ("." === k) return setDelete(element, key.slice(1), value);
if (/(aria|data)([A-Z])/.test(key)) {
key = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
return setRemoveAttr(key, value);
}
if ("className" === key) key = "class";
switch (key) {
case "xlink:href":
return n(e, r, "http://www.w3.org/1999/xlink");
return setRemoveAttr(key, value, "http://www.w3.org/1999/xlink");
case "textContent":
return $(t, e, r);
return setDeleteAttr2(element, key, value);
case "style":
if (typeof r != "object") break;
if (typeof value !== "object") break;
/* falls through */
case "dataset":
return M(o, e, t, r, F.bind(null, t[e]));
return forEachEntries(s, key, element, value, setDelete.bind(null, element[key]));
case "ariaset":
return M(o, e, t, r, (f, p) => n("aria-" + f, p));
return forEachEntries(s, key, element, value, (key2, val) => setRemoveAttr("aria-" + key2, val));
case "classList":
return K.call(c, t, r);
return classListDeclarative.call(_this, element, value);
}
return X(t, e) ? $(t, e, r) : n(e, r);
return isPropSetter(element, key) ? setDeleteAttr2(element, key, value) : setRemoveAttr(key, value);
}
function H(t, e) {
if (N.has(t)) return N.get(t);
let n = (t instanceof a.S ? tt : Y).bind(null, t, "Attribute"), o = S(e);
return { setRemoveAttr: n, s: o };
function assignContext(element, _this) {
if (assign_context.has(element)) return assign_context.get(element);
const is_svg = isInstance(element, enviroment.S);
const setRemoveAttr = (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
const s = signals(_this);
return { setRemoveAttr, s };
}
function K(t, e) {
let r = S(this);
return M(
r,
function classListDeclarative(element, toggle) {
const s = signals(this);
forEachEntries(
s,
"classList",
t,
e,
(n, o) => t.classList.toggle(n, o === -1 ? void 0 : !!o)
), t;
element,
toggle,
(class_name, val) => element.classList.toggle(class_name, val === -1 ? void 0 : Boolean(val))
);
return element;
}
function Q(t, e, r, n) {
return t instanceof a.H ? t[e + "Attribute"](r, n) : t[e + "AttributeNS"](null, r, n);
function elementAttribute(element, op, key, value) {
if (isInstance(element, enviroment.H))
return element[op + "Attribute"](key, value);
return element[op + "AttributeNS"](null, key, value);
}
function X(t, e) {
if (!(e in t)) return !1;
let r = z(t, e);
return !m(r.set);
function isPropSetter(el, key) {
if (!(key in el)) return false;
const des = getPropDescriptor(el, key);
return !isUndef(des.set);
}
function z(t, e) {
if (t = Object.getPrototypeOf(t), !t) return {};
let r = Object.getOwnPropertyDescriptor(t, e);
return r || z(t, e);
function getPropDescriptor(p, key) {
p = Object.getPrototypeOf(p);
if (!p) return {};
const des = Object.getOwnPropertyDescriptor(p, key);
if (!des) return getPropDescriptor(p, key);
return des;
}
function M(t, e, r, n, o) {
let c = String;
if (!(typeof n != "object" || n === null))
return Object.entries(n).forEach(function([f, p]) {
f && (f = new c(f), f.target = e, p = t.processReactiveAttribute(r, f, p, o), o(f, p));
function forEachEntries(s, target, element, obj, cb) {
const S = String;
if (typeof obj !== "object" || obj === null) return;
return Object.entries(obj).forEach(function process([key, val]) {
if (!key) return;
key = new S(key);
key.target = target;
val = s.processReactiveAttribute(element, key, val, cb);
cb(key, val);
});
}
function Y(t, e, r, n) {
return t[(m(n) ? "remove" : "set") + e](r, n);
function setRemove(obj, prop, key, val) {
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
}
function tt(t, e, r, n, o = null) {
return t[(m(n) ? "remove" : "set") + e + "NS"](o, r, n);
function setRemoveNS(obj, prop, key, val, ns = null) {
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
}
function F(t, e, r) {
if (Reflect.set(t, e, r), !!m(r))
return Reflect.deleteProperty(t, e);
function setDelete(obj, key, val) {
Reflect.set(obj, key, val);
if (!isUndef(val)) return;
return Reflect.deleteProperty(obj, key);
}
// src/events-observer.js
var _ = a.M ? et() : new Proxy({}, {
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function et() {
let t = /* @__PURE__ */ new Map(), e = !1, r = (s) => function(u) {
for (let i of u)
if (i.type === "childList") {
if (l(i.addedNodes, !0)) {
s();
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
E(i.removedNodes, !0) && s();
}
}, n = new a.M(r(f));
return {
observe(s) {
let u = new a.M(r(() => {
}));
return u.observe(s, { childList: !0, subtree: !0 }), () => u.disconnect();
},
onConnected(s, u) {
d();
let i = c(s);
i.connected.has(u) || (i.connected.add(u), i.length_c += 1);
},
offConnected(s, u) {
if (!t.has(s)) return;
let i = t.get(s);
i.connected.has(u) && (i.connected.delete(u), i.length_c -= 1, o(s, i));
},
onDisconnected(s, u) {
d();
let i = c(s);
i.disconnected.has(u) || (i.disconnected.add(u), i.length_d += 1);
},
offDisconnected(s, u) {
if (!t.has(s)) return;
let i = t.get(s);
i.disconnected.has(u) && (i.disconnected.delete(u), i.length_d -= 1, o(s, i));
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
function o(s, u) {
u.length_c || u.length_d || (t.delete(s), f());
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
function c(s) {
if (t.has(s)) return t.get(s);
let u = {
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
return t.set(s, u), u;
store.set(element, out);
return out;
}
function d() {
e || (e = !0, n.observe(a.D.body, { childList: !0, subtree: !0 }));
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function f() {
!e || t.size || (e = !1, n.disconnect());
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function p() {
return new Promise(function(s) {
(requestIdleCallback || requestAnimationFrame)(s);
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function b(s) {
t.size > 30 && await p();
let u = [];
if (!(s instanceof Node)) return u;
for (let i of t.keys())
i === s || !(i instanceof Node) || s.contains(i) && u.push(i);
return u;
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, Node)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, Node)) continue;
if (element.contains(el))
out.push(el);
}
function l(s, u) {
let i = !1;
for (let h of s) {
if (u && b(h).then(l), !t.has(h)) continue;
let A = t.get(h);
A.length_c && (h.dispatchEvent(new Event(v)), A.connected = /* @__PURE__ */ new WeakSet(), A.length_c = 0, A.length_d || t.delete(h), i = !0);
return out;
}
return i;
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
function E(s, u) {
let i = !1;
for (let h of s)
u && b(h).then(E), !(!t.has(h) || !t.get(h).length_d) && ((globalThis.queueMicrotask || setTimeout)(I(h)), i = !0);
return i;
return out;
}
function I(s) {
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
s.isConnected || (s.dispatchEvent(new Event(w)), t.delete(s));
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/customElement.js
function wt(t, e, r = rt) {
let n = t.host || t;
O.push({
scope: n,
host: (...d) => d.length ? d.forEach((f) => f(n)) : n
}), typeof r == "function" && (r = r.call(n, n));
let o = n[x];
o || nt(n);
let c = e.call(n, r);
return o || n.dispatchEvent(new Event(v)), t.nodeType === 11 && typeof t.mode == "string" && n.addEventListener(w, _.observe(t), { once: !0 }), O.pop(), t.append(c);
function customElementRender(target, render, props = observedAttributes2) {
const custom_element = target.host || target;
scope.push({
scope: custom_element,
host: (...c) => c.length ? c.forEach((c2) => c2(custom_element)) : custom_element
});
if (typeof props === "function") props = props.call(custom_element, custom_element);
const is_lte = custom_element[keyLTE];
if (!is_lte) lifecyclesToEvents(custom_element);
const out = render.call(custom_element, props);
if (!is_lte) custom_element.dispatchEvent(new Event(evc));
if (target.nodeType === 11 && typeof target.mode === "string")
custom_element.addEventListener(evd, c_ch_o.observe(target), { once: true });
scope.pop();
return target.append(out);
}
function nt(t) {
return j(t.prototype, "connectedCallback", function(e, r, n) {
e.apply(r, n), r.dispatchEvent(new Event(v));
}), j(t.prototype, "disconnectedCallback", function(e, r, n) {
e.apply(r, n), (globalThis.queueMicrotask || setTimeout)(
() => !r.isConnected && r.dispatchEvent(new Event(w))
function lifecyclesToEvents(class_declaration) {
wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail) {
target.apply(thisArg, detail);
thisArg.dispatchEvent(new Event(evc));
});
wrapMethod(class_declaration.prototype, "disconnectedCallback", function(target, thisArg, detail) {
target.apply(thisArg, detail);
(globalThis.queueMicrotask || setTimeout)(
() => !thisArg.isConnected && thisArg.dispatchEvent(new Event(evd))
);
}), j(t.prototype, "attributeChangedCallback", function(e, r, n) {
let [o, , c] = n;
r.dispatchEvent(new CustomEvent(y, {
detail: [o, c]
})), e.apply(r, n);
}), t.prototype[x] = !0, t;
});
wrapMethod(class_declaration.prototype, "attributeChangedCallback", function(target, thisArg, detail) {
const [attribute, , value] = detail;
thisArg.dispatchEvent(new CustomEvent(eva, {
detail: [attribute, value]
}));
target.apply(thisArg, detail);
});
class_declaration.prototype[keyLTE] = true;
return class_declaration;
}
function j(t, e, r) {
t[e] = new Proxy(t[e] || (() => {
}), { apply: r });
function wrapMethod(obj, method, apply) {
obj[method] = new Proxy(obj[method] || (() => {
}), { apply });
}
function rt(t) {
return q(t, (e, r) => e.getAttribute(r));
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js
function yt(t, e, r) {
return e || (e = {}), function(o, ...c) {
r && (c.unshift(o), o = typeof r == "function" ? r() : r);
let d = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e);
return o.dispatchEvent(d);
function dispatchEvent(name, options, host) {
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
}
function D(t, e, r) {
return function(o) {
return o.addEventListener(t, e, r), o;
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var B = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 });
D.connected = function(t, e) {
return e = B(e), function(n) {
return n.addEventListener(v, t, e), n[x] ? n : n.isConnected ? (n.dispatchEvent(new Event(v)), n) : (L(e.signal, () => _.offConnected(n, t)) && _.onConnected(n, t), n);
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
};
D.disconnected = function(t, e) {
return e = B(e), function(n) {
return n.addEventListener(w, t, e), n[x] || L(e.signal, () => _.offDisconnected(n, t)) && _.onDisconnected(n, t), n;
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
var W = /* @__PURE__ */ new WeakMap();
D.disconnectedAsAbort = function(t) {
if (W.has(t)) return W.get(t);
let e = new AbortController();
return W.set(t, e), t(D.disconnected(() => e.abort())), e;
var store_abort = /* @__PURE__ */ new WeakMap();
on.disconnectedAsAbort = function(host) {
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a;
};
var ot = /* @__PURE__ */ new WeakSet();
D.attributeChanged = function(t, e) {
return typeof e != "object" && (e = {}), function(n) {
if (n.addEventListener(y, t, e), n[x] || ot.has(n) || !a.M) return n;
let o = new a.M(function(d) {
for (let { attributeName: f, target: p } of d)
p.dispatchEvent(
new CustomEvent(y, { detail: [f, p.getAttribute(f)] })
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {
if (typeof options !== "object")
options = {};
return function registerElement(element) {
element.addEventListener(eva, listener, options);
if (element[keyLTE] || els_attribute_store.has(element))
return element;
if (!enviroment.M) return element;
const observer = new enviroment.M(function(mutations) {
for (const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
);
});
return L(e.signal, () => o.disconnect()) && o.observe(n, { attributes: !0 }), n;
const c = onAbort(options.signal, () => observer.disconnect());
if (c) observer.observe(element, { attributes: true });
return element;
};
};
globalThis.dde= {
assign: R,
assignAttribute: U,
chainableAppend: J,
classListDeclarative: K,
createElement: P,
createElementNS: pt,
customElementRender: wt,
customElementWithDDE: nt,
dispatchEvent: yt,
el: P,
elNS: pt,
elementAttribute: Q,
lifecyclesToEvents: nt,
observedAttributes: rt,
on: D,
queue: dt,
registerReactivity: Z,
scope: O,
simulateSlots: lt
assign,
assignAttribute,
chainableAppend,
classListDeclarative,
createElement,
createElementNS,
customElementRender,
customElementWithDDE: lifecyclesToEvents,
dispatchEvent,
el: createElement,
elNS: createElementNS,
elementAttribute,
lifecyclesToEvents,
observedAttributes: observedAttributes2,
on,
queue,
registerReactivity,
scope,
simulateSlots
};
})();

25
dist/dde.min.js vendored Normal file

File diff suppressed because one or more lines are too long

593
dist/esm-with-signals.d.min.ts vendored Normal file
View File

@ -0,0 +1,593 @@
declare global{ /* ddeSignal */ }
type CustomElementTagNameMap= { '#text': Text, '#comment': Comment }
type SupportedElement=
HTMLElementTagNameMap[keyof HTMLElementTagNameMap]
| SVGElementTagNameMap[keyof SVGElementTagNameMap]
| MathMLElementTagNameMap[keyof MathMLElementTagNameMap]
| CustomElementTagNameMap[keyof CustomElementTagNameMap]
declare global {
type ddeComponentAttributes= Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node>= (element: El)=> any;
type ddeString= string | ddeSignal<string>
type ddeStringable= ddeString | number | ddeSignal<number>
}
type PascalCase=
`${Capitalize<string>}${string}`;
type AttrsModified= {
/**
* Use string like in HTML (internally uses `*.setAttribute("style", *)`), or object representation (like DOM API).
*/
style: Partial<CSSStyleDeclaration> | ddeString
| Partial<{ [K in keyof CSSStyleDeclaration]: ddeSignal<CSSStyleDeclaration[K]> }>
/**
* 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<string,-1|0|1|boolean|ddeSignal<-1|0|1|boolean>>,
/**
* Used by the dataset HTML attribute to represent data for custom attributes added to elements.
* Values are converted to string (see {@link DOMStringMap}).
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMStringMap)
* */
dataset: Record<string, ddeStringable>,
/**
* Sets `aria-*` simiraly to `dataset`
* */
ariaset: Record<string, ddeString>,
} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, ddeString>
& Record<`.${string}`, any>
type _fromElsInterfaces<EL extends SupportedElement>= Omit<EL, keyof AttrsModified>;
type IsReadonly<T, K extends keyof T> =
T extends { readonly [P in K]: T[K] } ? true : false;
/**
* Just element attributtes
*
* In most cases, you can use native propertie such as
* [MDN WEB/API/Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) and so on
* (e.g. [`Text`](https://developer.mozilla.org/en-US/docs/Web/API/Text)).
*
* There is added support for `data[A-Z].*`/`aria[A-Z].*` to be converted to the kebab-case alternatives.
* @private
*/
type ElementAttributes<T extends SupportedElement>= Partial<{
[K in keyof _fromElsInterfaces<T>]:
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
: ddeStringable)
} & AttrsModified> & Record<string, any>;
export function classListDeclarative<El extends SupportedElement>(
element: El,
classList: AttrsModified["classList"]
): El
export function assign<El extends SupportedElement>(element: El, ...attrs_array: ElementAttributes<El>[]): El
export function assignAttribute<El extends SupportedElement, ATT extends keyof ElementAttributes<El>>(
element: El,
attr: ATT,
value: ElementAttributes<El>[ATT]
): ElementAttributes<El>[ATT]
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap;
export function el<
TAG extends keyof ExtendedHTMLElementTagNameMap,
>(
tag_name: TAG,
attrs?: ElementAttributes<ExtendedHTMLElementTagNameMap[NoInfer<TAG>]> | ddeStringable,
...addons: ddeElementAddon<
ExtendedHTMLElementTagNameMap[NoInfer<TAG>]
>[], // TODO: for now addons must have the same element
): TAG extends keyof ddeHTMLElementTagNameMap ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement
export function el(
tag_name?: "<>",
): ddeDocumentFragment
export function el(
tag_name: string,
attrs?: ElementAttributes<HTMLElement> | ddeStringable,
...addons: ddeElementAddon<HTMLElement>[]
): ddeHTMLElement
export function el<
C extends (attr: ddeComponentAttributes)=> SupportedElement | ddeDocumentFragment
>(
component: C,
attrs?: Parameters<C>[0] | ddeStringable,
...addons: ddeElementAddon<ReturnType<C>>[]
): ReturnType<C> extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
? ReturnType<C>
: ( ReturnType<C> extends ddeDocumentFragment ? ReturnType<C> : ddeHTMLElement )
export { el as createElement }
export function elNS(
namespace: "http://www.w3.org/2000/svg"
): <
TAG extends keyof SVGElementTagNameMap & string,
EL extends ( TAG extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[TAG] : SVGElement ),
>(
tag_name: TAG,
attrs?: ElementAttributes<NoInfer<EL>> | ddeStringable,
...addons: ddeElementAddon<NoInfer<EL>>[]
)=> TAG extends keyof ddeSVGElementTagNameMap ? ddeSVGElementTagNameMap[TAG] : ddeSVGElement
export function elNS(
namespace: "http://www.w3.org/1998/Math/MathML"
): <
TAG extends keyof MathMLElementTagNameMap & string,
EL extends ( TAG extends keyof MathMLElementTagNameMap ? MathMLElementTagNameMap[TAG] : MathMLElement ),
>(
tag_name: TAG,
attrs?: ddeStringable | Partial<{
[key in keyof EL]: EL[key] | ddeSignal<EL[key]> | string | number | boolean
}>,
...addons: ddeElementAddon<NoInfer<EL>>[]
)=> ddeMathMLElement
export function elNS(
namespace: string
): (
tag_name: string,
attrs?: string | ddeStringable | Record<string, any>,
...addons: ddeElementAddon<SupportedElement>[]
)=> SupportedElement
export { elNS as createElementNS }
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
root: EL,
): EL
/**
* Simulate slots in Custom Elements without using `shadowRoot`.
* @param el Custom Element root element
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
el: HTMLElement,
body: EL,
): EL
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
(element: SupportedElement, data?: any)=> void;
export function dispatchEvent(
name: keyof DocumentEventMap | string,
options: EventInit | null,
element: SupportedElement | (()=> SupportedElement)
): (data?: any)=> void;
interface On{
/** Listens to the DOM event. See {@link Document.addEventListener} */
<
Event extends keyof DocumentEventMap,
EE extends ddeElementAddon<SupportedElement>= ddeElementAddon<HTMLElement>,
>(
type: Event,
listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any,
options?: AddEventListenerOptions
) : EE;
<
EE extends ddeElementAddon<SupportedElement>= ddeElementAddon<HTMLElement>,
>(
type: string,
listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent ) => any,
options?: AddEventListenerOptions
) : EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
connected<
EE extends ddeElementAddon<SupportedElement>,
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
>(
listener: (this: El, event: CustomEvent<El>) => any,
options?: AddEventListenerOptions
) : EE;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
disconnected<
EE extends ddeElementAddon<SupportedElement>,
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
>(
listener: (this: El, event: CustomEvent<void>) => any,
options?: AddEventListenerOptions
) : EE;
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
attributeChanged<
EE extends ddeElementAddon<SupportedElement>,
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
>(
listener: (this: El, event: CustomEvent<[ string, string ]>) => any,
options?: AddEventListenerOptions
) : EE;
}
export const on: On;
type Scope= {
scope: Node | Function | Object,
host: ddeElementAddon<any>,
custom_element: false | HTMLElement,
prevent: boolean
};
/** Current scope created last time the `el(Function)` was invoke. (Or {@link scope.push}) */
export const scope: {
current: Scope,
/** Stops all automatizations. E. g. signals used as attributes in current scope
* registers removing these listeners (and clean signal if no other listeners are detected)
* on `disconnected` event. */
preventDefault<T extends boolean>(prevent: T): T,
/**
* This represents reference to the current host element — `scope.host()`.
* It can be also used to register Addon(s) (functions to be called when component is initized)
* — `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[])=> HTMLElement,
state: Scope[],
/** Adds new child scope. All attributes are inherited by default. */
push(scope: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
/** Adds root scope as a child of the current scope. */
pushRoot(): ReturnType<Array<Scope>["push"]>,
/** Removes last/current child scope. */
pop(): ReturnType<Array<Scope>["pop"]>,
};
export function customElementRender<
EL extends HTMLElement,
P extends any = Record<string, string | ddeSignal<string>>
>(
target: ShadowRoot | EL,
render: (props: P)=> SupportedElement | DocumentFragment,
props?: P | ((el: EL)=> P)
): EL
export function customElementWithDDE<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
export function lifecyclesToEvents<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
export function observedAttributes(custom_element: HTMLElement): Record<string, string>
/* TypeScript MEH */
declare global{
type ddeAppend<el>= (...nodes: (Node | string)[])=> el;
interface ddeDocumentFragment extends DocumentFragment{ append: ddeAppend<ddeDocumentFragment>; }
interface ddeHTMLElement extends HTMLElement{ append: ddeAppend<ddeHTMLElement>; }
interface ddeSVGElement extends SVGElement{ append: ddeAppend<ddeSVGElement>; }
interface ddeMathMLElement extends MathMLElement{ append: ddeAppend<ddeMathMLElement>; }
interface ddeHTMLElementTagNameMap {
"a": ddeHTMLAnchorElement;
"area": ddeHTMLAreaElement;
"audio": ddeHTMLAudioElement;
"base": ddeHTMLBaseElement;
"blockquote": ddeHTMLQuoteElement;
"body": ddeHTMLBodyElement;
"br": ddeHTMLBRElement;
"button": ddeHTMLButtonElement;
"canvas": ddeHTMLCanvasElement;
"caption": ddeHTMLTableCaptionElement;
"col": ddeHTMLTableColElement;
"colgroup": ddeHTMLTableColElement;
"data": ddeHTMLDataElement;
"datalist": ddeHTMLDataListElement;
"del": ddeHTMLModElement;
"details": ddeHTMLDetailsElement;
"dialog": ddeHTMLDialogElement;
"div": ddeHTMLDivElement;
"dl": ddeHTMLDListElement;
"embed": ddeHTMLEmbedElement;
"fieldset": ddeHTMLFieldSetElement;
"form": ddeHTMLFormElement;
"h1": ddeHTMLHeadingElement;
"h2": ddeHTMLHeadingElement;
"h3": ddeHTMLHeadingElement;
"h4": ddeHTMLHeadingElement;
"h5": ddeHTMLHeadingElement;
"h6": ddeHTMLHeadingElement;
"head": ddeHTMLHeadElement;
"hr": ddeHTMLHRElement;
"html": ddeHTMLHtmlElement;
"iframe": ddeHTMLIFrameElement;
"img": ddeHTMLImageElement;
"input": ddeHTMLInputElement;
"ins": ddeHTMLModElement;
"label": ddeHTMLLabelElement;
"legend": ddeHTMLLegendElement;
"li": ddeHTMLLIElement;
"link": ddeHTMLLinkElement;
"map": ddeHTMLMapElement;
"menu": ddeHTMLMenuElement;
"meta": ddeHTMLMetaElement;
"meter": ddeHTMLMeterElement;
"object": ddeHTMLObjectElement;
"ol": ddeHTMLOListElement;
"optgroup": ddeHTMLOptGroupElement;
"option": ddeHTMLOptionElement;
"output": ddeHTMLOutputElement;
"p": ddeHTMLParagraphElement;
"picture": ddeHTMLPictureElement;
"pre": ddeHTMLPreElement;
"progress": ddeHTMLProgressElement;
"q": ddeHTMLQuoteElement;
"script": ddeHTMLScriptElement;
"select": ddeHTMLSelectElement;
"slot": ddeHTMLSlotElement;
"source": ddeHTMLSourceElement;
"span": ddeHTMLSpanElement;
"style": ddeHTMLStyleElement;
"table": ddeHTMLTableElement;
"tbody": ddeHTMLTableSectionElement;
"td": ddeHTMLTableCellElement;
"template": ddeHTMLTemplateElement;
"textarea": ddeHTMLTextAreaElement;
"tfoot": ddeHTMLTableSectionElement;
"th": ddeHTMLTableCellElement;
"thead": ddeHTMLTableSectionElement;
"time": ddeHTMLTimeElement;
"title": ddeHTMLTitleElement;
"tr": ddeHTMLTableRowElement;
"track": ddeHTMLTrackElement;
"ul": ddeHTMLUListElement;
"video": ddeHTMLVideoElement;
}
interface ddeSVGElementTagNameMap {
"a": ddeSVGAElement;
"animate": ddeSVGAnimateElement;
"animateMotion": ddeSVGAnimateMotionElement;
"animateTransform": ddeSVGAnimateTransformElement;
"circle": ddeSVGCircleElement;
"clipPath": ddeSVGClipPathElement;
"defs": ddeSVGDefsElement;
"desc": ddeSVGDescElement;
"ellipse": ddeSVGEllipseElement;
"feBlend": ddeSVGFEBlendElement;
"feColorMatrix": ddeSVGFEColorMatrixElement;
"feComponentTransfer": ddeSVGFEComponentTransferElement;
"feComposite": ddeSVGFECompositeElement;
"feConvolveMatrix": ddeSVGFEConvolveMatrixElement;
"feDiffuseLighting": ddeSVGFEDiffuseLightingElement;
"feDisplacementMap": ddeSVGFEDisplacementMapElement;
"feDistantLight": ddeSVGFEDistantLightElement;
"feDropShadow": ddeSVGFEDropShadowElement;
"feFlood": ddeSVGFEFloodElement;
"feFuncA": ddeSVGFEFuncAElement;
"feFuncB": ddeSVGFEFuncBElement;
"feFuncG": ddeSVGFEFuncGElement;
"feFuncR": ddeSVGFEFuncRElement;
"feGaussianBlur": ddeSVGFEGaussianBlurElement;
"feImage": ddeSVGFEImageElement;
"feMerge": ddeSVGFEMergeElement;
"feMergeNode": ddeSVGFEMergeNodeElement;
"feMorphology": ddeSVGFEMorphologyElement;
"feOffset": ddeSVGFEOffsetElement;
"fePointLight": ddeSVGFEPointLightElement;
"feSpecularLighting": ddeSVGFESpecularLightingElement;
"feSpotLight": ddeSVGFESpotLightElement;
"feTile": ddeSVGFETileElement;
"feTurbulence": ddeSVGFETurbulenceElement;
"filter": ddeSVGFilterElement;
"foreignObject": ddeSVGForeignObjectElement;
"g": ddeSVGGElement;
"image": ddeSVGImageElement;
"line": ddeSVGLineElement;
"linearGradient": ddeSVGLinearGradientElement;
"marker": ddeSVGMarkerElement;
"mask": ddeSVGMaskElement;
"metadata": ddeSVGMetadataElement;
"mpath": ddeSVGMPathElement;
"path": ddeSVGPathElement;
"pattern": ddeSVGPatternElement;
"polygon": ddeSVGPolygonElement;
"polyline": ddeSVGPolylineElement;
"radialGradient": ddeSVGRadialGradientElement;
"rect": ddeSVGRectElement;
"script": ddeSVGScriptElement;
"set": ddeSVGSetElement;
"stop": ddeSVGStopElement;
"style": ddeSVGStyleElement;
"svg": ddeSVGSVGElement;
"switch": ddeSVGSwitchElement;
"symbol": ddeSVGSymbolElement;
"text": ddeSVGTextElement;
"textPath": ddeSVGTextPathElement;
"title": ddeSVGTitleElement;
"tspan": ddeSVGTSpanElement;
"use": ddeSVGUseElement;
"view": ddeSVGViewElement;
}
}
// editorconfig-checker-disable
interface ddeHTMLAnchorElement extends HTMLAnchorElement{ append: ddeAppend<ddeHTMLAnchorElement>; }
interface ddeHTMLAreaElement extends HTMLAreaElement{ append: ddeAppend<ddeHTMLAreaElement>; }
interface ddeHTMLAudioElement extends HTMLAudioElement{ append: ddeAppend<ddeHTMLAudioElement>; }
interface ddeHTMLBaseElement extends HTMLBaseElement{ append: ddeAppend<ddeHTMLBaseElement>; }
interface ddeHTMLQuoteElement extends HTMLQuoteElement{ append: ddeAppend<ddeHTMLQuoteElement>; }
interface ddeHTMLBodyElement extends HTMLBodyElement{ append: ddeAppend<ddeHTMLBodyElement>; }
interface ddeHTMLBRElement extends HTMLBRElement{ append: ddeAppend<ddeHTMLBRElement>; }
interface ddeHTMLButtonElement extends HTMLButtonElement{ append: ddeAppend<ddeHTMLButtonElement>; }
interface ddeHTMLCanvasElement extends HTMLCanvasElement{ append: ddeAppend<ddeHTMLCanvasElement>; }
interface ddeHTMLTableCaptionElement extends HTMLTableCaptionElement{ append: ddeAppend<ddeHTMLTableCaptionElement>; }
interface ddeHTMLTableColElement extends HTMLTableColElement{ append: ddeAppend<ddeHTMLTableColElement>; }
interface ddeHTMLTableColElement extends HTMLTableColElement{ append: ddeAppend<ddeHTMLTableColElement>; }
interface ddeHTMLDataElement extends HTMLDataElement{ append: ddeAppend<ddeHTMLDataElement>; }
interface ddeHTMLDataListElement extends HTMLDataListElement{ append: ddeAppend<ddeHTMLDataListElement>; }
interface ddeHTMLModElement extends HTMLModElement{ append: ddeAppend<ddeHTMLModElement>; }
interface ddeHTMLDetailsElement extends HTMLDetailsElement{ append: ddeAppend<ddeHTMLDetailsElement>; }
interface ddeHTMLDialogElement extends HTMLDialogElement{ append: ddeAppend<ddeHTMLDialogElement>; }
interface ddeHTMLDivElement extends HTMLDivElement{ append: ddeAppend<ddeHTMLDivElement>; }
interface ddeHTMLDListElement extends HTMLDListElement{ append: ddeAppend<ddeHTMLDListElement>; }
interface ddeHTMLEmbedElement extends HTMLEmbedElement{ append: ddeAppend<ddeHTMLEmbedElement>; }
interface ddeHTMLFieldSetElement extends HTMLFieldSetElement{ append: ddeAppend<ddeHTMLFieldSetElement>; }
interface ddeHTMLFormElement extends HTMLFormElement{ append: ddeAppend<ddeHTMLFormElement>; }
interface ddeHTMLHeadingElement extends HTMLHeadingElement{ append: ddeAppend<ddeHTMLHeadingElement>; }
interface ddeHTMLHeadElement extends HTMLHeadElement{ append: ddeAppend<ddeHTMLHeadElement>; }
interface ddeHTMLHRElement extends HTMLHRElement{ append: ddeAppend<ddeHTMLHRElement>; }
interface ddeHTMLHtmlElement extends HTMLHtmlElement{ append: ddeAppend<ddeHTMLHtmlElement>; }
interface ddeHTMLIFrameElement extends HTMLIFrameElement{ append: ddeAppend<ddeHTMLIFrameElement>; }
interface ddeHTMLImageElement extends HTMLImageElement{ append: ddeAppend<ddeHTMLImageElement>; }
interface ddeHTMLInputElement extends HTMLInputElement{ append: ddeAppend<ddeHTMLInputElement>; }
interface ddeHTMLLabelElement extends HTMLLabelElement{ append: ddeAppend<ddeHTMLLabelElement>; }
interface ddeHTMLLegendElement extends HTMLLegendElement{ append: ddeAppend<ddeHTMLLegendElement>; }
interface ddeHTMLLIElement extends HTMLLIElement{ append: ddeAppend<ddeHTMLLIElement>; }
interface ddeHTMLLinkElement extends HTMLLinkElement{ append: ddeAppend<ddeHTMLLinkElement>; }
interface ddeHTMLMapElement extends HTMLMapElement{ append: ddeAppend<ddeHTMLMapElement>; }
interface ddeHTMLMenuElement extends HTMLMenuElement{ append: ddeAppend<ddeHTMLMenuElement>; }
interface ddeHTMLMetaElement extends HTMLMetaElement{ append: ddeAppend<ddeHTMLMetaElement>; }
interface ddeHTMLMeterElement extends HTMLMeterElement{ append: ddeAppend<ddeHTMLMeterElement>; }
interface ddeHTMLObjectElement extends HTMLObjectElement{ append: ddeAppend<ddeHTMLObjectElement>; }
interface ddeHTMLOListElement extends HTMLOListElement{ append: ddeAppend<ddeHTMLOListElement>; }
interface ddeHTMLOptGroupElement extends HTMLOptGroupElement{ append: ddeAppend<ddeHTMLOptGroupElement>; }
interface ddeHTMLOptionElement extends HTMLOptionElement{ append: ddeAppend<ddeHTMLOptionElement>; }
interface ddeHTMLOutputElement extends HTMLOutputElement{ append: ddeAppend<ddeHTMLOutputElement>; }
interface ddeHTMLParagraphElement extends HTMLParagraphElement{ append: ddeAppend<ddeHTMLParagraphElement>; }
interface ddeHTMLPictureElement extends HTMLPictureElement{ append: ddeAppend<ddeHTMLPictureElement>; }
interface ddeHTMLPreElement extends HTMLPreElement{ append: ddeAppend<ddeHTMLPreElement>; }
interface ddeHTMLProgressElement extends HTMLProgressElement{ append: ddeAppend<ddeHTMLProgressElement>; }
interface ddeHTMLScriptElement extends HTMLScriptElement{ append: ddeAppend<ddeHTMLScriptElement>; }
interface ddeHTMLSelectElement extends HTMLSelectElement{ append: ddeAppend<ddeHTMLSelectElement>; }
interface ddeHTMLSlotElement extends HTMLSlotElement{ append: ddeAppend<ddeHTMLSlotElement>; }
interface ddeHTMLSourceElement extends HTMLSourceElement{ append: ddeAppend<ddeHTMLSourceElement>; }
interface ddeHTMLSpanElement extends HTMLSpanElement{ append: ddeAppend<ddeHTMLSpanElement>; }
interface ddeHTMLStyleElement extends HTMLStyleElement{ append: ddeAppend<ddeHTMLStyleElement>; }
interface ddeHTMLTableElement extends HTMLTableElement{ append: ddeAppend<ddeHTMLTableElement>; }
interface ddeHTMLTableSectionElement extends HTMLTableSectionElement{ append: ddeAppend<ddeHTMLTableSectionElement>; }
interface ddeHTMLTableCellElement extends HTMLTableCellElement{ append: ddeAppend<ddeHTMLTableCellElement>; }
interface ddeHTMLTemplateElement extends HTMLTemplateElement{ append: ddeAppend<ddeHTMLTemplateElement>; }
interface ddeHTMLTextAreaElement extends HTMLTextAreaElement{ append: ddeAppend<ddeHTMLTextAreaElement>; }
interface ddeHTMLTableCellElement extends HTMLTableCellElement{ append: ddeAppend<ddeHTMLTableCellElement>; }
interface ddeHTMLTimeElement extends HTMLTimeElement{ append: ddeAppend<ddeHTMLTimeElement>; }
interface ddeHTMLTitleElement extends HTMLTitleElement{ append: ddeAppend<ddeHTMLTitleElement>; }
interface ddeHTMLTableRowElement extends HTMLTableRowElement{ append: ddeAppend<ddeHTMLTableRowElement>; }
interface ddeHTMLTrackElement extends HTMLTrackElement{ append: ddeAppend<ddeHTMLTrackElement>; }
interface ddeHTMLUListElement extends HTMLUListElement{ append: ddeAppend<ddeHTMLUListElement>; }
interface ddeHTMLVideoElement extends HTMLVideoElement{ append: ddeAppend<ddeHTMLVideoElement>; }
interface ddeSVGAElement extends SVGAElement{ append: ddeAppend<ddeSVGAElement>; }
interface ddeSVGAnimateElement extends SVGAnimateElement{ append: ddeAppend<ddeSVGAnimateElement>; }
interface ddeSVGAnimateMotionElement extends SVGAnimateMotionElement{ append: ddeAppend<ddeSVGAnimateMotionElement>; }
interface ddeSVGAnimateTransformElement extends SVGAnimateTransformElement{ append: ddeAppend<ddeSVGAnimateTransformElement>; }
interface ddeSVGCircleElement extends SVGCircleElement{ append: ddeAppend<ddeSVGCircleElement>; }
interface ddeSVGClipPathElement extends SVGClipPathElement{ append: ddeAppend<ddeSVGClipPathElement>; }
interface ddeSVGDefsElement extends SVGDefsElement{ append: ddeAppend<ddeSVGDefsElement>; }
interface ddeSVGDescElement extends SVGDescElement{ append: ddeAppend<ddeSVGDescElement>; }
interface ddeSVGEllipseElement extends SVGEllipseElement{ append: ddeAppend<ddeSVGEllipseElement>; }
interface ddeSVGFEBlendElement extends SVGFEBlendElement{ append: ddeAppend<ddeSVGFEBlendElement>; }
interface ddeSVGFEColorMatrixElement extends SVGFEColorMatrixElement{ append: ddeAppend<ddeSVGFEColorMatrixElement>; }
interface ddeSVGFEComponentTransferElement extends SVGFEComponentTransferElement{ append: ddeAppend<ddeSVGFEComponentTransferElement>; }
interface ddeSVGFECompositeElement extends SVGFECompositeElement{ append: ddeAppend<ddeSVGFECompositeElement>; }
interface ddeSVGFEConvolveMatrixElement extends SVGFEConvolveMatrixElement{ append: ddeAppend<ddeSVGFEConvolveMatrixElement>; }
interface ddeSVGFEDiffuseLightingElement extends SVGFEDiffuseLightingElement{ append: ddeAppend<ddeSVGFEDiffuseLightingElement>; }
interface ddeSVGFEDisplacementMapElement extends SVGFEDisplacementMapElement{ append: ddeAppend<ddeSVGFEDisplacementMapElement>; }
interface ddeSVGFEDistantLightElement extends SVGFEDistantLightElement{ append: ddeAppend<ddeSVGFEDistantLightElement>; }
interface ddeSVGFEDropShadowElement extends SVGFEDropShadowElement{ append: ddeAppend<ddeSVGFEDropShadowElement>; }
interface ddeSVGFEFloodElement extends SVGFEFloodElement{ append: ddeAppend<ddeSVGFEFloodElement>; }
interface ddeSVGFEFuncAElement extends SVGFEFuncAElement{ append: ddeAppend<ddeSVGFEFuncAElement>; }
interface ddeSVGFEFuncBElement extends SVGFEFuncBElement{ append: ddeAppend<ddeSVGFEFuncBElement>; }
interface ddeSVGFEFuncGElement extends SVGFEFuncGElement{ append: ddeAppend<ddeSVGFEFuncGElement>; }
interface ddeSVGFEFuncRElement extends SVGFEFuncRElement{ append: ddeAppend<ddeSVGFEFuncRElement>; }
interface ddeSVGFEGaussianBlurElement extends SVGFEGaussianBlurElement{ append: ddeAppend<ddeSVGFEGaussianBlurElement>; }
interface ddeSVGFEImageElement extends SVGFEImageElement{ append: ddeAppend<ddeSVGFEImageElement>; }
interface ddeSVGFEMergeElement extends SVGFEMergeElement{ append: ddeAppend<ddeSVGFEMergeElement>; }
interface ddeSVGFEMergeNodeElement extends SVGFEMergeNodeElement{ append: ddeAppend<ddeSVGFEMergeNodeElement>; }
interface ddeSVGFEMorphologyElement extends SVGFEMorphologyElement{ append: ddeAppend<ddeSVGFEMorphologyElement>; }
interface ddeSVGFEOffsetElement extends SVGFEOffsetElement{ append: ddeAppend<ddeSVGFEOffsetElement>; }
interface ddeSVGFEPointLightElement extends SVGFEPointLightElement{ append: ddeAppend<ddeSVGFEPointLightElement>; }
interface ddeSVGFESpecularLightingElement extends SVGFESpecularLightingElement{ append: ddeAppend<ddeSVGFESpecularLightingElement>; }
interface ddeSVGFESpotLightElement extends SVGFESpotLightElement{ append: ddeAppend<ddeSVGFESpotLightElement>; }
interface ddeSVGFETileElement extends SVGFETileElement{ append: ddeAppend<ddeSVGFETileElement>; }
interface ddeSVGFETurbulenceElement extends SVGFETurbulenceElement{ append: ddeAppend<ddeSVGFETurbulenceElement>; }
interface ddeSVGFilterElement extends SVGFilterElement{ append: ddeAppend<ddeSVGFilterElement>; }
interface ddeSVGForeignObjectElement extends SVGForeignObjectElement{ append: ddeAppend<ddeSVGForeignObjectElement>; }
interface ddeSVGGElement extends SVGGElement{ append: ddeAppend<ddeSVGGElement>; }
interface ddeSVGImageElement extends SVGImageElement{ append: ddeAppend<ddeSVGImageElement>; }
interface ddeSVGLineElement extends SVGLineElement{ append: ddeAppend<ddeSVGLineElement>; }
interface ddeSVGLinearGradientElement extends SVGLinearGradientElement{ append: ddeAppend<ddeSVGLinearGradientElement>; }
interface ddeSVGMarkerElement extends SVGMarkerElement{ append: ddeAppend<ddeSVGMarkerElement>; }
interface ddeSVGMaskElement extends SVGMaskElement{ append: ddeAppend<ddeSVGMaskElement>; }
interface ddeSVGMetadataElement extends SVGMetadataElement{ append: ddeAppend<ddeSVGMetadataElement>; }
interface ddeSVGMPathElement extends SVGMPathElement{ append: ddeAppend<ddeSVGMPathElement>; }
interface ddeSVGPathElement extends SVGPathElement{ append: ddeAppend<ddeSVGPathElement>; }
interface ddeSVGPatternElement extends SVGPatternElement{ append: ddeAppend<ddeSVGPatternElement>; }
interface ddeSVGPolygonElement extends SVGPolygonElement{ append: ddeAppend<ddeSVGPolygonElement>; }
interface ddeSVGPolylineElement extends SVGPolylineElement{ append: ddeAppend<ddeSVGPolylineElement>; }
interface ddeSVGRadialGradientElement extends SVGRadialGradientElement{ append: ddeAppend<ddeSVGRadialGradientElement>; }
interface ddeSVGRectElement extends SVGRectElement{ append: ddeAppend<ddeSVGRectElement>; }
interface ddeSVGScriptElement extends SVGScriptElement{ append: ddeAppend<ddeSVGScriptElement>; }
interface ddeSVGSetElement extends SVGSetElement{ append: ddeAppend<ddeSVGSetElement>; }
interface ddeSVGStopElement extends SVGStopElement{ append: ddeAppend<ddeSVGStopElement>; }
interface ddeSVGStyleElement extends SVGStyleElement{ append: ddeAppend<ddeSVGStyleElement>; }
interface ddeSVGSVGElement extends SVGSVGElement{ append: ddeAppend<ddeSVGSVGElement>; }
interface ddeSVGSwitchElement extends SVGSwitchElement{ append: ddeAppend<ddeSVGSwitchElement>; }
interface ddeSVGSymbolElement extends SVGSymbolElement{ append: ddeAppend<ddeSVGSymbolElement>; }
interface ddeSVGTextElement extends SVGTextElement{ append: ddeAppend<ddeSVGTextElement>; }
interface ddeSVGTextPathElement extends SVGTextPathElement{ append: ddeAppend<ddeSVGTextPathElement>; }
interface ddeSVGTitleElement extends SVGTitleElement{ append: ddeAppend<ddeSVGTitleElement>; }
interface ddeSVGTSpanElement extends SVGTSpanElement{ append: ddeAppend<ddeSVGTSpanElement>; }
interface ddeSVGUseElement extends SVGUseElement{ append: ddeAppend<ddeSVGUseElement>; }
interface ddeSVGViewElement extends SVGViewElement{ append: ddeAppend<ddeSVGViewElement>; }
// editorconfig-checker-enable
export interface Signal<V, A> {
/** The current value of the signal */
get(): V;
/** Set new value of the signal */
set(value: V): V;
toJSON(): V;
valueOf(): V;
}
type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void;
//type SymbolSignal= Symbol;
type SymbolOnclear= symbol;
type Actions<V>= Record<string | SymbolOnclear, Action<V>>;
type OnListenerOptions= Pick<AddEventListenerOptions, "signal"> & { first_time?: boolean };
interface signal{
_: Symbol
/**
* Computations signal. This creates a signal which is computed from other signals.
* */
<V extends ()=> any>(computation: V): Signal<ReturnType<V>, {}>
/**
* 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.get()+" "+surname.get());
* ```
* @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`.
* */
<V, A extends Actions<V>>(value: V, actions?: A): Signal<V, A>;
action<S extends Signal<any, Actions<any>>, A extends (S extends Signal<any, infer A> ? A : never), N extends keyof A>(
signal: S,
name: N,
...params: A[N] extends (...args: infer P)=> any ? P : never
): void;
clear(...signals: Signal<any, any>[]): void;
on<T>(signal: Signal<T, any>, 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<S extends any>(signal: Signal<S, any>, el: (v: S)=> Element | Element[] | DocumentFragment): DocumentFragment;
observedAttributes(custom_element: HTMLElement): Record<string, Signal<string, {}>>;
}
export const signal: signal;
export const S: signal;
declare global {
type ddeSignal<T, A= {}>= Signal<T, A>;
type ddeAction<V>= Action<V>
type ddeActions<V>= Actions<V>
}

View File

@ -52,9 +52,12 @@ type IsReadonly<T, K extends keyof T> =
* @private
*/
type ElementAttributes<T extends SupportedElement>= Partial<{
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false
[K in keyof _fromElsInterfaces<T>]:
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
: ddeStringable
: ddeStringable)
} & AttrsModified> & Record<string, any>;
export function classListDeclarative<El extends SupportedElement>(
element: El,
@ -515,7 +518,14 @@ interface ddeSVGTSpanElement extends SVGTSpanElement{ append: ddeAppend<ddeSVGTS
interface ddeSVGUseElement extends SVGUseElement{ append: ddeAppend<ddeSVGUseElement>; }
interface ddeSVGViewElement extends SVGViewElement{ append: ddeAppend<ddeSVGViewElement>; }
// editorconfig-checker-enable
export type Signal<V, A>= (set?: V)=> V & A;
export interface Signal<V, A> {
/** The current value of the signal */
get(): V;
/** Set new value of the signal */
set(value: V): V;
toJSON(): V;
valueOf(): V;
}
type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void;
//type SymbolSignal= Symbol;
type SymbolOnclear= symbol;
@ -544,7 +554,7 @@ interface signal{
* ```js
* const name= S("Jan");
* const surname= S("Andrle");
* const fullname= S(()=> name()+" "+surname());
* const fullname= S(()=> name.get()+" "+surname.get());
* ```
* @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.

1336
dist/esm-with-signals.js vendored

File diff suppressed because it is too large Load Diff

4
dist/esm-with-signals.min.js vendored Normal file

File diff suppressed because one or more lines are too long

520
dist/esm.d.min.ts vendored Normal file
View File

@ -0,0 +1,520 @@
declare global{ /* ddeSignal */ }
type CustomElementTagNameMap= { '#text': Text, '#comment': Comment }
type SupportedElement=
HTMLElementTagNameMap[keyof HTMLElementTagNameMap]
| SVGElementTagNameMap[keyof SVGElementTagNameMap]
| MathMLElementTagNameMap[keyof MathMLElementTagNameMap]
| CustomElementTagNameMap[keyof CustomElementTagNameMap]
declare global {
type ddeComponentAttributes= Record<any, any> | undefined;
type ddeElementAddon<El extends SupportedElement | DocumentFragment | Node>= (element: El)=> any;
type ddeString= string | ddeSignal<string>
type ddeStringable= ddeString | number | ddeSignal<number>
}
type PascalCase=
`${Capitalize<string>}${string}`;
type AttrsModified= {
/**
* Use string like in HTML (internally uses `*.setAttribute("style", *)`), or object representation (like DOM API).
*/
style: Partial<CSSStyleDeclaration> | ddeString
| Partial<{ [K in keyof CSSStyleDeclaration]: ddeSignal<CSSStyleDeclaration[K]> }>
/**
* 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<string,-1|0|1|boolean|ddeSignal<-1|0|1|boolean>>,
/**
* Used by the dataset HTML attribute to represent data for custom attributes added to elements.
* Values are converted to string (see {@link DOMStringMap}).
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMStringMap)
* */
dataset: Record<string, ddeStringable>,
/**
* Sets `aria-*` simiraly to `dataset`
* */
ariaset: Record<string, ddeString>,
} & Record<`=${string}` | `data${PascalCase}` | `aria${PascalCase}`, ddeString>
& Record<`.${string}`, any>
type _fromElsInterfaces<EL extends SupportedElement>= Omit<EL, keyof AttrsModified>;
type IsReadonly<T, K extends keyof T> =
T extends { readonly [P in K]: T[K] } ? true : false;
/**
* Just element attributtes
*
* In most cases, you can use native propertie such as
* [MDN WEB/API/Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) and so on
* (e.g. [`Text`](https://developer.mozilla.org/en-US/docs/Web/API/Text)).
*
* There is added support for `data[A-Z].*`/`aria[A-Z].*` to be converted to the kebab-case alternatives.
* @private
*/
type ElementAttributes<T extends SupportedElement>= Partial<{
[K in keyof _fromElsInterfaces<T>]:
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
: ddeStringable)
} & AttrsModified> & Record<string, any>;
export function classListDeclarative<El extends SupportedElement>(
element: El,
classList: AttrsModified["classList"]
): El
export function assign<El extends SupportedElement>(element: El, ...attrs_array: ElementAttributes<El>[]): El
export function assignAttribute<El extends SupportedElement, ATT extends keyof ElementAttributes<El>>(
element: El,
attr: ATT,
value: ElementAttributes<El>[ATT]
): ElementAttributes<El>[ATT]
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap;
export function el<
TAG extends keyof ExtendedHTMLElementTagNameMap,
>(
tag_name: TAG,
attrs?: ElementAttributes<ExtendedHTMLElementTagNameMap[NoInfer<TAG>]> | ddeStringable,
...addons: ddeElementAddon<
ExtendedHTMLElementTagNameMap[NoInfer<TAG>]
>[], // TODO: for now addons must have the same element
): TAG extends keyof ddeHTMLElementTagNameMap ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement
export function el(
tag_name?: "<>",
): ddeDocumentFragment
export function el(
tag_name: string,
attrs?: ElementAttributes<HTMLElement> | ddeStringable,
...addons: ddeElementAddon<HTMLElement>[]
): ddeHTMLElement
export function el<
C extends (attr: ddeComponentAttributes)=> SupportedElement | ddeDocumentFragment
>(
component: C,
attrs?: Parameters<C>[0] | ddeStringable,
...addons: ddeElementAddon<ReturnType<C>>[]
): ReturnType<C> extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
? ReturnType<C>
: ( ReturnType<C> extends ddeDocumentFragment ? ReturnType<C> : ddeHTMLElement )
export { el as createElement }
export function elNS(
namespace: "http://www.w3.org/2000/svg"
): <
TAG extends keyof SVGElementTagNameMap & string,
EL extends ( TAG extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[TAG] : SVGElement ),
>(
tag_name: TAG,
attrs?: ElementAttributes<NoInfer<EL>> | ddeStringable,
...addons: ddeElementAddon<NoInfer<EL>>[]
)=> TAG extends keyof ddeSVGElementTagNameMap ? ddeSVGElementTagNameMap[TAG] : ddeSVGElement
export function elNS(
namespace: "http://www.w3.org/1998/Math/MathML"
): <
TAG extends keyof MathMLElementTagNameMap & string,
EL extends ( TAG extends keyof MathMLElementTagNameMap ? MathMLElementTagNameMap[TAG] : MathMLElement ),
>(
tag_name: TAG,
attrs?: ddeStringable | Partial<{
[key in keyof EL]: EL[key] | ddeSignal<EL[key]> | string | number | boolean
}>,
...addons: ddeElementAddon<NoInfer<EL>>[]
)=> ddeMathMLElement
export function elNS(
namespace: string
): (
tag_name: string,
attrs?: string | ddeStringable | Record<string, any>,
...addons: ddeElementAddon<SupportedElement>[]
)=> SupportedElement
export { elNS as createElementNS }
export function chainableAppend<EL extends SupportedElement>(el: EL): EL;
/** Simulate slots for ddeComponents */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
root: EL,
): EL
/**
* Simulate slots in Custom Elements without using `shadowRoot`.
* @param el Custom Element root element
* @param body Body of the custom element
* */
export function simulateSlots<EL extends SupportedElement | DocumentFragment>(
el: HTMLElement,
body: EL,
): EL
export function dispatchEvent(name: keyof DocumentEventMap | string, options?: EventInit):
(element: SupportedElement, data?: any)=> void;
export function dispatchEvent(
name: keyof DocumentEventMap | string,
options: EventInit | null,
element: SupportedElement | (()=> SupportedElement)
): (data?: any)=> void;
interface On{
/** Listens to the DOM event. See {@link Document.addEventListener} */
<
Event extends keyof DocumentEventMap,
EE extends ddeElementAddon<SupportedElement>= ddeElementAddon<HTMLElement>,
>(
type: Event,
listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: DocumentEventMap[Event]) => any,
options?: AddEventListenerOptions
) : EE;
<
EE extends ddeElementAddon<SupportedElement>= ddeElementAddon<HTMLElement>,
>(
type: string,
listener: (this: EE extends ddeElementAddon<infer El> ? El : never, ev: Event | CustomEvent ) => any,
options?: AddEventListenerOptions
) : EE;
/** Listens to the element is connected to the live DOM. In case of custom elements uses [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
connected<
EE extends ddeElementAddon<SupportedElement>,
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
>(
listener: (this: El, event: CustomEvent<El>) => any,
options?: AddEventListenerOptions
) : EE;
/** Listens to the element is disconnected from the live DOM. In case of custom elements uses [`disconnectedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
disconnected<
EE extends ddeElementAddon<SupportedElement>,
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
>(
listener: (this: El, event: CustomEvent<void>) => any,
options?: AddEventListenerOptions
) : EE;
/** Listens to the element attribute changes. In case of custom elements uses [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks), or {@link MutationObserver} else where */// editorconfig-checker-disable-line
attributeChanged<
EE extends ddeElementAddon<SupportedElement>,
El extends ( EE extends ddeElementAddon<infer El> ? El : never )
>(
listener: (this: El, event: CustomEvent<[ string, string ]>) => any,
options?: AddEventListenerOptions
) : EE;
}
export const on: On;
type Scope= {
scope: Node | Function | Object,
host: ddeElementAddon<any>,
custom_element: false | HTMLElement,
prevent: boolean
};
/** Current scope created last time the `el(Function)` was invoke. (Or {@link scope.push}) */
export const scope: {
current: Scope,
/** Stops all automatizations. E. g. signals used as attributes in current scope
* registers removing these listeners (and clean signal if no other listeners are detected)
* on `disconnected` event. */
preventDefault<T extends boolean>(prevent: T): T,
/**
* This represents reference to the current host element — `scope.host()`.
* It can be also used to register Addon(s) (functions to be called when component is initized)
* — `scope.host(on.connected(console.log))`.
* */
host: (...addons: ddeElementAddon<SupportedElement>[])=> HTMLElement,
state: Scope[],
/** Adds new child scope. All attributes are inherited by default. */
push(scope: Partial<Scope>): ReturnType<Array<Scope>["push"]>,
/** Adds root scope as a child of the current scope. */
pushRoot(): ReturnType<Array<Scope>["push"]>,
/** Removes last/current child scope. */
pop(): ReturnType<Array<Scope>["pop"]>,
};
export function customElementRender<
EL extends HTMLElement,
P extends any = Record<string, string | ddeSignal<string>>
>(
target: ShadowRoot | EL,
render: (props: P)=> SupportedElement | DocumentFragment,
props?: P | ((el: EL)=> P)
): EL
export function customElementWithDDE<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
export function lifecyclesToEvents<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
export function observedAttributes(custom_element: HTMLElement): Record<string, string>
/* TypeScript MEH */
declare global{
type ddeAppend<el>= (...nodes: (Node | string)[])=> el;
interface ddeDocumentFragment extends DocumentFragment{ append: ddeAppend<ddeDocumentFragment>; }
interface ddeHTMLElement extends HTMLElement{ append: ddeAppend<ddeHTMLElement>; }
interface ddeSVGElement extends SVGElement{ append: ddeAppend<ddeSVGElement>; }
interface ddeMathMLElement extends MathMLElement{ append: ddeAppend<ddeMathMLElement>; }
interface ddeHTMLElementTagNameMap {
"a": ddeHTMLAnchorElement;
"area": ddeHTMLAreaElement;
"audio": ddeHTMLAudioElement;
"base": ddeHTMLBaseElement;
"blockquote": ddeHTMLQuoteElement;
"body": ddeHTMLBodyElement;
"br": ddeHTMLBRElement;
"button": ddeHTMLButtonElement;
"canvas": ddeHTMLCanvasElement;
"caption": ddeHTMLTableCaptionElement;
"col": ddeHTMLTableColElement;
"colgroup": ddeHTMLTableColElement;
"data": ddeHTMLDataElement;
"datalist": ddeHTMLDataListElement;
"del": ddeHTMLModElement;
"details": ddeHTMLDetailsElement;
"dialog": ddeHTMLDialogElement;
"div": ddeHTMLDivElement;
"dl": ddeHTMLDListElement;
"embed": ddeHTMLEmbedElement;
"fieldset": ddeHTMLFieldSetElement;
"form": ddeHTMLFormElement;
"h1": ddeHTMLHeadingElement;
"h2": ddeHTMLHeadingElement;
"h3": ddeHTMLHeadingElement;
"h4": ddeHTMLHeadingElement;
"h5": ddeHTMLHeadingElement;
"h6": ddeHTMLHeadingElement;
"head": ddeHTMLHeadElement;
"hr": ddeHTMLHRElement;
"html": ddeHTMLHtmlElement;
"iframe": ddeHTMLIFrameElement;
"img": ddeHTMLImageElement;
"input": ddeHTMLInputElement;
"ins": ddeHTMLModElement;
"label": ddeHTMLLabelElement;
"legend": ddeHTMLLegendElement;
"li": ddeHTMLLIElement;
"link": ddeHTMLLinkElement;
"map": ddeHTMLMapElement;
"menu": ddeHTMLMenuElement;
"meta": ddeHTMLMetaElement;
"meter": ddeHTMLMeterElement;
"object": ddeHTMLObjectElement;
"ol": ddeHTMLOListElement;
"optgroup": ddeHTMLOptGroupElement;
"option": ddeHTMLOptionElement;
"output": ddeHTMLOutputElement;
"p": ddeHTMLParagraphElement;
"picture": ddeHTMLPictureElement;
"pre": ddeHTMLPreElement;
"progress": ddeHTMLProgressElement;
"q": ddeHTMLQuoteElement;
"script": ddeHTMLScriptElement;
"select": ddeHTMLSelectElement;
"slot": ddeHTMLSlotElement;
"source": ddeHTMLSourceElement;
"span": ddeHTMLSpanElement;
"style": ddeHTMLStyleElement;
"table": ddeHTMLTableElement;
"tbody": ddeHTMLTableSectionElement;
"td": ddeHTMLTableCellElement;
"template": ddeHTMLTemplateElement;
"textarea": ddeHTMLTextAreaElement;
"tfoot": ddeHTMLTableSectionElement;
"th": ddeHTMLTableCellElement;
"thead": ddeHTMLTableSectionElement;
"time": ddeHTMLTimeElement;
"title": ddeHTMLTitleElement;
"tr": ddeHTMLTableRowElement;
"track": ddeHTMLTrackElement;
"ul": ddeHTMLUListElement;
"video": ddeHTMLVideoElement;
}
interface ddeSVGElementTagNameMap {
"a": ddeSVGAElement;
"animate": ddeSVGAnimateElement;
"animateMotion": ddeSVGAnimateMotionElement;
"animateTransform": ddeSVGAnimateTransformElement;
"circle": ddeSVGCircleElement;
"clipPath": ddeSVGClipPathElement;
"defs": ddeSVGDefsElement;
"desc": ddeSVGDescElement;
"ellipse": ddeSVGEllipseElement;
"feBlend": ddeSVGFEBlendElement;
"feColorMatrix": ddeSVGFEColorMatrixElement;
"feComponentTransfer": ddeSVGFEComponentTransferElement;
"feComposite": ddeSVGFECompositeElement;
"feConvolveMatrix": ddeSVGFEConvolveMatrixElement;
"feDiffuseLighting": ddeSVGFEDiffuseLightingElement;
"feDisplacementMap": ddeSVGFEDisplacementMapElement;
"feDistantLight": ddeSVGFEDistantLightElement;
"feDropShadow": ddeSVGFEDropShadowElement;
"feFlood": ddeSVGFEFloodElement;
"feFuncA": ddeSVGFEFuncAElement;
"feFuncB": ddeSVGFEFuncBElement;
"feFuncG": ddeSVGFEFuncGElement;
"feFuncR": ddeSVGFEFuncRElement;
"feGaussianBlur": ddeSVGFEGaussianBlurElement;
"feImage": ddeSVGFEImageElement;
"feMerge": ddeSVGFEMergeElement;
"feMergeNode": ddeSVGFEMergeNodeElement;
"feMorphology": ddeSVGFEMorphologyElement;
"feOffset": ddeSVGFEOffsetElement;
"fePointLight": ddeSVGFEPointLightElement;
"feSpecularLighting": ddeSVGFESpecularLightingElement;
"feSpotLight": ddeSVGFESpotLightElement;
"feTile": ddeSVGFETileElement;
"feTurbulence": ddeSVGFETurbulenceElement;
"filter": ddeSVGFilterElement;
"foreignObject": ddeSVGForeignObjectElement;
"g": ddeSVGGElement;
"image": ddeSVGImageElement;
"line": ddeSVGLineElement;
"linearGradient": ddeSVGLinearGradientElement;
"marker": ddeSVGMarkerElement;
"mask": ddeSVGMaskElement;
"metadata": ddeSVGMetadataElement;
"mpath": ddeSVGMPathElement;
"path": ddeSVGPathElement;
"pattern": ddeSVGPatternElement;
"polygon": ddeSVGPolygonElement;
"polyline": ddeSVGPolylineElement;
"radialGradient": ddeSVGRadialGradientElement;
"rect": ddeSVGRectElement;
"script": ddeSVGScriptElement;
"set": ddeSVGSetElement;
"stop": ddeSVGStopElement;
"style": ddeSVGStyleElement;
"svg": ddeSVGSVGElement;
"switch": ddeSVGSwitchElement;
"symbol": ddeSVGSymbolElement;
"text": ddeSVGTextElement;
"textPath": ddeSVGTextPathElement;
"title": ddeSVGTitleElement;
"tspan": ddeSVGTSpanElement;
"use": ddeSVGUseElement;
"view": ddeSVGViewElement;
}
}
// editorconfig-checker-disable
interface ddeHTMLAnchorElement extends HTMLAnchorElement{ append: ddeAppend<ddeHTMLAnchorElement>; }
interface ddeHTMLAreaElement extends HTMLAreaElement{ append: ddeAppend<ddeHTMLAreaElement>; }
interface ddeHTMLAudioElement extends HTMLAudioElement{ append: ddeAppend<ddeHTMLAudioElement>; }
interface ddeHTMLBaseElement extends HTMLBaseElement{ append: ddeAppend<ddeHTMLBaseElement>; }
interface ddeHTMLQuoteElement extends HTMLQuoteElement{ append: ddeAppend<ddeHTMLQuoteElement>; }
interface ddeHTMLBodyElement extends HTMLBodyElement{ append: ddeAppend<ddeHTMLBodyElement>; }
interface ddeHTMLBRElement extends HTMLBRElement{ append: ddeAppend<ddeHTMLBRElement>; }
interface ddeHTMLButtonElement extends HTMLButtonElement{ append: ddeAppend<ddeHTMLButtonElement>; }
interface ddeHTMLCanvasElement extends HTMLCanvasElement{ append: ddeAppend<ddeHTMLCanvasElement>; }
interface ddeHTMLTableCaptionElement extends HTMLTableCaptionElement{ append: ddeAppend<ddeHTMLTableCaptionElement>; }
interface ddeHTMLTableColElement extends HTMLTableColElement{ append: ddeAppend<ddeHTMLTableColElement>; }
interface ddeHTMLTableColElement extends HTMLTableColElement{ append: ddeAppend<ddeHTMLTableColElement>; }
interface ddeHTMLDataElement extends HTMLDataElement{ append: ddeAppend<ddeHTMLDataElement>; }
interface ddeHTMLDataListElement extends HTMLDataListElement{ append: ddeAppend<ddeHTMLDataListElement>; }
interface ddeHTMLModElement extends HTMLModElement{ append: ddeAppend<ddeHTMLModElement>; }
interface ddeHTMLDetailsElement extends HTMLDetailsElement{ append: ddeAppend<ddeHTMLDetailsElement>; }
interface ddeHTMLDialogElement extends HTMLDialogElement{ append: ddeAppend<ddeHTMLDialogElement>; }
interface ddeHTMLDivElement extends HTMLDivElement{ append: ddeAppend<ddeHTMLDivElement>; }
interface ddeHTMLDListElement extends HTMLDListElement{ append: ddeAppend<ddeHTMLDListElement>; }
interface ddeHTMLEmbedElement extends HTMLEmbedElement{ append: ddeAppend<ddeHTMLEmbedElement>; }
interface ddeHTMLFieldSetElement extends HTMLFieldSetElement{ append: ddeAppend<ddeHTMLFieldSetElement>; }
interface ddeHTMLFormElement extends HTMLFormElement{ append: ddeAppend<ddeHTMLFormElement>; }
interface ddeHTMLHeadingElement extends HTMLHeadingElement{ append: ddeAppend<ddeHTMLHeadingElement>; }
interface ddeHTMLHeadElement extends HTMLHeadElement{ append: ddeAppend<ddeHTMLHeadElement>; }
interface ddeHTMLHRElement extends HTMLHRElement{ append: ddeAppend<ddeHTMLHRElement>; }
interface ddeHTMLHtmlElement extends HTMLHtmlElement{ append: ddeAppend<ddeHTMLHtmlElement>; }
interface ddeHTMLIFrameElement extends HTMLIFrameElement{ append: ddeAppend<ddeHTMLIFrameElement>; }
interface ddeHTMLImageElement extends HTMLImageElement{ append: ddeAppend<ddeHTMLImageElement>; }
interface ddeHTMLInputElement extends HTMLInputElement{ append: ddeAppend<ddeHTMLInputElement>; }
interface ddeHTMLLabelElement extends HTMLLabelElement{ append: ddeAppend<ddeHTMLLabelElement>; }
interface ddeHTMLLegendElement extends HTMLLegendElement{ append: ddeAppend<ddeHTMLLegendElement>; }
interface ddeHTMLLIElement extends HTMLLIElement{ append: ddeAppend<ddeHTMLLIElement>; }
interface ddeHTMLLinkElement extends HTMLLinkElement{ append: ddeAppend<ddeHTMLLinkElement>; }
interface ddeHTMLMapElement extends HTMLMapElement{ append: ddeAppend<ddeHTMLMapElement>; }
interface ddeHTMLMenuElement extends HTMLMenuElement{ append: ddeAppend<ddeHTMLMenuElement>; }
interface ddeHTMLMetaElement extends HTMLMetaElement{ append: ddeAppend<ddeHTMLMetaElement>; }
interface ddeHTMLMeterElement extends HTMLMeterElement{ append: ddeAppend<ddeHTMLMeterElement>; }
interface ddeHTMLObjectElement extends HTMLObjectElement{ append: ddeAppend<ddeHTMLObjectElement>; }
interface ddeHTMLOListElement extends HTMLOListElement{ append: ddeAppend<ddeHTMLOListElement>; }
interface ddeHTMLOptGroupElement extends HTMLOptGroupElement{ append: ddeAppend<ddeHTMLOptGroupElement>; }
interface ddeHTMLOptionElement extends HTMLOptionElement{ append: ddeAppend<ddeHTMLOptionElement>; }
interface ddeHTMLOutputElement extends HTMLOutputElement{ append: ddeAppend<ddeHTMLOutputElement>; }
interface ddeHTMLParagraphElement extends HTMLParagraphElement{ append: ddeAppend<ddeHTMLParagraphElement>; }
interface ddeHTMLPictureElement extends HTMLPictureElement{ append: ddeAppend<ddeHTMLPictureElement>; }
interface ddeHTMLPreElement extends HTMLPreElement{ append: ddeAppend<ddeHTMLPreElement>; }
interface ddeHTMLProgressElement extends HTMLProgressElement{ append: ddeAppend<ddeHTMLProgressElement>; }
interface ddeHTMLScriptElement extends HTMLScriptElement{ append: ddeAppend<ddeHTMLScriptElement>; }
interface ddeHTMLSelectElement extends HTMLSelectElement{ append: ddeAppend<ddeHTMLSelectElement>; }
interface ddeHTMLSlotElement extends HTMLSlotElement{ append: ddeAppend<ddeHTMLSlotElement>; }
interface ddeHTMLSourceElement extends HTMLSourceElement{ append: ddeAppend<ddeHTMLSourceElement>; }
interface ddeHTMLSpanElement extends HTMLSpanElement{ append: ddeAppend<ddeHTMLSpanElement>; }
interface ddeHTMLStyleElement extends HTMLStyleElement{ append: ddeAppend<ddeHTMLStyleElement>; }
interface ddeHTMLTableElement extends HTMLTableElement{ append: ddeAppend<ddeHTMLTableElement>; }
interface ddeHTMLTableSectionElement extends HTMLTableSectionElement{ append: ddeAppend<ddeHTMLTableSectionElement>; }
interface ddeHTMLTableCellElement extends HTMLTableCellElement{ append: ddeAppend<ddeHTMLTableCellElement>; }
interface ddeHTMLTemplateElement extends HTMLTemplateElement{ append: ddeAppend<ddeHTMLTemplateElement>; }
interface ddeHTMLTextAreaElement extends HTMLTextAreaElement{ append: ddeAppend<ddeHTMLTextAreaElement>; }
interface ddeHTMLTableCellElement extends HTMLTableCellElement{ append: ddeAppend<ddeHTMLTableCellElement>; }
interface ddeHTMLTimeElement extends HTMLTimeElement{ append: ddeAppend<ddeHTMLTimeElement>; }
interface ddeHTMLTitleElement extends HTMLTitleElement{ append: ddeAppend<ddeHTMLTitleElement>; }
interface ddeHTMLTableRowElement extends HTMLTableRowElement{ append: ddeAppend<ddeHTMLTableRowElement>; }
interface ddeHTMLTrackElement extends HTMLTrackElement{ append: ddeAppend<ddeHTMLTrackElement>; }
interface ddeHTMLUListElement extends HTMLUListElement{ append: ddeAppend<ddeHTMLUListElement>; }
interface ddeHTMLVideoElement extends HTMLVideoElement{ append: ddeAppend<ddeHTMLVideoElement>; }
interface ddeSVGAElement extends SVGAElement{ append: ddeAppend<ddeSVGAElement>; }
interface ddeSVGAnimateElement extends SVGAnimateElement{ append: ddeAppend<ddeSVGAnimateElement>; }
interface ddeSVGAnimateMotionElement extends SVGAnimateMotionElement{ append: ddeAppend<ddeSVGAnimateMotionElement>; }
interface ddeSVGAnimateTransformElement extends SVGAnimateTransformElement{ append: ddeAppend<ddeSVGAnimateTransformElement>; }
interface ddeSVGCircleElement extends SVGCircleElement{ append: ddeAppend<ddeSVGCircleElement>; }
interface ddeSVGClipPathElement extends SVGClipPathElement{ append: ddeAppend<ddeSVGClipPathElement>; }
interface ddeSVGDefsElement extends SVGDefsElement{ append: ddeAppend<ddeSVGDefsElement>; }
interface ddeSVGDescElement extends SVGDescElement{ append: ddeAppend<ddeSVGDescElement>; }
interface ddeSVGEllipseElement extends SVGEllipseElement{ append: ddeAppend<ddeSVGEllipseElement>; }
interface ddeSVGFEBlendElement extends SVGFEBlendElement{ append: ddeAppend<ddeSVGFEBlendElement>; }
interface ddeSVGFEColorMatrixElement extends SVGFEColorMatrixElement{ append: ddeAppend<ddeSVGFEColorMatrixElement>; }
interface ddeSVGFEComponentTransferElement extends SVGFEComponentTransferElement{ append: ddeAppend<ddeSVGFEComponentTransferElement>; }
interface ddeSVGFECompositeElement extends SVGFECompositeElement{ append: ddeAppend<ddeSVGFECompositeElement>; }
interface ddeSVGFEConvolveMatrixElement extends SVGFEConvolveMatrixElement{ append: ddeAppend<ddeSVGFEConvolveMatrixElement>; }
interface ddeSVGFEDiffuseLightingElement extends SVGFEDiffuseLightingElement{ append: ddeAppend<ddeSVGFEDiffuseLightingElement>; }
interface ddeSVGFEDisplacementMapElement extends SVGFEDisplacementMapElement{ append: ddeAppend<ddeSVGFEDisplacementMapElement>; }
interface ddeSVGFEDistantLightElement extends SVGFEDistantLightElement{ append: ddeAppend<ddeSVGFEDistantLightElement>; }
interface ddeSVGFEDropShadowElement extends SVGFEDropShadowElement{ append: ddeAppend<ddeSVGFEDropShadowElement>; }
interface ddeSVGFEFloodElement extends SVGFEFloodElement{ append: ddeAppend<ddeSVGFEFloodElement>; }
interface ddeSVGFEFuncAElement extends SVGFEFuncAElement{ append: ddeAppend<ddeSVGFEFuncAElement>; }
interface ddeSVGFEFuncBElement extends SVGFEFuncBElement{ append: ddeAppend<ddeSVGFEFuncBElement>; }
interface ddeSVGFEFuncGElement extends SVGFEFuncGElement{ append: ddeAppend<ddeSVGFEFuncGElement>; }
interface ddeSVGFEFuncRElement extends SVGFEFuncRElement{ append: ddeAppend<ddeSVGFEFuncRElement>; }
interface ddeSVGFEGaussianBlurElement extends SVGFEGaussianBlurElement{ append: ddeAppend<ddeSVGFEGaussianBlurElement>; }
interface ddeSVGFEImageElement extends SVGFEImageElement{ append: ddeAppend<ddeSVGFEImageElement>; }
interface ddeSVGFEMergeElement extends SVGFEMergeElement{ append: ddeAppend<ddeSVGFEMergeElement>; }
interface ddeSVGFEMergeNodeElement extends SVGFEMergeNodeElement{ append: ddeAppend<ddeSVGFEMergeNodeElement>; }
interface ddeSVGFEMorphologyElement extends SVGFEMorphologyElement{ append: ddeAppend<ddeSVGFEMorphologyElement>; }
interface ddeSVGFEOffsetElement extends SVGFEOffsetElement{ append: ddeAppend<ddeSVGFEOffsetElement>; }
interface ddeSVGFEPointLightElement extends SVGFEPointLightElement{ append: ddeAppend<ddeSVGFEPointLightElement>; }
interface ddeSVGFESpecularLightingElement extends SVGFESpecularLightingElement{ append: ddeAppend<ddeSVGFESpecularLightingElement>; }
interface ddeSVGFESpotLightElement extends SVGFESpotLightElement{ append: ddeAppend<ddeSVGFESpotLightElement>; }
interface ddeSVGFETileElement extends SVGFETileElement{ append: ddeAppend<ddeSVGFETileElement>; }
interface ddeSVGFETurbulenceElement extends SVGFETurbulenceElement{ append: ddeAppend<ddeSVGFETurbulenceElement>; }
interface ddeSVGFilterElement extends SVGFilterElement{ append: ddeAppend<ddeSVGFilterElement>; }
interface ddeSVGForeignObjectElement extends SVGForeignObjectElement{ append: ddeAppend<ddeSVGForeignObjectElement>; }
interface ddeSVGGElement extends SVGGElement{ append: ddeAppend<ddeSVGGElement>; }
interface ddeSVGImageElement extends SVGImageElement{ append: ddeAppend<ddeSVGImageElement>; }
interface ddeSVGLineElement extends SVGLineElement{ append: ddeAppend<ddeSVGLineElement>; }
interface ddeSVGLinearGradientElement extends SVGLinearGradientElement{ append: ddeAppend<ddeSVGLinearGradientElement>; }
interface ddeSVGMarkerElement extends SVGMarkerElement{ append: ddeAppend<ddeSVGMarkerElement>; }
interface ddeSVGMaskElement extends SVGMaskElement{ append: ddeAppend<ddeSVGMaskElement>; }
interface ddeSVGMetadataElement extends SVGMetadataElement{ append: ddeAppend<ddeSVGMetadataElement>; }
interface ddeSVGMPathElement extends SVGMPathElement{ append: ddeAppend<ddeSVGMPathElement>; }
interface ddeSVGPathElement extends SVGPathElement{ append: ddeAppend<ddeSVGPathElement>; }
interface ddeSVGPatternElement extends SVGPatternElement{ append: ddeAppend<ddeSVGPatternElement>; }
interface ddeSVGPolygonElement extends SVGPolygonElement{ append: ddeAppend<ddeSVGPolygonElement>; }
interface ddeSVGPolylineElement extends SVGPolylineElement{ append: ddeAppend<ddeSVGPolylineElement>; }
interface ddeSVGRadialGradientElement extends SVGRadialGradientElement{ append: ddeAppend<ddeSVGRadialGradientElement>; }
interface ddeSVGRectElement extends SVGRectElement{ append: ddeAppend<ddeSVGRectElement>; }
interface ddeSVGScriptElement extends SVGScriptElement{ append: ddeAppend<ddeSVGScriptElement>; }
interface ddeSVGSetElement extends SVGSetElement{ append: ddeAppend<ddeSVGSetElement>; }
interface ddeSVGStopElement extends SVGStopElement{ append: ddeAppend<ddeSVGStopElement>; }
interface ddeSVGStyleElement extends SVGStyleElement{ append: ddeAppend<ddeSVGStyleElement>; }
interface ddeSVGSVGElement extends SVGSVGElement{ append: ddeAppend<ddeSVGSVGElement>; }
interface ddeSVGSwitchElement extends SVGSwitchElement{ append: ddeAppend<ddeSVGSwitchElement>; }
interface ddeSVGSymbolElement extends SVGSymbolElement{ append: ddeAppend<ddeSVGSymbolElement>; }
interface ddeSVGTextElement extends SVGTextElement{ append: ddeAppend<ddeSVGTextElement>; }
interface ddeSVGTextPathElement extends SVGTextPathElement{ append: ddeAppend<ddeSVGTextPathElement>; }
interface ddeSVGTitleElement extends SVGTitleElement{ append: ddeAppend<ddeSVGTitleElement>; }
interface ddeSVGTSpanElement extends SVGTSpanElement{ append: ddeAppend<ddeSVGTSpanElement>; }
interface ddeSVGUseElement extends SVGUseElement{ append: ddeAppend<ddeSVGUseElement>; }
interface ddeSVGViewElement extends SVGViewElement{ append: ddeAppend<ddeSVGViewElement>; }
// editorconfig-checker-enable

7
dist/esm.d.ts vendored
View File

@ -52,9 +52,12 @@ type IsReadonly<T, K extends keyof T> =
* @private
*/
type ElementAttributes<T extends SupportedElement>= Partial<{
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false
[K in keyof _fromElsInterfaces<T>]:
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
: ddeStringable
: ddeStringable)
} & AttrsModified> & Record<string, any>;
export function classListDeclarative<El extends SupportedElement>(
element: El,

841
dist/esm.js vendored
View File

@ -1,451 +1,658 @@
// src/signals-common.js
var C = {
isSignal(t) {
return !1;
},
processReactiveAttribute(t, e, r, n) {
return r;
}
};
function Z(t, e = !0) {
return e ? Object.assign(C, t) : (Object.setPrototypeOf(t, C), t);
}
function S(t) {
return C.isPrototypeOf(t) && t !== C ? t : C;
}
// src/helpers.js
function m(t) {
return typeof t > "u";
function isUndef(value) {
return typeof value === "undefined";
}
function L(t, e) {
if (!t || !(t instanceof AbortSignal))
return !0;
if (!t.aborted)
return t.addEventListener("abort", e), function() {
t.removeEventListener("abort", e);
function isInstance(obj, cls) {
return obj instanceof cls;
}
function isProtoFrom(obj, cls) {
return Object.prototype.isPrototypeOf.call(cls, obj);
}
function oAssign(...o) {
return Object.assign(...o);
}
function onAbort(signal, listener) {
if (!signal || !isInstance(signal, AbortSignal))
return true;
if (signal.aborted)
return;
signal.addEventListener("abort", listener);
return function cleanUp() {
signal.removeEventListener("abort", listener);
};
}
function q(t, e) {
let { observedAttributes: r = [] } = t.constructor;
return r.reduce(function(n, o) {
return n[G(o)] = e(t, o), n;
function observedAttributes(instance, observedAttribute) {
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
return observedAttributes3.reduce(function(out, name) {
out[kebabToCamel(name)] = observedAttribute(instance, name);
return out;
}, {});
}
function G(t) {
return t.replace(/-./g, (e) => e[1].toUpperCase());
function kebabToCamel(name) {
return name.replace(/-./g, (x) => x[1].toUpperCase());
}
// src/signals-lib/common.js
var signals_global = {
/**
* Checks if a value is a signal
* @param {any} attributes - Value to check
* @returns {boolean} Whether the value is a signal
*/
isSignal(attributes) {
return false;
},
/**
* Processes an attribute that might be reactive
* @param {Element} obj - Element that owns the attribute
* @param {string} key - Attribute name
* @param {any} attr - Attribute value
* @param {Function} set - Function to set the attribute
* @returns {any} Processed attribute value
*/
processReactiveAttribute(obj, key, attr, set) {
return attr;
}
};
function registerReactivity(def, global = true) {
if (global) return oAssign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
function signals(_this) {
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
}
// src/dom-common.js
var a = {
setDeleteAttr: V,
var enviroment = {
setDeleteAttr,
ssr: "",
D: globalThis.document,
F: globalThis.DocumentFragment,
H: globalThis.HTMLElement,
S: globalThis.SVGElement,
M: globalThis.MutationObserver,
q: (t) => t || Promise.resolve()
q: (p) => p || Promise.resolve()
};
function V(t, e, r) {
if (Reflect.set(t, e, r), !!m(r)) {
if (Reflect.deleteProperty(t, e), t instanceof a.H && t.getAttribute(e) === "undefined")
return t.removeAttribute(e);
if (Reflect.get(t, e) === "undefined")
return Reflect.set(t, e, "");
function setDeleteAttr(obj, prop, val) {
Reflect.set(obj, prop, val);
if (!isUndef(val)) return;
Reflect.deleteProperty(obj, prop);
if (isInstance(obj, enviroment.H) && obj.getAttribute(prop) === "undefined")
return obj.removeAttribute(prop);
if (Reflect.get(obj, prop) === "undefined")
return Reflect.set(obj, prop, "");
}
}
var x = "__dde_lifecyclesToEvents", v = "dde:connected", w = "dde:disconnected", y = "dde:attributeChanged";
var keyLTE = "__dde_lifecyclesToEvents";
var evc = "dde:connected";
var evd = "dde:disconnected";
var eva = "dde:attributeChanged";
// src/dom.js
function dt(t) {
return a.q(t);
function queue(promise) {
return enviroment.q(promise);
}
var g = [{
var scopes = [{
get scope() {
return a.D.body;
return enviroment.D.body;
},
host: (t) => t ? t(a.D.body) : a.D.body,
prevent: !0
}], O = {
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: true
}];
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
*/
get current() {
return g[g.length - 1];
return scopes[scopes.length - 1];
},
/**
* Gets the host element of the current scope
* @returns {Function} Host accessor function
*/
get host() {
return this.current.host;
},
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
preventDefault() {
let { current: t } = this;
return t.prevent = !0, t;
const { current } = this;
current.prevent = true;
return current;
},
/**
* Gets a copy of the current scope stack
* @returns {Array} Copy of scope stack
*/
get state() {
return [...g];
return [...scopes];
},
push(t = {}) {
return g.push(Object.assign({}, this.current, { prevent: !1 }, t));
/**
* Pushes a new scope to the stack
* @param {Object} [s={}] - Scope object to push
* @returns {number} New length of the scope stack
*/
push(s = {}) {
return scopes.push(oAssign({}, this.current, { prevent: false }, s));
},
/**
* Pushes the root scope to the stack
* @returns {number} New length of the scope stack
*/
pushRoot() {
return g.push(g[0]);
return scopes.push(scopes[0]);
},
/**
* Pops the current scope from the stack
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
*/
pop() {
if (g.length !== 1)
return g.pop();
if (scopes.length === 1) return;
return scopes.pop();
}
};
function k(...t) {
return this.appendOriginal(...t), this;
function append(...els) {
this.appendOriginal(...els);
return this;
}
function J(t) {
return t.append === k || (t.appendOriginal = t.append, t.append = k), t;
function chainableAppend(el) {
if (el.append === append) return el;
el.appendOriginal = el.append;
el.append = append;
return el;
}
var T;
function P(t, e, ...r) {
let n = S(this), o = 0, c, d;
switch ((Object(e) !== e || n.isSignal(e)) && (e = { textContent: e }), !0) {
case typeof t == "function": {
o = 1;
let f = (...l) => l.length ? (o === 1 ? r.unshift(...l) : l.forEach((E) => E(d)), void 0) : d;
O.push({ scope: t, host: f }), c = t(e || void 0);
let p = c instanceof a.F;
if (c.nodeName === "#comment") break;
let b = P.mark({
var namespace;
function createElement(tag, attributes, ...addons) {
const s = signals(this);
let scoped = 0;
let el, el_host;
if (Object(attributes) !== attributes || s.isSignal(attributes))
attributes = { textContent: attributes };
switch (true) {
case typeof tag === "function": {
scoped = 1;
const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host });
el = tag(attributes || void 0);
const is_fragment = isInstance(el, enviroment.F);
if (el.nodeName === "#comment") break;
const el_mark = createElement.mark({
type: "component",
name: t.name,
host: p ? "this" : "parentElement"
name: tag.name,
host: is_fragment ? "this" : "parentElement"
});
c.prepend(b), p && (d = b);
el.prepend(el_mark);
if (is_fragment) el_host = el_mark;
break;
}
case t === "#text":
c = R.call(this, a.D.createTextNode(""), e);
case tag === "#text":
el = assign.call(this, enviroment.D.createTextNode(""), attributes);
break;
case (t === "<>" || !t):
c = R.call(this, a.D.createDocumentFragment(), e);
case (tag === "<>" || !tag):
el = assign.call(this, enviroment.D.createDocumentFragment(), attributes);
break;
case !!T:
c = R.call(this, a.D.createElementNS(T, t), e);
case Boolean(namespace):
el = assign.call(this, enviroment.D.createElementNS(namespace, tag), attributes);
break;
case !c:
c = R.call(this, a.D.createElement(t), e);
case !el:
el = assign.call(this, enviroment.D.createElement(tag), attributes);
}
return J(c), d || (d = c), r.forEach((f) => f(d)), o && O.pop(), o = 2, c;
chainableAppend(el);
if (!el_host) el_host = el;
addons.forEach((c) => c(el_host));
if (scoped) scope.pop();
scoped = 2;
return el;
}
P.mark = function(t, e = !1) {
t = Object.entries(t).map(([o, c]) => o + `="${c}"`).join(" ");
let r = e ? "" : "/", n = a.D.createComment(`<dde:mark ${t}${a.ssr}${r}>`);
return e && (n.end = a.D.createComment("</dde:mark>")), n;
createElement.mark = function(attrs, is_open = false) {
attrs = Object.entries(attrs).map(([n, v]) => n + `="${v}"`).join(" ");
const end = is_open ? "" : "/";
const out = enviroment.D.createComment(`<dde:mark ${attrs}${enviroment.ssr}${end}>`);
if (is_open) out.end = enviroment.D.createComment("</dde:mark>");
return out;
};
function pt(t) {
let e = this;
return function(...n) {
T = t;
let o = P.call(e, ...n);
return T = void 0, o;
function createElementNS(ns) {
const _this = this;
return function createElementNSCurried(...rest) {
namespace = ns;
const el = createElement.call(_this, ...rest);
namespace = void 0;
return el;
};
}
function lt(t, e = t) {
let r = "\xB9\u2070", n = "\u2713", o = Object.fromEntries(
Array.from(e.querySelectorAll("slot")).filter((c) => !c.name.endsWith(r)).map((c) => [c.name += r, c])
function simulateSlots(element, root = element) {
const mark_e = "\xB9\u2070", mark_s = "\u2713";
const slots = Object.fromEntries(
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
);
if (t.append = new Proxy(t.append, {
apply(c, d, f) {
if (f[0] === e) return c.apply(t, f);
for (let p of f) {
let b = (p.slot || "") + r;
element.append = new Proxy(element.append, {
apply(orig, _, els) {
if (els[0] === root) return orig.apply(element, els);
for (const el of els) {
const name = (el.slot || "") + mark_e;
try {
Q(p, "remove", "slot");
} catch {
elementAttribute(el, "remove", "slot");
} catch (_error) {
}
let l = o[b];
if (!l) return;
l.name.startsWith(n) || (l.childNodes.forEach((E) => E.remove()), l.name = n + b), l.append(p);
const slot = slots[name];
if (!slot) return;
if (!slot.name.startsWith(mark_s)) {
slot.childNodes.forEach((c) => c.remove());
slot.name = mark_s + name;
}
return t.append = c, t;
slot.append(el);
}
}), t !== e) {
let c = Array.from(t.childNodes);
t.append(...c);
element.append = orig;
return element;
}
return e;
});
if (element !== root) {
const els = Array.from(element.childNodes);
element.append(...els);
}
var N = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: $ } = a;
function R(t, ...e) {
if (!e.length) return t;
N.set(t, H(t, this));
for (let [r, n] of Object.entries(Object.assign({}, ...e)))
U.call(this, t, r, n);
return N.delete(t), t;
return root;
}
function U(t, e, r) {
let { setRemoveAttr: n, s: o } = H(t, this), c = this;
r = o.processReactiveAttribute(
t,
e,
r,
(f, p) => U.call(c, t, f, p)
var assign_context = /* @__PURE__ */ new WeakMap();
var { setDeleteAttr: setDeleteAttr2 } = enviroment;
function assign(element, ...attributes) {
if (!attributes.length) return element;
assign_context.set(element, assignContext(element, this));
for (const [key, value] of Object.entries(oAssign({}, ...attributes)))
assignAttribute.call(this, element, key, value);
assign_context.delete(element);
return element;
}
function assignAttribute(element, key, value) {
const { setRemoveAttr, s } = assignContext(element, this);
const _this = this;
value = s.processReactiveAttribute(
element,
key,
value,
(key2, value2) => assignAttribute.call(_this, element, key2, value2)
);
let [d] = e;
if (d === "=") return n(e.slice(1), r);
if (d === ".") return F(t, e.slice(1), r);
if (/(aria|data)([A-Z])/.test(e))
return e = e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), n(e, r);
switch (e === "className" && (e = "class"), e) {
const [k] = key;
if ("=" === k) return setRemoveAttr(key.slice(1), value);
if ("." === k) return setDelete(element, key.slice(1), value);
if (/(aria|data)([A-Z])/.test(key)) {
key = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
return setRemoveAttr(key, value);
}
if ("className" === key) key = "class";
switch (key) {
case "xlink:href":
return n(e, r, "http://www.w3.org/1999/xlink");
return setRemoveAttr(key, value, "http://www.w3.org/1999/xlink");
case "textContent":
return $(t, e, r);
return setDeleteAttr2(element, key, value);
case "style":
if (typeof r != "object") break;
if (typeof value !== "object") break;
/* falls through */
case "dataset":
return M(o, e, t, r, F.bind(null, t[e]));
return forEachEntries(s, key, element, value, setDelete.bind(null, element[key]));
case "ariaset":
return M(o, e, t, r, (f, p) => n("aria-" + f, p));
return forEachEntries(s, key, element, value, (key2, val) => setRemoveAttr("aria-" + key2, val));
case "classList":
return K.call(c, t, r);
return classListDeclarative.call(_this, element, value);
}
return X(t, e) ? $(t, e, r) : n(e, r);
return isPropSetter(element, key) ? setDeleteAttr2(element, key, value) : setRemoveAttr(key, value);
}
function H(t, e) {
if (N.has(t)) return N.get(t);
let n = (t instanceof a.S ? tt : Y).bind(null, t, "Attribute"), o = S(e);
return { setRemoveAttr: n, s: o };
function assignContext(element, _this) {
if (assign_context.has(element)) return assign_context.get(element);
const is_svg = isInstance(element, enviroment.S);
const setRemoveAttr = (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
const s = signals(_this);
return { setRemoveAttr, s };
}
function K(t, e) {
let r = S(this);
return M(
r,
function classListDeclarative(element, toggle) {
const s = signals(this);
forEachEntries(
s,
"classList",
t,
e,
(n, o) => t.classList.toggle(n, o === -1 ? void 0 : !!o)
), t;
element,
toggle,
(class_name, val) => element.classList.toggle(class_name, val === -1 ? void 0 : Boolean(val))
);
return element;
}
function Q(t, e, r, n) {
return t instanceof a.H ? t[e + "Attribute"](r, n) : t[e + "AttributeNS"](null, r, n);
function elementAttribute(element, op, key, value) {
if (isInstance(element, enviroment.H))
return element[op + "Attribute"](key, value);
return element[op + "AttributeNS"](null, key, value);
}
function X(t, e) {
if (!(e in t)) return !1;
let r = z(t, e);
return !m(r.set);
function isPropSetter(el, key) {
if (!(key in el)) return false;
const des = getPropDescriptor(el, key);
return !isUndef(des.set);
}
function z(t, e) {
if (t = Object.getPrototypeOf(t), !t) return {};
let r = Object.getOwnPropertyDescriptor(t, e);
return r || z(t, e);
function getPropDescriptor(p, key) {
p = Object.getPrototypeOf(p);
if (!p) return {};
const des = Object.getOwnPropertyDescriptor(p, key);
if (!des) return getPropDescriptor(p, key);
return des;
}
function M(t, e, r, n, o) {
let c = String;
if (!(typeof n != "object" || n === null))
return Object.entries(n).forEach(function([f, p]) {
f && (f = new c(f), f.target = e, p = t.processReactiveAttribute(r, f, p, o), o(f, p));
function forEachEntries(s, target, element, obj, cb) {
const S = String;
if (typeof obj !== "object" || obj === null) return;
return Object.entries(obj).forEach(function process([key, val]) {
if (!key) return;
key = new S(key);
key.target = target;
val = s.processReactiveAttribute(element, key, val, cb);
cb(key, val);
});
}
function Y(t, e, r, n) {
return t[(m(n) ? "remove" : "set") + e](r, n);
function setRemove(obj, prop, key, val) {
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
}
function tt(t, e, r, n, o = null) {
return t[(m(n) ? "remove" : "set") + e + "NS"](o, r, n);
function setRemoveNS(obj, prop, key, val, ns = null) {
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
}
function F(t, e, r) {
if (Reflect.set(t, e, r), !!m(r))
return Reflect.deleteProperty(t, e);
function setDelete(obj, key, val) {
Reflect.set(obj, key, val);
if (!isUndef(val)) return;
return Reflect.deleteProperty(obj, key);
}
// src/events-observer.js
var _ = a.M ? et() : new Proxy({}, {
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() {
return () => {
};
}
});
function et() {
let t = /* @__PURE__ */ new Map(), e = !1, r = (s) => function(u) {
for (let i of u)
if (i.type === "childList") {
if (l(i.addedNodes, !0)) {
s();
function connectionsChangesObserverConstructor() {
const store = /* @__PURE__ */ new Map();
let is_observing = false;
const observerListener = (stop2) => function(mutations) {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue;
}
E(i.removedNodes, !0) && s();
}
}, n = new a.M(r(f));
return {
observe(s) {
let u = new a.M(r(() => {
}));
return u.observe(s, { childList: !0, subtree: !0 }), () => u.disconnect();
},
onConnected(s, u) {
d();
let i = c(s);
i.connected.has(u) || (i.connected.add(u), i.length_c += 1);
},
offConnected(s, u) {
if (!t.has(s)) return;
let i = t.get(s);
i.connected.has(u) && (i.connected.delete(u), i.length_c -= 1, o(s, i));
},
onDisconnected(s, u) {
d();
let i = c(s);
i.disconnected.has(u) || (i.disconnected.add(u), i.length_d += 1);
},
offDisconnected(s, u) {
if (!t.has(s)) return;
let i = t.get(s);
i.disconnected.has(u) && (i.disconnected.delete(u), i.length_d -= 1, o(s, i));
if (observerRemoved(mutation.removedNodes, true))
stop2();
}
};
function o(s, u) {
u.length_c || u.length_d || (t.delete(s), f());
const observer = new enviroment.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
}
function c(s) {
if (t.has(s)) return t.get(s);
let u = {
};
function cleanWhenOff(element, ls) {
if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(),
length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0
};
return t.set(s, u), u;
store.set(element, out);
return out;
}
function d() {
e || (e = !0, n.observe(a.D.body, { childList: !0, subtree: !0 }));
function start() {
if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
}
function f() {
!e || t.size || (e = !1, n.disconnect());
function stop() {
if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
}
function p() {
return new Promise(function(s) {
(requestIdleCallback || requestAnimationFrame)(s);
function requestIdle() {
return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(resolve);
});
}
async function b(s) {
t.size > 30 && await p();
let u = [];
if (!(s instanceof Node)) return u;
for (let i of t.keys())
i === s || !(i instanceof Node) || s.contains(i) && u.push(i);
return u;
async function collectChildren(element) {
if (store.size > 30)
await requestIdle();
const out = [];
if (!isInstance(element, Node)) return out;
for (const el of store.keys()) {
if (el === element || !isInstance(el, Node)) continue;
if (element.contains(el))
out.push(el);
}
function l(s, u) {
let i = !1;
for (let h of s) {
if (u && b(h).then(l), !t.has(h)) continue;
let A = t.get(h);
A.length_c && (h.dispatchEvent(new Event(v)), A.connected = /* @__PURE__ */ new WeakSet(), A.length_c = 0, A.length_d || t.delete(h), i = !0);
return out;
}
return i;
function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
}
function E(s, u) {
let i = !1;
for (let h of s)
u && b(h).then(E), !(!t.has(h) || !t.get(h).length_d) && ((globalThis.queueMicrotask || setTimeout)(I(h)), i = !0);
return i;
return out;
}
function I(s) {
function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => {
s.isConnected || (s.dispatchEvent(new Event(w)), t.delete(s));
if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
};
}
}
// src/customElement.js
function wt(t, e, r = rt) {
let n = t.host || t;
O.push({
scope: n,
host: (...d) => d.length ? d.forEach((f) => f(n)) : n
}), typeof r == "function" && (r = r.call(n, n));
let o = n[x];
o || nt(n);
let c = e.call(n, r);
return o || n.dispatchEvent(new Event(v)), t.nodeType === 11 && typeof t.mode == "string" && n.addEventListener(w, _.observe(t), { once: !0 }), O.pop(), t.append(c);
function customElementRender(target, render, props = observedAttributes2) {
const custom_element = target.host || target;
scope.push({
scope: custom_element,
host: (...c) => c.length ? c.forEach((c2) => c2(custom_element)) : custom_element
});
if (typeof props === "function") props = props.call(custom_element, custom_element);
const is_lte = custom_element[keyLTE];
if (!is_lte) lifecyclesToEvents(custom_element);
const out = render.call(custom_element, props);
if (!is_lte) custom_element.dispatchEvent(new Event(evc));
if (target.nodeType === 11 && typeof target.mode === "string")
custom_element.addEventListener(evd, c_ch_o.observe(target), { once: true });
scope.pop();
return target.append(out);
}
function nt(t) {
return j(t.prototype, "connectedCallback", function(e, r, n) {
e.apply(r, n), r.dispatchEvent(new Event(v));
}), j(t.prototype, "disconnectedCallback", function(e, r, n) {
e.apply(r, n), (globalThis.queueMicrotask || setTimeout)(
() => !r.isConnected && r.dispatchEvent(new Event(w))
function lifecyclesToEvents(class_declaration) {
wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail) {
target.apply(thisArg, detail);
thisArg.dispatchEvent(new Event(evc));
});
wrapMethod(class_declaration.prototype, "disconnectedCallback", function(target, thisArg, detail) {
target.apply(thisArg, detail);
(globalThis.queueMicrotask || setTimeout)(
() => !thisArg.isConnected && thisArg.dispatchEvent(new Event(evd))
);
}), j(t.prototype, "attributeChangedCallback", function(e, r, n) {
let [o, , c] = n;
r.dispatchEvent(new CustomEvent(y, {
detail: [o, c]
})), e.apply(r, n);
}), t.prototype[x] = !0, t;
});
wrapMethod(class_declaration.prototype, "attributeChangedCallback", function(target, thisArg, detail) {
const [attribute, , value] = detail;
thisArg.dispatchEvent(new CustomEvent(eva, {
detail: [attribute, value]
}));
target.apply(thisArg, detail);
});
class_declaration.prototype[keyLTE] = true;
return class_declaration;
}
function j(t, e, r) {
t[e] = new Proxy(t[e] || (() => {
}), { apply: r });
function wrapMethod(obj, method, apply) {
obj[method] = new Proxy(obj[method] || (() => {
}), { apply });
}
function rt(t) {
return q(t, (e, r) => e.getAttribute(r));
function observedAttributes2(instance) {
return observedAttributes(instance, (i, n) => i.getAttribute(n));
}
// src/events.js
function yt(t, e, r) {
return e || (e = {}), function(o, ...c) {
r && (c.unshift(o), o = typeof r == "function" ? r() : r);
let d = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e);
return o.dispatchEvent(d);
function dispatchEvent(name, options, host) {
if (!options) options = {};
return function dispatch(element, ...d) {
if (host) {
d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
}
function D(t, e, r) {
return function(o) {
return o.addEventListener(t, e, r), o;
function on(event, listener, options) {
return function registerElement(element) {
element.addEventListener(event, listener, options);
return element;
};
}
var B = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 });
D.connected = function(t, e) {
return e = B(e), function(n) {
return n.addEventListener(v, t, e), n[x] ? n : n.isConnected ? (n.dispatchEvent(new Event(v)), n) : (L(e.signal, () => _.offConnected(n, t)) && _.onConnected(n, t), n);
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
on.connected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
};
};
D.disconnected = function(t, e) {
return e = B(e), function(n) {
return n.addEventListener(w, t, e), n[x] || L(e.signal, () => _.offDisconnected(n, t)) && _.onDisconnected(n, t), n;
on.disconnected = function(listener, options) {
options = lifeOptions(options);
return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
};
};
var W = /* @__PURE__ */ new WeakMap();
D.disconnectedAsAbort = function(t) {
if (W.has(t)) return W.get(t);
let e = new AbortController();
return W.set(t, e), t(D.disconnected(() => e.abort())), e;
var store_abort = /* @__PURE__ */ new WeakMap();
on.disconnectedAsAbort = function(host) {
if (store_abort.has(host)) return store_abort.get(host);
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a;
};
var ot = /* @__PURE__ */ new WeakSet();
D.attributeChanged = function(t, e) {
return typeof e != "object" && (e = {}), function(n) {
if (n.addEventListener(y, t, e), n[x] || ot.has(n) || !a.M) return n;
let o = new a.M(function(d) {
for (let { attributeName: f, target: p } of d)
p.dispatchEvent(
new CustomEvent(y, { detail: [f, p.getAttribute(f)] })
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {
if (typeof options !== "object")
options = {};
return function registerElement(element) {
element.addEventListener(eva, listener, options);
if (element[keyLTE] || els_attribute_store.has(element))
return element;
if (!enviroment.M) return element;
const observer = new enviroment.M(function(mutations) {
for (const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
);
});
return L(e.signal, () => o.disconnect()) && o.observe(n, { attributes: !0 }), n;
const c = onAbort(options.signal, () => observer.disconnect());
if (c) observer.observe(element, { attributes: true });
return element;
};
};
export {
R as assign,
U as assignAttribute,
J as chainableAppend,
K as classListDeclarative,
P as createElement,
pt as createElementNS,
wt as customElementRender,
nt as customElementWithDDE,
yt as dispatchEvent,
P as el,
pt as elNS,
Q as elementAttribute,
nt as lifecyclesToEvents,
rt as observedAttributes,
D as on,
dt as queue,
Z as registerReactivity,
O as scope,
lt as simulateSlots
assign,
assignAttribute,
chainableAppend,
classListDeclarative,
createElement,
createElementNS,
customElementRender,
lifecyclesToEvents as customElementWithDDE,
dispatchEvent,
createElement as el,
createElementNS as elNS,
elementAttribute,
lifecyclesToEvents,
observedAttributes2 as observedAttributes,
on,
queue,
registerReactivity,
scope,
simulateSlots
};

1
dist/esm.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,28 +1,179 @@
import { registerClientFile, styles } from "../ssr.js";
const host= "."+code.name;
styles.css`
/* Code block styling */
${host} {
--shiki-color-text: #e9eded;
--shiki-color-background: #212121;
/* Theme for dark mode - matches Flems/CodeMirror dark theme */
--shiki-color-text: #f8f8f2;
--shiki-color-background: var(--code-bg);
--shiki-token-constant: #82b1ff;
--shiki-token-string: #c3e88d;
--shiki-token-comment: #546e7a;
--shiki-token-keyword: #c792ea;
--shiki-token-parameter: #AA0000;
--shiki-token-parameter: #fd971f;
--shiki-token-function: #80cbae;
--shiki-token-string-expression: #c3e88d;
--shiki-token-punctuation: var(--code);
--shiki-token-link: #EE0000;
--shiki-token-punctuation: #89ddff;
--shiki-token-link: #82aaff;
--shiki-token-variable: #f8f8f2;
--shiki-token-number: #f78c6c;
--shiki-token-boolean: #82b1ff;
--shiki-token-tag: #f07178;
--shiki-token-attribute: #ffcb6b;
--shiki-token-property: #82b1ff;
--shiki-token-operator: #89ddff;
--shiki-token-regex: #c3e88d;
--shiki-token-class: #ffcb6b;
/* Basic styling */
white-space: pre;
tab-size: 2; /* TODO: allow custom tab size?! */
overflow: scroll;
tab-size: 2;
overflow: auto;
border-radius: var(--border-radius);
font-family: var(--font-mono);
font-size: 0.8rem;
line-height: 1.5;
position: relative;
margin-block: 1rem;
width: 100%;
}
/* Light mode overrides to match GitHub-like theme */
@media (prefers-color-scheme: light) {
${host} {
--shiki-color-text: #24292e;
--shiki-color-background: var(--code-bg);
--shiki-token-constant: #005cc5;
--shiki-token-string: #22863a;
--shiki-token-comment: #6a737d;
--shiki-token-keyword: #d73a49;
--shiki-token-parameter: #e36209;
--shiki-token-function: #6f42c1;
--shiki-token-string-expression: #22863a;
--shiki-token-punctuation: #24292e;
--shiki-token-link: #0366d6;
--shiki-token-variable: #24292e;
--shiki-token-number: #005cc5;
--shiki-token-boolean: #005cc5;
--shiki-token-tag: #22863a;
--shiki-token-attribute: #005cc5;
--shiki-token-property: #005cc5;
--shiki-token-operator: #d73a49;
--shiki-token-regex: #032f62;
--shiki-token-class: #6f42c1;
}
}
/* Support for theme toggles */
html[data-theme="light"] ${host} {
--shiki-color-text: #24292e;
--shiki-color-background: var(--code-bg);
--shiki-token-constant: #005cc5;
--shiki-token-string: #22863a;
--shiki-token-comment: #6a737d;
--shiki-token-keyword: #d73a49;
--shiki-token-parameter: #e36209;
--shiki-token-function: #6f42c1;
--shiki-token-string-expression: #22863a;
--shiki-token-punctuation: #24292e;
--shiki-token-link: #0366d6;
--shiki-token-variable: #24292e;
--shiki-token-number: #005cc5;
--shiki-token-boolean: #005cc5;
--shiki-token-tag: #22863a;
--shiki-token-attribute: #005cc5;
--shiki-token-property: #005cc5;
--shiki-token-operator: #d73a49;
--shiki-token-regex: #032f62;
--shiki-token-class: #6f42c1;
}
html[data-theme="dark"] ${host} {
--shiki-color-text: #f8f8f2;
--shiki-color-background: var(--code-bg);
--shiki-token-constant: #82b1ff;
--shiki-token-string: #c3e88d;
--shiki-token-comment: #546e7a;
--shiki-token-keyword: #c792ea;
--shiki-token-parameter: #fd971f;
--shiki-token-function: #80cbae;
--shiki-token-string-expression: #c3e88d;
--shiki-token-punctuation: #89ddff;
--shiki-token-link: #82aaff;
--shiki-token-variable: #f8f8f2;
--shiki-token-number: #f78c6c;
--shiki-token-boolean: #82b1ff;
--shiki-token-tag: #f07178;
--shiki-token-attribute: #ffcb6b;
--shiki-token-property: #82b1ff;
--shiki-token-operator: #89ddff;
--shiki-token-regex: #c3e88d;
--shiki-token-class: #ffcb6b;
}
/* Code block with syntax highlighting waiting for JS */
${host}[data-js=todo] {
border: 1px solid var(--border);
border-radius: var(--standard-border-radius);
margin-bottom: 1rem;
margin-top: 18.4px; /* to fix shift when → dataJS=done */
padding: 1rem 1.4rem;
border-radius: var(--border-radius);
padding: 1rem;
background-color: var(--code-bg);
position: relative;
}
/* Add a subtle loading indicator */
${host}[data-js=todo]::before {
content: "Loading syntax highlighting...";
position: absolute;
top: 0.5rem;
right: 0.5rem;
font-size: 0.75rem;
color: var(--text-light);
background-color: var(--bg);
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius);
opacity: 0.7;
}
/* All code blocks should have consistent font and sizing */
${host} code {
font-family: var(--font-mono);
font-size: inherit;
line-height: 1.5;
padding: 0;
}
${host} pre {
margin-block: 0;
font-size: inherit;
}
/* Ensure line numbers (if added) are styled appropriately */
${host} .line-number {
user-select: none;
opacity: 0.5;
margin-right: 1rem;
min-width: 1.5rem;
display: inline-block;
text-align: right;
}
/* If there's a copy button, style it */
${host} .copy-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background-color: var(--bg);
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--border-radius);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
}
${host}:hover .copy-button {
opacity: 1;
}
`;
import { el } from "deka-dom-el";
@ -52,12 +203,46 @@ let is_registered= {};
function registerClientPart(page_id){
if(is_registered[page_id]) return;
// Add Shiki with a more reliable loading method
document.head.append(
el("script", { src: "https://cdn.jsdelivr.net/npm/shiki@0.9", defer: true }),
// Use a newer version of Shiki with better performance
el("script", { src: "https://cdn.jsdelivr.net/npm/shiki@0.14.3/dist/index.unpkg.iife.js", defer: true }),
// Make sure we can match Flems styling in dark/light mode
el("style", `
/* Ensure CodeMirror and Shiki use the same font */
.CodeMirror *, .shiki * {
font-family: var(--font-mono) !important;
}
/* Style Shiki's output to match our theme */
.shiki {
background-color: var(--shiki-color-background) !important;
color: var(--shiki-color-text) !important;
padding: 1rem;
border-radius: var(--border-radius);
tab-size: 2;
}
/* Ensure Shiki code tokens use our CSS variables */
.shiki .keyword { color: var(--shiki-token-keyword) !important; }
.shiki .constant { color: var(--shiki-token-constant) !important; }
.shiki .string { color: var(--shiki-token-string) !important; }
.shiki .comment { color: var(--shiki-token-comment) !important; }
.shiki .function { color: var(--shiki-token-function) !important; }
.shiki .operator, .shiki .punctuation { color: var(--shiki-token-punctuation) !important; }
.shiki .parameter { color: var(--shiki-token-parameter) !important; }
.shiki .variable { color: var(--shiki-token-variable) !important; }
.shiki .property { color: var(--shiki-token-property) !important; }
`),
);
// Register our highlighting script to run after Shiki loads
const scriptElement = el("script", { type: "module" });
registerClientFile(
new URL("./code.js.js", import.meta.url),
el("script", { type: "module" })
scriptElement
);
is_registered[page_id]= true;
}

View File

@ -1,12 +1,61 @@
try {
// Initialize Shiki with our custom theme
const highlighter = await globalThis.shiki.getHighlighter({
theme: "css-variables",
langs: ["js", "ts", "css", "html", "shell"],
langs: ["javascript", "typescript", "css", "html", "shell"],
});
const codeBlocks= document.querySelectorAll('code[class*="language-"]');
// Find all code blocks that need highlighting
const codeBlocks = document.querySelectorAll('div[data-js="todo"] code[class*="language-"]');
// Process each code block
codeBlocks.forEach((block) => {
const lang= block.className.replace("language-", "");
try {
// Get the language from the class
const langClass = block.className.match(/language-(\w+)/);
// Map the language to Shiki format
let lang = langClass ? langClass[1] : 'javascript';
if (lang === 'js') lang = 'javascript';
if (lang === 'ts') lang = 'typescript';
// Mark the container as processed
block.parentElement.dataset.js = "done";
const html= highlighter.codeToHtml(block.textContent, { lang });
// Highlight the code
const code = block.textContent;
const html = highlighter.codeToHtml(code, { lang });
// Insert the highlighted HTML
block.innerHTML = html;
// Add copy button functionality
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-button';
copyBtn.textContent = 'Copy';
copyBtn.setAttribute('aria-label', 'Copy code to clipboard');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(code).then(() => {
copyBtn.textContent = 'Copied!';
setTimeout(() => {
copyBtn.textContent = 'Copy';
}, 2000);
});
});
// Add the copy button to the code block container
block.parentElement.appendChild(copyBtn);
} catch (err) {
console.error('Error highlighting code block:', err);
// Make sure we don't leave the block in a pending state
block.parentElement.dataset.js = "error";
}
});
} catch (err) {
console.error('Failed to initialize Shiki:', err);
// Fallback: at least mark blocks as processed so they don't show loading indicator
document.querySelectorAll('div[data-js="todo"]').forEach(block => {
block.dataset.js = "error";
});
}

View File

@ -6,11 +6,98 @@ ${host}{
width: 100%;
max-width: calc(9/5 * var(--body-max-width));
height: calc(3/5 * var(--body-max-width));
margin-inline: auto;
margin: 2rem auto;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
border: 1px solid var(--border);
}
${host} .runtime {
background-color: whitesmoke;
}
/* CodeMirror styling to match our theme */
.CodeMirror {
height: 100% !important;
font-family: var(--font-mono) !important;
font-size: 0.8rem !important;
line-height: 1.5 !important;
}
/* Dark mode styles for CodeMirror */
.CodeMirror, .CodeMirror-gutters {
background: #212121 !important;
border: 1px solid white;
background: var(--code-bg) !important;
border: 1px solid var(--border) !important;
color: var(--text) !important;
}
/* Light mode adjustments for CodeMirror - using CSS variables */
@media (prefers-color-scheme: light) {
/* Core syntax elements */
.cm-s-material .cm-keyword { color: var(--shiki-token-keyword, #d73a49) !important; }
.cm-s-material .cm-atom { color: var(--shiki-token-constant, #005cc5) !important; }
.cm-s-material .cm-number { color: var(--shiki-token-number, #005cc5) !important; }
.cm-s-material .cm-def { color: var(--shiki-token-function, #6f42c1) !important; }
.cm-s-material .cm-variable { color: var(--shiki-token-variable, #24292e) !important; }
.cm-s-material .cm-variable-2 { color: var(--shiki-token-variable, #24292e) !important; }
.cm-s-material .cm-variable-3 { color: var(--shiki-token-variable, #24292e) !important; }
.cm-s-material .cm-property { color: var(--shiki-token-property, #005cc5) !important; }
.cm-s-material .cm-operator { color: var(--shiki-token-operator, #d73a49) !important; }
.cm-s-material .cm-comment { color: var(--shiki-token-comment, #6a737d) !important; }
.cm-s-material .cm-string { color: var(--shiki-token-string, #22863a) !important; }
.cm-s-material .cm-string-2 { color: var(--shiki-token-string, #22863a) !important; }
.cm-s-material .cm-tag { color: var(--shiki-token-tag, #22863a) !important; }
.cm-s-material .cm-attribute { color: var(--shiki-token-attribute, #005cc5) !important; }
.cm-s-material .cm-bracket { color: var(--shiki-token-punctuation, #24292e) !important; }
.cm-s-material .cm-punctuation { color: var(--shiki-token-punctuation, #24292e) !important; }
.cm-s-material .cm-link { color: var(--shiki-token-link, #0366d6) !important; }
.cm-s-material .cm-error { color: #f44336 !important; }
}
/* Handle theme toggle */
html[data-theme="light"] .CodeMirror {
background: #f5f7fa !important;
}
html[data-theme="light"] .CodeMirror-gutters {
background: #f5f7fa !important;
border-right: 1px solid #e5e7eb !important;
}
/* Also apply the same styles to CodeMirror with data-theme */
html[data-theme="light"] .cm-s-material .cm-keyword { color: var(--shiki-token-keyword, #d73a49) !important; }
html[data-theme="light"] .cm-s-material .cm-atom { color: var(--shiki-token-constant, #005cc5) !important; }
html[data-theme="light"] .cm-s-material .cm-number { color: var(--shiki-token-number, #005cc5) !important; }
html[data-theme="light"] .cm-s-material .cm-def { color: var(--shiki-token-function, #6f42c1) !important; }
html[data-theme="light"] .cm-s-material .cm-variable { color: var(--shiki-token-variable, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-variable-2 { color: var(--shiki-token-variable, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-variable-3 { color: var(--shiki-token-variable, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-property { color: var(--shiki-token-property, #005cc5) !important; }
html[data-theme="light"] .cm-s-material .cm-operator { color: var(--shiki-token-operator, #d73a49) !important; }
html[data-theme="light"] .cm-s-material .cm-comment { color: var(--shiki-token-comment, #6a737d) !important; }
html[data-theme="light"] .cm-s-material .cm-string { color: var(--shiki-token-string, #22863a) !important; }
html[data-theme="light"] .cm-s-material .cm-string-2 { color: var(--shiki-token-string, #22863a) !important; }
html[data-theme="light"] .cm-s-material .cm-tag { color: var(--shiki-token-tag, #22863a) !important; }
html[data-theme="light"] .cm-s-material .cm-attribute { color: var(--shiki-token-attribute, #005cc5) !important; }
html[data-theme="light"] .cm-s-material .cm-bracket { color: var(--shiki-token-punctuation, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-punctuation { color: var(--shiki-token-punctuation, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-link { color: var(--shiki-token-link, #0366d6) !important; }
html[data-theme="light"] .cm-s-material .cm-error { color: #f44336 !important; }
/* Mobile adjustments */
@media (max-width: 767px) {
${host} {
height: 50vh;
max-width: 100%;
}
${host} main {
flex-grow: 1;
display: flex;
flex-direction: column;
}
${host} main > * {
width: 100%;
max-width: 100% !important;
}
}
`;

View File

@ -26,7 +26,7 @@ function ddeComponent({ attr }){
on.connected(e=> console.log(( /** @type {HTMLParagraphElement} */ (e.target)).outerHTML)),
);
return el().append(
el("p", S(()=> `Hello from Custom Element with attribute '${attr()}'`))
el("p", S(()=> `Hello from Custom Element with attribute '${attr.get()}'`))
);
}
customElementWithDDE(HTMLCustomElement);

View File

@ -0,0 +1,14 @@
// Debugging a (derived) signal with `console.log`
import { S } from "deka-dom-el/signals";
const name= S("Alice");
const greeting = S(() => {
// log derived signals
const log = "Hello, " + name.get();
console.log(log);
return log;
});
// log signals in general
S.on(greeting, value => console.log("Greeting changed to:", value));
name.set("Bob"); // Should trigger computation and listener`)

View File

@ -0,0 +1,15 @@
import { S } from "deka-dom-el/signals";
// Debouncing signal updates
function debounce(func, wait) {
let timeout;
return (...args)=> {
clearTimeout(timeout);
timeout= setTimeout(() => func(...args), wait);
};
}
const inputSignal= S("");
const debouncedSet= debounce(value => inputSignal.set(value), 300);
// In your input handler
inputElement.addEventListener("input", e=> debouncedSet(e.target.value));

View File

@ -0,0 +1,4 @@
// Example of reactive element marker
<!--<dde:mark type=\"reactive\" source=\"...\">-->
<!-- content that updates when signal changes -->
<!--</dde:mark>-->

View File

@ -0,0 +1,15 @@
import { S } from "deka-dom-el/signals";
// Wrong - direct mutation doesn't trigger updates
const todos1 = S([{ text: "Learn signals", completed: false }]);
todos1.get().push({ text: "Debug signals", completed: false }); // Won't trigger updates!
// Correct - using .set() with a new array
todos1.set([...todos1.get(), { text: "Debug signals", completed: false }]);
// Better - using actions
const todos2 = S([], {
add(text) {
this.value.push({ text, completed: false });
}
});
S.action(todos2, "add", "Debug signals");

View File

@ -4,11 +4,11 @@ const threePS= ({ emoji= "🚀" })=> {
const clicks= S(0); // A
return el().append(
el("p", S(()=>
"Hello World "+emoji.repeat(clicks()) // B
"Hello World "+emoji.repeat(clicks.get()) // B
)),
el("button", {
type: "button",
onclick: ()=> clicks(clicks()+1), // C
onclick: ()=> clicks.set(clicks.get()+1), // C
textContent: "Fire",
})
);

View File

@ -22,9 +22,9 @@ const onsubmit= on("submit", function(event){
S.action(todos, "push", data.get("todo"));
break;
case "E"/*dit*/: {
const last= todos().at(-1);
const last= todos.get().at(-1);
if(!last) break;
last(data.get("todo"));
last.set(data.get("todo"));
break;
}
case "R"/*emove*/:

View File

@ -1,15 +1,15 @@
import { S } from "deka-dom-el/signals";
const signal= S(0);
// computation pattern
const double= S(()=> 2*signal());
const double= S(()=> 2*signal.get());
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);
signal.set(signal.get()+1);
const interval= 5 * 1000;
const id= setInterval(()=> signal(signal()+1), interval);
const id= setInterval(()=> signal.set(signal.get()+1), interval);
ac.signal.addEventListener("abort",
()=> setTimeout(()=> clearInterval(id), 2*interval));

View File

@ -0,0 +1,20 @@
import { S } from "deka-dom-el/signals";
// Debugging a derived signal
const name = S('Alice');
const greeting = S(() => {
console.log('Computing greeting...');
return 'Hello, ' + name.get();
});
// Monitor the derived signal
S.on(greeting, value => console.log('Greeting changed to:', value));
// Later update the dependency
name.set('Bob'); // Should trigger computation and listener
// Console output:
// Computing greeting...
// Greeting changed to: Hello, Alice
// Computing greeting...
// Greeting changed to: Hello, Bob

View File

@ -0,0 +1,38 @@
import { el, on, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
// Create a component with reactive elements
function ReactiveCounter() {
const count = S(0);
scope.host(on.connected(ev=>
console.log(ev.target.__dde_reactive)
));
const counter = el('div', {
// This element will be added into the __dde_reactive property
textContent: count,
});
const incrementBtn = el('button', {
textContent: 'Increment',
onclick: () => count.set(count.get() + 1)
});
// Dynamic section will be added into __dde_signal property
const counterInfo = S.el(count, value =>
el('p', `Current count is ${value}`)
);
return el('div', { id: 'counter' }).append(
counter,
incrementBtn,
counterInfo
);
}
document.body.append(
el(ReactiveCounter),
);
// In DevTools console:
const counter = document.querySelector('#counter');
setTimeout(()=> console.log(counter.__dde_reactive), 1000); // See reactive bindings

View File

@ -0,0 +1,13 @@
import { S } from "deka-dom-el/signals";
// Create base signals
const firstName = S("John");
const lastName = S("Doe");
// Create a derived signal
const fullName = S(() => firstName.get() + " " + lastName.get());
// The fullName signal updates automatically when either dependency changes
S.on(fullName, name => console.log("Name changed to:", name));
firstName.set("Jane"); // logs: "Name changed to: Jane Doe"

View File

@ -3,8 +3,8 @@ const count= S(0);
import { el } from "deka-dom-el";
document.body.append(
el("p", S(()=> "Currently: "+count())),
el("p", { classList: { red: S(()=> count()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }),
el("p", S(()=> "Currently: "+count.get())),
el("p", { classList: { red: S(()=> count.get()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }),
);
document.head.append(
el("style", ".red { color: red; }")
@ -12,4 +12,4 @@ document.head.append(
const interval= 5 * 1000;
setTimeout(clearInterval, 10*interval,
setInterval(()=> count(count()+1), interval));
setInterval(()=> count.set(count.get()+1), interval));

View File

@ -2,7 +2,7 @@ import { S } from "deka-dom-el/signals";
const count= S(0, {
add(){ this.value= this.value + Math.round(Math.random()*10); }
});
const numbers= S([ count() ], {
const numbers= S([ count.get() ], {
push(next){ this.value.push(next); }
});
@ -22,5 +22,5 @@ document.body.append(
const interval= 5*1000;
setTimeout(clearInterval, 10*interval, setInterval(function(){
S.action(count, "add");
S.action(numbers, "push", count());
S.action(numbers, "push", count.get());
}, interval));

View File

@ -4,7 +4,7 @@ const signal= S(0);
// β — just reacts on signal changes
S.on(signal, console.log);
// γ — just updates the value
const update= ()=> signal(signal()+1);
const update= ()=> signal.set(signal.get()+1);
update();
const interval= 5*1000;

View File

@ -0,0 +1,43 @@
// Handling async data in SSR
import { JSDOM } from "jsdom";
import { S } from "deka-dom-el/signals";
import { register, queue } from "deka-dom-el/jsdom";
async function renderWithAsyncData() {
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
const { el } = await register(dom);
// Create a component that fetches data
function AsyncComponent() {
const title= S("-");
const description= S("-");
// Use the queue to track the async operation
queue(fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => {
title.set(data.title);
description.set(data.description);
}));
return el("div", { className: "async-content" }).append(
el("h2", title),
el("p", description)
);
}
// Render the page
dom.window.document.body.append(
el("h1", "Page with Async Data"),
el(AsyncComponent)
);
// IMPORTANT: Wait for all queued operations to complete
await queue();
// Now the HTML includes all async content
const html = dom.serialize();
console.log(html);
}
renderWithAsyncData();

View File

@ -0,0 +1,47 @@
// Basic SSR Example
import { JSDOM } from "jsdom";
import { register, queue } from "deka-dom-el/jsdom";
import { writeFileSync } from "node:fs";
async function renderPage() {
// Create a jsdom instance
const dom = new JSDOM("<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head><body></body></html>");
// Register with deka-dom-el and get the el function
const { el } = await register(dom);
// Create a simple header component
function Header({ title }) {
return el("header").append(
el("h1", title),
el("nav").append(
el("ul").append(
el("li").append(el("a", { href: "/" }, "Home")),
el("li").append(el("a", { href: "/about" }, "About")),
el("li").append(el("a", { href: "/contact" }, "Contact"))
)
)
);
}
// Create the page content
dom.window.document.body.append(
el(Header, { title: "My Static Site" }),
el("main").append(
el("h2", "Welcome!"),
el("p", "This page was rendered with deka-dom-el on the server.")
),
el("footer", "© 2025 My Company")
);
// Wait for any async operations
await queue();
// Get the HTML and write it to a file
const html = dom.serialize();
writeFileSync("index.html", html);
console.log("Page rendered successfully!");
}
renderPage().catch(console.error);

View File

@ -0,0 +1,2 @@
// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js
import { register, unregister, queue } from "deka-dom-el/jsdom";

View File

@ -0,0 +1,35 @@
// ❌ WRONG: Static imports are hoisted and will register before JSDOM is created
import { register } from "deka-dom-el/jsdom";
import { el } from "deka-dom-el";
import { Header } from "./components/Header.js";
// ✅ CORRECT: Use dynamic imports to ensure proper initialization order
import { JSDOM } from "jsdom";
async function renderPage() {
// 1. Create JSDOM instance first
const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`);
// 2. Dynamically import jsdom module
const { register, queue } = await import("deka-dom-el/jsdom");
// 3. Register and get el function
const { el } = await register(dom);
// 4. Dynamically import page components
const { Header } = await import("./components/Header.js");
const { Content } = await import("./components/Content.js");
// 5. Render components
const body = dom.window.document.body;
el(body).append(
el(Header, { title: "My Page" }),
el(Content, { text: "This is server-rendered content" })
);
// 6. Wait for async operations
await queue();
// 7. Get HTML and clean up
return dom.serialize();
}

View File

@ -0,0 +1,27 @@
// Basic jsdom integration example
import { JSDOM } from "jsdom";
import { register, unregister, queue } from "deka-dom-el/jsdom.js";
// Create a jsdom instance
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
// Register the dom with deka-dom-el
const { el } = await register(dom);
// Use deka-dom-el normally
dom.window.document.body.append(
el("div", { className: "container" }).append(
el("h1", "Hello, SSR World!"),
el("p", "This content was rendered on the server.")
)
);
// Wait for any async operations to complete
await queue();
// Get the rendered HTML
const html = dom.serialize();
console.log(html);
// Clean up when done
unregister();

View File

@ -0,0 +1,44 @@
// Building a simple static site generator
import { JSDOM } from "jsdom";
import { register, queue } from "deka-dom-el/jsdom";
import { writeFileSync, mkdirSync } from "node:fs";
async function buildSite() {
// Define pages to build
const pages = [
{ id: "index", title: "Home", component: "./pages/home.js" },
{ id: "about", title: "About", component: "./pages/about.js" },
{ id: "docs", title: "Documentation", component: "./pages/docs.js" }
];
// Create output directory
mkdirSync("./dist", { recursive: true });
// Build each page
for (const page of pages) {
// Create a fresh jsdom instance for each page
const dom = new JSDOM("<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head><body></body></html>");
// Register with deka-dom-el
const { el } = await register(dom);
// Import the page component
const { default: PageComponent } = await import(page.component);
// Render the page with its metadata
dom.window.document.body.append(
el(PageComponent, { title: page.title, pages })
);
// Wait for any async operations
await queue();
// Write the HTML to a file
const html = dom.serialize();
writeFileSync(`./dist/${page.id}.html`, html);
console.log(`Built page: ${page.id}.html`);
}
}
buildSite().catch(console.error);

View File

@ -1,18 +1,67 @@
import { pages, styles } from "../ssr.js";
const host= "."+prevNext.name;
styles.css`
/* Previous/Next navigation */
${host} {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
margin-top: 1rem;
display: flex;
justify-content: space-between;
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
gap: 1rem;
width: 100%;
}
${host} a {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: var(--border-radius);
background-color: var(--primary-dark); /* Darker background for better contrast */
color: white;
font-weight: 600; /* Bolder text for better readability */
text-decoration: none;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
max-width: 45%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); /* Subtle shadow for better visibility */
}
${host} a:hover {
background-color: var(--primary);
transform: translateY(-2px);
text-decoration: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Enhanced shadow on hover */
}
${host} [rel=prev] {
grid-column: 1;
margin-right: auto;
}
${host} [rel=next] {
grid-column: 3;
text-align: right;
margin-left: auto;
}
${host} [rel=prev]::before {
content: "←";
margin-right: 0.75rem;
font-size: 1.2em;
}
${host} [rel=next]::after {
content: "→";
margin-left: 0.75rem;
font-size: 1.2em;
}
/* If there's no previous/next, ensure the spacing still works */
${host} a:only-child {
margin-left: auto;
}
@media (max-width: 640px) {
${host} a {
font-size: 0.9rem;
}
}
`;
import { el } from "../../index.js";
@ -24,8 +73,15 @@ import { el } from "../../index.js";
export function h3({ textContent, id }){
if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, "");
return el("h3", { id }).append(
el("a", { textContent: "#", href: "#"+id, tabIndex: -1 }),
" ", textContent
el("a", {
className: "heading-anchor",
href: "#"+id,
textContent: "#",
title: `Link to this section: ${textContent}`,
"aria-label": `Link to section ${textContent}`
}),
" ",
textContent,
);
}
/**
@ -46,9 +102,20 @@ export function prevNext(page){
function pageLink({ rel, page }){
if(!page) return el();
let { href, title, description }= page;
return el("a", { rel, href, title: description }).append(
rel==="next" ?"(next) " : "",
title,
rel==="prev" ? " (previous)" : "",
// Find the page index to show numbering
const pageIndex = pages.findIndex(p => p === page);
const pageNumber = pageIndex + 1;
const linkTitle = rel === "prev"
? `Previous: ${pageNumber}. ${title}`
: `Next: ${pageNumber}. ${title}`;
return el("a", {
rel,
href,
title: description || linkTitle
}).append(
title
);
}

View File

@ -1,124 +1,420 @@
import { styles } from "./ssr.js";
styles.css`
@import url(https://cdn.simplecss.org/simple.min.css);
:root {
--body-max-width: 45rem;
--marked: #fb3779;
--code: #0d47a1;
--accent: #d81b60;
--primary: #b71c1c;
--primary-light: #f05545;
--primary-dark: #7f0000;
--primary-rgb: 183, 28, 28;
--secondary: #700037;
--secondary-light: #ae1357;
--secondary-dark: #4a0027;
--secondary-rgb: 112, 0, 55;
--font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Code', 'JetBrains Mono', 'SF Mono',
SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
--body-max-width: 40rem;
--sidebar-width: 20rem;
--header-height: 4rem;
--border-radius: 0.375rem;
--bg: #ffffff;
--bg-sidebar: #fff5f5;
--text: #1a1313;
--text-light: #555050;
--code-bg: #f9f2f2;
--code-text: #9a0000;
--border: #d8c0c0;
--selection: rgba(183, 28, 28, 0.15);
--marked: #b71c1c;
--accent: var(--secondary);
--shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
--link-color: #9a0000;
--link-hover: #7f0000;
--button-text: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--accent: #f06292;
--code: #62c1f0;
--bg: #121212;
--bg-sidebar: #1a1212;
--text: #ffffff;
--text-light: #cccccc;
--code-bg: #2c2020;
--code-text: #ff9e80;
--border: #4d3939;
--selection: rgba(255, 99, 71, 0.25);
--primary: #b74141;
--primary-light: #ff867f;
--primary-dark: #c62828;
--secondary: #f02b47;
--secondary-light: #ff6090;
--secondary-dark: #b0003a;
--accent: var(--secondary);
--link-color: #ff5252;
--link-hover: #ff867f;
--button-text: #ffffff;
--nav-current-bg: #aa2222;
--nav-current-text: #ffffff;
--primary-rgb: 255, 82, 82;
--secondary-rgb: 233, 30, 99;
}
}
body {
grid-template-columns: 100%;
grid-template-areas: "header" "sidebar" "content";
/* Base styling */
* {
box-sizing: border-box;
}
@media (min-width:768px) {
body{
grid-template-rows: auto auto;
grid-template-columns: calc(10 * var(--body-max-width) / 27) auto;
grid-template-areas:
"header header"
"sidebar content"
html {
scroll-behavior: smooth;
}
/* Accessibility improvements */
:focus {
outline: 3px solid rgba(63, 81, 181, 0.5);
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 3px solid rgba(63, 81, 181, 0.5);
outline-offset: 2px;
}
/* Ensure reduced motion preferences are respected */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
body > *{
grid-column: unset;
}
body > header{
grid-area: header;
padding: 0;
}
body > nav{
grid-area: sidebar;
background-color: var(--accent-bg);
display: flex;
flex-flow: column nowrap;
}
body > nav {
font-size: 1rem;
line-height: 2;
padding: 1rem 0 0 0;
}
body > nav ol,
body > nav ul {
align-content: space-around;
align-items: center;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
list-style-type: none;
margin: 0;
padding: 0;
}
body > nav ol li,
body > nav ul li {
display:inline-block
}
body > nav a,
body > nav a:visited {
margin: 0 .5rem 1rem .5rem;
border: 1px solid currentColor;
border-radius: var(--standard-border-radius);
color: var(--text-light);
display: inline-block;
padding: .1rem 1rem;
/* Skip link for better keyboard navigation */
.skip-link {
position: absolute;
top: 0;
left: 0;
transform: translateX(-100%);
z-index: 9999;
background-color: var(--primary);
color: white;
padding: 0.5rem 1rem;
text-decoration: none;
cursor: pointer;
transition: all .15s;
transition: transform 0.3s ease-in-out;
}
body > nav a.current,
body > nav a[aria-current=page] {
.skip-link:focus {
transform: translateX(0);
}
body {
font-family: var(--font-main);
background-color: var(--bg);
color: var(--text);
line-height: 1.6;
font-size: 1rem;
display: grid;
grid-template-columns: 100%;
grid-template-areas:
"header"
"sidebar"
"content";
min-height: 100vh;
margin: 0;
}
body > nav a:hover{
background-color: var(--bg);
color: var(--accent);
::selection {
background-color: var(--selection);
}
@media only screen and (max-width:720px) {
body > nav{
flex-flow: row wrap;
padding-block: .5rem;
/* Typography */
h1, h2, h3, h4, h5, h6 {
margin-bottom: 1rem;
margin-top: 2rem;
font-weight: 700;
line-height: 1.25;
color: var(--text);
}
body > nav a {
border:none;
text-decoration:underline;
margin-block: .1rem;
padding-block:.1rem;
line-height: 1rem;
font-size: .9rem;
h1 {
font-size: 2.25rem;
margin-top: 0;
color: var(--primary-dark);
}
h1 > a {
font-weight: unset;
color: unset;
}
h3 {
font-size: 1.25rem;
color: var(--secondary);
}
p {
margin-bottom: 1.5rem;
}
a {
color: var(--link-color, var(--primary));
transition: color 0.2s ease;
font-weight: 500;
text-underline-offset: 3px;
transition: color 0.2s ease, text-underline-offset 0.2s ease;
}
a:visited {
--link-color: var(--secondary, #700037);
}
a:hover {
--link-color: var(--link-hover, var(--primary-light));
text-underline-offset: 5px;
}
code, pre {
font-family: var(--font-mono);
font-size: 0.9em;
border-radius: var(--border-radius);
}
code {
background-color: var(--code-bg);
color: var(--code-text);
padding: 0.2em 0.4em;
}
pre {
background-color: var(--code-bg);
padding: 1rem;
overflow-x: auto;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
}
pre code {
background-color: transparent;
padding: 0;
}
/* Layout */
@media (min-width: 768px) {
body {
grid-template-rows: var(--header-height) 1fr;
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-areas:
"header header"
"sidebar content";
}
}
main{
/* Main content */
body > main {
grid-area: content;
padding: 2rem;
max-width: 100%;
overflow-x: hidden;
display: grid;
grid-template-columns:
[full-main-start] 1fr
[main-start] min(var(--body-max-width), 90%) [main-end]
1fr [full-main-end];
}
main > *, main slot > *{
body > main > *, body > main slot > * {
width: 100%;
max-width: 100%;
margin-inline: auto;
grid-column: main;
}
/* Page title with ID anchor for skip link */
body > main .page-title {
margin-top: 0;
border-bottom: 1px solid var(--border);
padding-bottom: 0.75rem;
margin-bottom: 1.5rem;
color: var(--primary);
position: relative;
}
/* Section headings with better visual hierarchy */
body > main h2, body > main h3 {
scroll-margin-top: calc(var(--header-height) + 1rem);
position: relative;
}
body > main h3 {
border-left: 3px solid var(--primary);
position: relative;
left: -1.5rem;
padding-inline-start: 1em;
}
/* Make clickable heading links for better navigation */
.heading-anchor {
position: absolute;
color: var(--text-light);
left: -2rem;
text-decoration: none;
font-weight: normal;
opacity: 0;
transition: opacity 0.2s;
}
h2:hover .heading-anchor,
h3:hover .heading-anchor {
opacity: 0.8;
}
@media (max-width: 767px) {
body > main {
padding: 1.5rem 1rem;
}
body > main h2, body > main h3 {
left: 1rem;
width: calc(100% - 1rem);
}
.heading-anchor {
opacity: 0.4;
}
}
/* Example boxes */
.example {
border: 1px solid var(--border);
border-radius: var(--border-radius);
margin: 2rem 0;
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s;
}
.example:hover {
box-shadow: var(--shadow);
}
.example-header {
background-color: var(--bg-sidebar);
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.example-content {
padding: 1.25rem;
}
/* Icon styling */
.icon {
vertical-align: sub;
padding-right: .25rem;
display: inline-block;
width: 1em;
height: 1.3em;
margin-right: 0.2rem;
height: 1em;
vertical-align: -0.125em;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
/* Information blocks */
.note, .tip, .warning {
padding: 1rem 1.25rem;
margin: 1.5rem 0;
border-radius: var(--border-radius);
position: relative;
font-size: 0.95rem;
line-height: 1.5;
}
.note {
font-size: .9rem;
font-style: italic;
background-color: rgba(63, 81, 181, 0.08);
border-left: 4px solid var(--primary);
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.tip {
background-color: rgba(46, 204, 113, 0.08);
border-left: 4px solid #2ecc71;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.warning {
background-color: rgba(241, 196, 15, 0.08);
border-left: 4px solid #f1c40f;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.note::before, .tip::before, .warning::before {
font-weight: 600;
display: block;
margin-bottom: 0.5rem;
}
.note::before {
content: "Note";
color: var(--primary);
}
.tip::before {
content: "Tip";
color: #2ecc71;
}
.warning::before {
content: "Warning";
color: #f1c40f;
}
/* Prev/Next buttons */
.prev-next {
display: flex;
justify-content: space-between;
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.prev-next a {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
background-color: var(--primary);
color: white;
transition: background-color 0.2s ease;
}
.prev-next a:hover {
background-color: var(--primary-dark);
text-decoration: none;
}
.prev-next a:empty {
display: none;
}
.prev-next a[rel="prev"]::before {
content: "←";
margin-right: 0.5rem;
}
.prev-next a[rel="next"]::after {
content: "→";
margin-left: 0.5rem;
}
`;

View File

@ -1,38 +1,242 @@
import { el, elNS } from "deka-dom-el";
import { pages } from "../ssr.js";
import { pages, styles } from "../ssr.js";
const host= "."+header.name;
const host_nav= "."+nav.name;
styles.css`
/* Header */
${host} {
grid-area: header;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
background-color: var(--primary);
color: white;
box-shadow: var(--shadow);
min-height: calc(var(--header-height) - 1em);
--_m: .75em;
margin: var(--_m) var(--_m) 0 var(--_m);
border-radius: var(--border-radius);
}
${host} .header-title {
display: flex;
align-items: center;
gap: 0.75rem;
}
${host} h1 {
font-size: 1.25rem;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: white;
}
${host} .version-badge {
font-size: 0.75rem;
background-color: rgba(150, 150, 150, 0.2);
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius);
}
${host} p {
display: block;
font-size: 0.875rem;
opacity: 0.9;
margin: 0;
}
${host} .github-link {
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
border-radius: var(--border-radius);
background-color: rgba(0, 0, 0, 0.2);
text-decoration: none;
transition: background-color 0.2s;
}
${host} .github-link:hover {
background-color: rgba(0, 0, 0, 0.3);
text-decoration: none;
}
/* Navigation */
${host_nav} {
grid-area: sidebar;
background-color: var(--bg-sidebar);
border-right: 1px solid var(--border);
padding: 1.5rem 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
${host_nav} a {
display: flex;
align-items: center;
padding: 0.625rem 0.75rem;
border-radius: var(--border-radius);
color: var(--text);
text-decoration: none;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
line-height: 1.2;
}
${host_nav} a:hover {
background-color: rgba(var(--primary-rgb), 0.08); /* Using CSS variables for better theming */
text-decoration: none;
transform: translateY(-1px);
color: var(--primary);
}
${host_nav} a.current,
${host_nav} a[aria-current=page] {
background-color: var(--nav-current-bg, var(--primary-dark));
color: var(--nav-current-text, white);
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
}
${host_nav} a.current:hover,
${host_nav} a[aria-current=page]:hover {
background-color: var(--primary);
color: white;
transform: translateY(-1px);
}
${host_nav} a .nav-number {
display: inline-block;
width: 1.5rem;
text-align: right;
margin-right: 0.5rem;
opacity: 0.7;
}
${host_nav} a:first-child {
display: flex;
align-items: center;
font-weight: 600;
margin-bottom: 0.5rem;
}
/* Mobile navigation */
@media (max-width: 767px) {
${host_nav} {
padding: 0.75rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
border-bottom: 1px solid var(--border);
border-right: none;
justify-content: center;
}
${host_nav} a {
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
white-space: nowrap;
}
${host_nav} a .nav-number {
width: auto;
margin-right: 0.25rem;
}
${host_nav} a:first-child {
margin-bottom: 0;
margin-right: 0.5rem;
min-width: 100%;
justify-content: center;
}
}
`;
/**
* @param {object} def
* @param {import("../types.d.ts").Info} def.info
* @param {import("../types.d.ts").Pkg} def.pkg Package information.
* */
export function header({ info: { href, title, description }, pkg }){
title= `\`${pkg.name}\`${title}`;
const pageTitle = `${pkg.name}${title}`;
// Add meta elements to the head
document.head.append(
head({ title, description, pkg })
head({ title: pageTitle, description, pkg })
);
// Add theme color meta tag
document.head.append(
el("meta", { name: "theme-color", content: "#3f51b5" })
);
return el().append(
el("header").append(
el("h1", title),
el("p", description)
),
el("nav").append(
el("a", { href: pkg.homepage }).append(
// Header section with accessibility support
el("header", { role: "banner", className: header.name }).append(
el("div", { className: "header-title" }).append(
el("a", {
href: pkg.homepage,
className: "github-link",
"aria-label": "View on GitHub",
target: "_blank",
rel: "noopener noreferrer"
}).append(
el(iconGitHub),
"GitHub"
),
...pages.map((p, i)=> el("a", {
href: p.href==="index" ? "./" : p.href,
textContent: (i+1) + ". " + p.title,
title: p.description,
classList: { current: p.href===href }
}))
)
el("h1").append(
el("a", { href: pages[0].href, textContent: pkg.name, title: "Go to documentation homepage" }),
),
el("span", {
className: "version-badge",
"aria-label": "Version",
textContent: pkg.version || ""
})
),
el("p", description),
),
// Navigation between pages
nav({ href })
);
}
function nav({ href }){
return el("nav", {
role: "navigation",
"aria-label": "Main navigation",
className: nav.name
}).append(
...pages.map((p, i) => {
const isIndex = p.href === "index";
const isCurrent = p.href === href;
return el("a", {
href: isIndex ? "./" : p.href,
title: p.description || `Go to ${p.title}`,
"aria-current": isCurrent ? "page" : null,
classList: { current: isCurrent }
}).append(
el("span", {
className: "nav-number",
"aria-hidden": "true",
textContent: `${i+1}.`
}),
p.title
);
})
);
}
function head({ title, description, pkg }){
return el().append(
el("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
el("meta", { name: "description", content: description }),
el("meta", { name: "theme-color", content: "#b71c1c" }),
el("title", title),
el(metaAuthor),
el(metaTwitter, pkg),

View File

@ -7,9 +7,25 @@ import { prevNext } from "../components/pageUtils.html.js";
/** @param {Pick<import("../types.d.ts").PageAttrs, "pkg" | "info">} attrs */
export function simplePage({ pkg, info }){
return simulateSlots(el().append(
// Skip link for keyboard navigation
el("a", {
href: "#main-content",
className: "skip-link",
textContent: "Skip to main content"
}),
// Header with site information
el(header, { info, pkg }),
el("main").append(
// Main content area
el("main", { id: "main-content", role: "main" }).append(
// Page title as an h1
el("h1", { className: "page-title", textContent: info.title }),
// Main content from child elements
el("slot"),
// Navigation between pages
el(prevNext, info)
)
));

View File

@ -1,7 +1,7 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Signals and reactivity`,
description: t`Handling reactivity in UI via signals.`,
title: t`Signals and Reactivity`,
description: t`Managing reactive UI state with signals.`,
};
import { el } from "deka-dom-el";
@ -43,86 +43,243 @@ const references= {
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("h2", t`Using signals to manage reactivity`),
el("h2", t`Building Reactive UIs with Signals`),
el("p").append(...T`
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.
Signals provide a simple yet powerful way to create reactive applications with DDE. They handle the
fundamental challenge of keeping your UI in sync with changing data in a declarative, efficient way.
`),
el("div", { class: "dde-callout" }).append(
el("h4", t`What Makes Signals Special?`),
el("ul").append(
el("li", t`Fine-grained reactivity without complex state management`),
el("li", t`Automatic UI updates when data changes`),
el("li", t`Clean separation between data, logic, and UI`),
el("li", t`Small runtime with minimal overhead`),
el("li", t`Works seamlessly with DDE's DOM creation`),
el("li", t`No dependencies or framework lock-in`)
)
),
el(code, { src: fileURL("./components/examples/signals/intro.js"), page_id }),
el(h3, t`Introducing signals`),
el(h3, t`The 3-Part Structure of Signals`),
el("p").append(...T`
Lets re-introduce
${el("a", { textContent: t`3PS principle`, href: "./#h-event-driven-programming--parts-separation--ps" })}.
`),
el("p").append(...T`
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.
Signals organize your code into three distinct parts, following the
${el("a", { textContent: t`3PS principle`, href: "./#h-event-driven-programming--parts-separation--ps" })}:
`),
el("div", { class: "dde-signal-diagram" }).append(
el("div", { class: "signal-part" }).append(
el("h4", t`α: Create Signal`),
el(code, { content: "const count = S(0);", page_id }),
el("p", t`Define a reactive value that can be observed and changed`)
),
el("div", { class: "signal-part" }).append(
el("h4", t`β: React to Changes`),
el(code, { content: "S.on(count, value => updateUI(value));", page_id }),
el("p", t`Subscribe to signal changes with callbacks or effects`)
),
el("div", { class: "signal-part" }).append(
el("h4", t`γ: Update Signal`),
el(code, { content: "count.set(count.get() + 1);", page_id }),
el("p", t`Modify the signal value, which automatically triggers updates`)
)
),
el(example, { src: fileURL("./components/examples/signals/signals.js"), page_id }),
el("div", { class: "dde-note" }).append(
el("p").append(...T`
All this is just an example of
${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven })} and
${el("a", { textContent: t`Publishsubscribe pattern`, ...references.wiki_pubsub })} (compare for example
with ${el("a", { textContent: t`fpubsub library`, ...references.fpubsub })}). All three parts can be in
some manner independent and still connected to the same reactive entity.
Signals implement the ${el("a", { textContent: t`Publishsubscribe pattern`, ...references.wiki_pubsub })},
a form of ${el("a", { textContent: t`Event-driven programming`, ...references.wiki_event_driven })}.
This architecture allows different parts of your application to stay synchronized through a shared signal,
without direct dependencies on each other.
`)
),
el(h3, t`Signal Essentials: Core API`),
el("div", { class: "dde-function-table" }).append(
el("dl").append(
el("dt", t`Creating a Signal`),
el("dd", t`S(initialValue) → creates a signal with the given value`),
el("dt", t`Reading a Signal`),
el("dd", t`signal.get() → returns the current value`),
el("dt", t`Writing to a Signal`),
el("dd", t`signal.set(newValue) → updates the value and notifies subscribers`),
el("dt", t`Subscribing to Changes`),
el("dd", t`S.on(signal, callback) → runs callback whenever signal changes`),
el("dt", t`Unsubscribing`),
el("dd", t`S.on(signal, callback, { signal: abortController.signal })`)
)
),
el("p").append(...T`
Signals can be created with any type of value, but they work best with
${el("a", { textContent: t`primitive types`, ...references.mdn_primitive })} like strings, numbers, and booleans.
For complex data types like objects and arrays, you'll want to use Actions (covered below).
`),
el(h3, t`Derived Signals: Computed Values`),
el("p").append(...T`
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('${t`a new value`}')`)}. For listenning the signal value changes, use
${el("code", "S.on(signal, console.log)")}.
Computed values (also called derived signals) automatically update when their dependencies change.
Create them by passing a function to S():
`),
el(example, { src: fileURL("./components/examples/signals/derived.js"), page_id }),
el("p").append(...T`
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 signal):
Derived signals are read-only - you can't call .set() on them. Their value is always computed
from their dependencies. They're perfect for transforming or combining data from other signals.
`),
el(example, { src: fileURL("./components/examples/signals/computations-abort.js"), page_id }),
el(h3, t`Signals and actions`),
el(h3, t`Signal Actions: For Complex State`),
el("p").append(...T`
${el("code", `S(/* ${t`primitive`} */)`)} allows you to declare simple reactive variables, typically, around
${el("em", t`immutable`)} ${el("a", { textContent: t`primitive types`, ...references.mdn_primitive })}.
However, it may also be necessary to use reactive arrays, objects, or other complex reactive structures.
When working with objects, arrays, or other complex data structures, Signal Actions provide
a structured way to modify state while maintaining reactivity.
`),
el("div", { class: "dde-illustration" }).append(
el("h4", t`Actions vs. Direct Mutation`),
el("div", { class: "comparison" }).append(
el("div", { class: "bad-practice" }).append(
el("h5", t`❌ Without Actions`),
el(code, { content: `
const todos = S([]);
// Directly mutating the array
const items = todos.get();
items.push("New todo");
// This WON'T trigger updates!`, page_id }))
),
el("div", { class: "good-practice" }).append(
el("h5", t`✅ With Actions`),
el(code, { content: `const todos = S([], {
add(text) {
this.value.push(text);
// Subscribers notified automatically
}
});
// Use the action
S.action(todos, "add", "New todo");`, page_id })
)
),
el(example, { src: fileURL("./components/examples/signals/actions-demo.js"), page_id }),
el("p", t`…but typical user-case is object/array (maps, sets and other mutable objects):`),
el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), page_id }),
el("p").append(...T`
In some way, you can compare it with ${el("a", { textContent: "useReducer", ...references.mdn_use_reducer })}
hook from React. So, the ${el("code", "S(<data>, <actions>)")} pattern creates a store “machine”. We can
then invoke (dispatch) registered action by calling ${el("code", "S.action(<signal>, <name>, ...<args>)")}
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")}).
Actions provide these benefits:
`),
el("ul").append(
el("li", t`Encapsulate state change logic in named methods`),
el("li", t`Guarantee notifications when state changes`),
el("li", t`Prevent accidental direct mutations`),
el("li", t`Act similar to reducers in other state management libraries`)
),
el("p").append(...T`
Here's a more complete example of a todo list using signal actions:
`),
el(example, { src: fileURL("./components/examples/signals/actions-todos.js"), page_id }),
el("div", { class: "dde-tip" }).append(
el("p").append(...T`
${el("strong", "Special Action Methods")}: Signal actions can implement special lifecycle hooks:
`),
el("ul").append(
el("li", t`[S.symbols.onclear]() - Called when the signal is cleared`),
el("li", t`[S.symbols.onget]() - Called when the signal value is read`),
el("li", t`[S.symbols.onset]() - Called after the signal value is changed`)
)
),
el(h3, t`Connecting Signals to the DOM`),
el("p").append(...T`
Signals really shine when connected to your UI. DDE provides several ways to bind signals to DOM elements:
`),
el(h3, t`Reactive DOM attributes and elements`),
el("p", t`There are on basic level two distinc situation to mirror dynamic value into the DOM/UI`),
el("ol").append(
el("li", t`to change some attribute(s) of existing element(s)`),
el("li", t`to generate elements itself dynamically this covers conditions and loops`)
el("div", { class: "dde-tabs" }).append(
el("div", { class: "tab", "data-tab": "attributes" }).append(
el("h4", t`Reactive Attributes`),
el("p", t`Bind signal values directly to element attributes, properties, or styles:`),
el(code, { content: `// Create a signal
const color = S("blue");
// Bind it to an element's style
el("div", {
style: {
color, // Updates when signal changes
fontWeight: S(() => color.get() === "red" ? "bold" : "normal")
}
}, "This text changes color");
// Later:
color.set("red"); // UI updates automatically`, page_id })
),
el("div", { class: "tab", "data-tab": "elements" }).append(
el("h4", t`Reactive Elements`),
el("p", t`Dynamically create or update elements based on signal values:`),
el(code, { content: `// Create an array signal
const items = S(["Apple", "Banana", "Cherry"]);
// Create a dynamic list that updates when items change
el("ul").append(
S.el(items, items =>
items.map(item => el("li", item))
)
);
// Later:
S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id })
)
),
el(example, { src: fileURL("./components/examples/signals/dom-attrs.js"), page_id }),
el("p").append(...T`
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")}.
The ${el("code", "assign")} and ${el("code", "el")} functions detect signals automatically and handle binding.
You can use special properties like ${el("code", "dataset")}, ${el("code", "ariaset")}, and
${el("code", "classList")} for fine-grained control over specific attribute types.
`),
el("p").append(...T`
For computation, you can use the “derived signal” (see above) like
${el("code", "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.
`),
el("p").append(...T`
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("code", "S.el()")} is especially powerful for conditional rendering and lists:
`),
el(example, { src: fileURL("./components/examples/signals/dom-el.js"), page_id }),
el(h3, t`Best Practices for Signals`),
el("p").append(...T`
Follow these guidelines to get the most out of signals:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Keep signals small and focused")}: Use many small signals rather than a few large ones
`),
el("li").append(...T`
${el("strong", "Use derived signals for computations")}: Don't recompute values in multiple places
`),
el("li").append(...T`
${el("strong", "Clean up signal subscriptions")}: Use AbortController or scope.host() to prevent memory leaks
`),
el("li").append(...T`
${el("strong", "Use actions for complex state")}: Don't directly mutate objects or arrays in signals
`),
el("li").append(...T`
${el("strong", "Avoid infinite loops")}: Be careful when one signal updates another in a subscription
`)
),
el("div", { class: "dde-troubleshooting" }).append(
el("h4", t`Common Signal Pitfalls`),
el("dl").append(
el("dt", t`UI not updating when array/object changes`),
el("dd", t`Use signal actions instead of direct mutation`),
el("dt", t`Infinite update loops`),
el("dd", t`Check for circular dependencies between signals`),
el("dt", t`Memory leaks`),
el("dd", t`Use AbortController or scope.host() to clean up subscriptions`),
el("dt", t`Multiple elements updating unnecessarily`),
el("dd", t`Split large signals into smaller, more focused ones`)
)
),
el(mnemonic)
);
}

View File

@ -1,7 +1,7 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Scopes and components`,
description: t`Organizing UI into components`,
title: t`Scopes and Components`,
description: t`Organizing UI into reusable, manageable components`,
};
import { el } from "deka-dom-el";
@ -28,60 +28,226 @@ const references= {
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("h2", t`Using functions as UI components`),
el("h2", t`Building Maintainable UIs with Scopes and Components`),
el("p").append(...T`
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 ${el("a", { textContent: t`Garbage collection`, ...references.garbage_collection })}.
Scopes provide a structured way to organize your UI code into reusable components that properly
manage their lifecycle, handle cleanup, and maintain clear boundaries between different parts of your application.
`),
el("div", { class: "dde-callout" }).append(
el("h4", t`Why Use Scopes?`),
el("ul").append(
el("li", t`Automatic resource cleanup when components are removed from DOM`),
el("li", t`Clear component boundaries with explicit host elements`),
el("li", t`Simplified event handling with proper "this" binding`),
el("li", t`Seamless integration with signals for reactive components`),
el("li", t`Better memory management with ${el("a", { textContent: t`GC`, ...references.garbage_collection })}`)
)
),
el(code, { src: fileURL("./components/examples/scopes/intro.js"), page_id }),
el("p").append(...T`The library therefore use ${el("em", t`scopes`)} to provide these functionalities.`),
el(h3, t`Scopes and hosts`),
el(h3, t`Understanding Host Elements and Scopes`),
el("div", { class: "dde-illustration" }).append(
el("h4", t`Component Anatomy`),
el("pre").append(el("code", `
┌─────────────────────────────────┐
│ // 1. Component scope created │
│ el(MyComponent); │
│ │
│ function MyComponent() { │
│ // 2. access the host element │
│ const { host } = scope; │
│ │
│ // 3. Add behavior to host │
│ host( │
│ on.click(handleClick) │
│ ); │
│ │
│ // 4. Return the host element │
│ return el("div", { │
│ className: "my-component" │
│ }).append( │
│ el("h2", "Title"), │
│ el("p", "Content") │
│ ); │
│ } │
└─────────────────────────────────┘
`))
),
el("p").append(...T`
The ${el("strong", "host")} is the name for the element representing the component. This is typically
element returned by function. To get reference, you can use ${el("code", "scope.host()")} to applly addons
just use ${el("code", "scope.host(...<addons>)")}.
The ${el("strong", "host element")} is the root element of your component - typically the element returned
by your component function. It serves as the identity of your component in the DOM.
`),
el("div", { class: "dde-function-table" }).append(
el("h4", t`scope.host()`),
el("dl").append(
el("dt", t`When called with no arguments`),
el("dd", t`Returns a reference to the host element (the root element of your component)`),
el("dt", t`When called with addons/callbacks`),
el("dd", t`Applies the addons to the host element and returns the host element`)
)
),
el(example, { src: fileURL("./components/examples/scopes/scopes-and-hosts.js"), page_id }),
el("div", { class: "dde-tip" }).append(
el("p").append(...T`
To better understanding we implement function ${el("code", "elClass")} helping to create component as
class instances.
${el("strong", "Best Practice:")} Always capture the host reference at the beginning of your component function
using ${el("code", "const { host } = scope")} to avoid scope-related issues, especially with asynchronous code.
`)
),
el(h3, t`Class-Based Components`),
el("p").append(...T`
While functional components are the primary pattern in DDE, you can also create class-based components
for more structured organization of component logic.
`),
el(example, { src: fileURL("./components/examples/scopes/class-component.js"), page_id }),
el("p").append(...T`
As you can see, the ${el("code", "scope.host()")} is stored temporarily and synchronously. Therefore, at
least in the beginning of using library, it is the good practise to store ${el("code", "host")} in the root
of your component. As it may be changed, typically when there is asynchronous code in the component.
This pattern can be useful when:
`),
el("ul").append(
el("li", t`You have complex component logic that benefits from object-oriented organization`),
el("li", t`You need private methods and properties for your component`),
el("li", t`You're transitioning from another class-based component system`)
),
el("div", { class: "dde-tip" }).append(
el("p").append(...T`
${el("strong", "Note:")} Even with class-based components, follow the best practice of storing the host reference
early in your component code. This ensures proper access to the host throughout the component's lifecycle.
`)
),
el(code, { src: fileURL("./components/examples/scopes/good-practise.js"), page_id }),
el(h3, t`Scopes, signals and cleaning magic`),
el(h3, t`Automatic Cleanup with Scopes`),
el("p").append(...T`
The ${el("code", "host")} is internally used to register the cleaning procedure, when the component
(${el("code", "host")} element) is removed from the DOM.
One of the most powerful features of scopes is automatic cleanup when components are removed from the DOM.
This prevents memory leaks and ensures resources are properly released.
`),
el("div", { class: "dde-illustration" }).append(
el("h4", t`Lifecycle Flow`),
el("pre").append(el("code", `
1. Component created → scope established
2. Component added to DOM → connected event
3. Component interactions happen
4. Component removed from DOM → disconnected event
5. Automatic cleanup of:
- Event listeners
- Signal subscriptions
- Custom cleanup code
`))
),
el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }),
el("div", { class: "dde-note" }).append(
el("p").append(...T`
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, /* ${t`update the paragraph`} */)`)}.
`),
In this example, when you click "Remove", the component is removed from the DOM, and all its associated
resources are automatically cleaned up, including the signal subscription that updates the text content.
This happens because the library internally registers a disconnected event handler on the host element.
`)
),
el(h3, t`Declarative vs Imperative Components`),
el("p").append(...T`
This listener must be removed when the component is removed from the DOM. To do it, the library assign
internally ${el("code", `on.disconnected(/* ${t`remove the listener`} */)(host())`)} to the host element.
`),
el("p", { className: "notice" }).append(...T`
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", ...references.signals })}.
Scopes work best with a declarative approach to UI building, especially when combined
with ${el("a", { textContent: "signals", ...references.signals })} for state management.
`),
el("div", { class: "dde-tabs" }).append(
el("div", { class: "tab", "data-tab": "declarative" }).append(
el("h4", t`✅ Declarative Approach`),
el("p", t`Define what your UI should look like based on state:`),
el("pre").append(el("code", `function Counter() {
const { host } = scope;
// Define state
const count = S(0);
// Define behavior
const increment = () => count.set(count.get() + 1);
// UI automatically updates when count changes
return el("div").append(
el("p", S(() => "Count: " + count.get())),
el("button", {
onclick: increment,
textContent: "Increment"
})
);
}`))
),
el("div", { class: "tab", "data-tab": "imperative" }).append(
el("h4", t`⚠️ Imperative Approach`),
el("p", t`Manually update the DOM in response to events:`),
el("pre").append(el("code", `function Counter() {
const { host } = scope;
let count = 0;
const counterText = el("p", "Count: 0");
// Manually update DOM element
const increment = () => {
count++;
counterText.textContent = "Count: " + count;
};
return el("div").append(
counterText,
el("button", {
onclick: increment,
textContent: "Increment"
})
);
}`))
)
),
el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id }),
el("div", { class: "dde-note" }).append(
el("p").append(...T`
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.
`),
While DDE supports both declarative and imperative approaches, the declarative style is recommended
as it leads to more maintainable code with fewer opportunities for bugs. Signals handle the complexity
of keeping your UI in sync with your data.
`)
),
el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id }),
el(h3, t`Best Practices for Scopes and Components`),
el("ol").append(
el("li").append(...T`
${el("strong", "Capture host early:")} Use ${el("code", "const { host } = scope")} at component start
`),
el("li").append(...T`
${el("strong", "Return a single root element:")} Components should have one host element that contains all others
`),
el("li").append(...T`
${el("strong", "Prefer declarative patterns:")} Use signals to drive UI updates rather than manual DOM manipulation
`),
el("li").append(...T`
${el("strong", "Keep components focused:")} Each component should do one thing well
`),
el("li").append(...T`
${el("strong", "Add explicit cleanup:")} For resources not managed by DDE, use ${el("code", "on.disconnected")}
`)
),
el("div", { class: "dde-troubleshooting" }).append(
el("h4", t`Common Scope Pitfalls`),
el("dl").append(
el("dt", t`Losing host reference in async code`),
el("dd", t`Store host reference early with const { host } = scope`),
el("dt", t`Memory leaks from custom resources`),
el("dd", t`Use host(on.disconnected(cleanup)) for manual resource cleanup`),
el("dt", t`Event handlers with incorrect 'this'`),
el("dd", t`Use arrow functions or .bind() to preserve context`),
el("dt", t`Mixing declarative and imperative styles`),
el("dd", t`Choose one approach and be consistent throughout a component`)
)
),
el(mnemonic)
);
}

View File

@ -53,90 +53,236 @@ const references= {
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("h2", t`Using web components in combinantion with DDE`),
el("h2", t`Using Web Components with DDE: Better Together`),
el("p").append(...T`
The DDE library allows for use within ${el("a", references.mdn_web_components).append( el("strong",
t`Web Components`) )} for dom-tree generation. However, in order to be able to use signals (possibly
mapping to registered ${el("a", references.mdn_observedAttributes).append( el("code", "observedAttributes")
)}) and additional functionality is (unfortunately) required to use helpers provided by the library.
DDE pairs powerfully with ${el("a", references.mdn_web_components).append(el("strong", t`Web Components`))}
to create reusable, encapsulated custom elements with all the benefits of DDE's declarative DOM
construction and reactivity system.
`),
el("div", { class: "dde-callout" }).append(
el("h4", t`Why Combine DDE with Web Components?`),
el("ul").append(
el("li", t`Declarative DOM creation within your components`),
el("li", t`Reactive attribute updates through signals`),
el("li", t`Simplified event handling with the same events API`),
el("li", t`Clean component lifecycle management`),
el("li", t`Improved code organization with scopes`)
)
),
el(code, { src: fileURL("./components/examples/customElement/intro.js"), page_id }),
el(h3, t`Custom Elements Introduction`),
el(h3, t`Getting Started: Web Components Basics`),
el("p").append(...T`
Web Components, specifically Custom Elements, are a set of web platform APIs that allow you to create
new HTML tags with custom functionality encapsulated within them. This allows for the creation of reusable
components that can be used across web applications.
`),
el("p").append(...T`
To start with, lets see how to use native Custom Elements. As starting point please read
${el("a", references.mdn_custom_elements).append( el("strong", t`Using Custom Elements`), t` on MDN` )}.
To sum up and for mnemonic see following code overview:
`),
el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }),
el("p").append(...T`
For more advanced use of Custom Elements, the summary ${el("a", references.custom_elements_tips)
.append( el("strong", t`Handy Custom Elements Patterns`) )} may be useful. Especially pay attention to
linking HTML attributes and defining setters/getters, this is very helpful to use in combination with
the library (${el("code", `el(HTMLCustomElement.tagName, { customAttribute: "${t`new-value`}" });`)}).
`),
el("p").append(...T`
Also see the Life Cycle Events sections, very similarly we would like to use
${el("a", { textContent: t`DDE events`, href: "./p03-events.html", title: t`See events part of the library
documentation` })}. To do it, the library provides function ${el("code", "customElementWithDDE")}
`),
el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }),
el("h3", t`Custom Elements with DDE`),
el("p").append(...T`
The ${el("code", "customElementWithDDE")} function is only (small) part of the inregration of the library.
More important for coexistence is render component function as a body of the Custom Element. For that, you
can use ${el("code", "customElementRender")} with arguments instance reference, target for connection,
render function and optional properties (will be passed to the render function) see later…
`),
el(example, { src: fileURL("./components/examples/customElement/dde.js"), page_id }),
el("p").append(...T`
…as you can see, you can use components created based on the documentation previously introduced. To unlock
full potential, use with combination ${el("code", "customElementWithDDE")} (allows to use livecycle events)
and ${el("code", "observedAttributes")} (converts attributes to render function arguments —
${el("em", "default")}) or ${el("code", "S.observedAttributes")} (converts attributes to signals).
`),
el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }),
el(h3, t`Shadow DOM`),
el("p").append(...T`
Shadow DOM is a web platform feature that allows for the encapsulation of a components internal DOM tree
from the rest of the document. This means that styles and scripts applied to the document will not affect
the components internal DOM, and vice versa.
`),
el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }),
el("p").append(...T`
Regarding to ${el("code", "this.attachShadow({ mode: 'open' })")} see quick overview
${el("a", { textContent: t`Using Shadow DOM`, ...references.mdn_shadow_dom_depth })}. An another source of
information can be ${el("a", { textContent: t`Shadow DOM in Depth`, ...references.shadow_dom_depth })}.
`),
el("p").append(...T`
Besides the encapsulation, the Shadow DOM allows for using the ${el("a", references.mdn_shadow_dom_slot).append(
el("strong", t`<slot>`), t` element(s)`)}. You can simulate this feature using ${el("code", "simulateSlots")}:
`),
el(example, { src: fileURL("./components/examples/customElement/simulateSlots.js"), page_id }),
el("p").append(...T`
To sum up:
Web Components are a set of standard browser APIs that let you create custom HTML elements with
encapsulated functionality. They consist of three main technologies:
`),
el("ul").append(
el("li").append(...T`
The use of shadow DOM to encapsulate the internal structure of the custom element, which affects how
the custom element can be styled and modified using JavaScript and CSS.
${el("strong", "Custom Elements:")} Create your own HTML tags with JS-defined behavior
`),
el("li").append(...T`
The ability to access and modify the internal structure of the custom element using JavaScript, which
is affected by the use of shadow DOM and the mode of the shadow DOM.
${el("strong", "Shadow DOM:")} Encapsulate styles and markup within a component
`),
el("li").append(...T`
The use of slots to allow for the insertion of content from the parent document into the custom
element, which is affected by the use of shadow DOM and the mode of the shadow DOM.
${el("strong", "HTML Templates:")} Define reusable markup structures
`)
),
el("p").append(...T`
Let's start with a basic Custom Element example without DDE to establish the foundation:
`),
el(code, { src: fileURL("./components/examples/customElement/native-basic.js"), page_id }),
el("div", { class: "dde-note" }).append(
el("p").append(...T`
For complete information on Web Components, see the
${el("a", references.mdn_custom_elements).append(el("strong", t`MDN documentation`))}.
Also, ${el("a", references.custom_elements_tips).append(el("strong", t`Handy Custom Elements Patterns`))}
provides useful techniques for connecting attributes with properties.
`)
),
el(h3, t`DDE Integration: Step 1 - Event Handling`),
el("p").append(...T`
The first step in integrating DDE with Web Components is enabling DDE's event system to work with your
Custom Elements. This is done with ${el("code", "customElementWithDDE")}, which makes your Custom Element
compatible with DDE's event handling.
`),
el("div", { class: "dde-function-table" }).append(
el("h4", t`customElementWithDDE`),
el("dl").append(
el("dt", t`Purpose`),
el("dd", t`Enables DDE's event system to work with your Custom Element`),
el("dt", t`Usage`),
el("dd", t`customElementWithDDE(YourElementClass)`),
el("dt", t`Benefits`),
el("dd", t`Allows using on.connected(), on.disconnected(), etc. with your element`)
)
),
el(example, { src: fileURL("./components/examples/customElement/customElementWithDDE.js"), page_id }),
el("div", { class: "dde-tip" }).append(
el("p").append(...T`
${el("strong", "Key Point:")} The ${el("code", "customElementWithDDE")} function adds event dispatching
to your Custom Element lifecycle methods, making them work seamlessly with DDE's event system.
`)
),
el(h3, t`DDE Integration: Step 2 - Rendering Components`),
el("p").append(...T`
The next step is to use DDE's component rendering within your Custom Element. This is done with
${el("code", "customElementRender")}, which connects your DDE component function to the Custom Element.
`),
el("div", { class: "dde-function-table" }).append(
el("h4", t`customElementRender`),
el("dl").append(
el("dt", t`Purpose`),
el("dd", t`Connects a DDE component function to a Custom Element`),
el("dt", t`Parameters`),
el("dd").append(
el("ol").append(
el("li", t`Target (usually this or this.shadowRoot)`),
el("li", t`Component function that returns a DOM tree`),
el("li", t`Optional: Attributes transformer function (default or S.observedAttributes)`)
)
),
el("dt", t`Returns`),
el("dd", t`The rendered DOM tree`)
)
),
el(example, { src: fileURL("./components/examples/customElement/dde.js"), page_id }),
el("div", { class: "dde-note" }).append(
el("p").append(...T`
In this example, we're using Shadow DOM (${el("code", "this.attachShadow()")}) for encapsulation,
but you can also render directly to the element with ${el("code", "customElementRender(this, ...)")}.
`)
),
el(h3, t`Reactive Web Components with Signals`),
el("p").append(...T`
One of the most powerful features of integrating DDE with Web Components is connecting HTML attributes
to DDE's reactive signals system. This creates truly reactive custom elements.
`),
el("div", { class: "dde-tip" }).append(
el("p").append(...T`
${el("strong", "Two Ways to Handle Attributes:")}
`),
el("ol").append(
el("li").append(...T`
${el("code", "observedAttributes")} - Passes attributes as regular values (static)
`),
el("li").append(...T`
${el("code", "S.observedAttributes")} - Transforms attributes into signals (reactive)
`)
)
),
el("p").append(...T`
Using ${el("code", "S.observedAttributes")} creates a reactive connection between your element's attributes
and its internal rendering. When attributes change, your component automatically updates!
`),
el(example, { src: fileURL("./components/examples/customElement/observedAttributes.js"), page_id }),
el("div", { class: "dde-callout" }).append(
el("h4", t`How S.observedAttributes Works`),
el("p").append(...T`
1. Takes each attribute listed in static observedAttributes
2. Creates a DDE signal for each one
3. Automatically updates these signals when attributes change
4. Passes the signals to your component function
5. Your component reacts to changes through signal subscriptions
`)
),
el(h3, t`Working with Shadow DOM`),
el("p").append(...T`
Shadow DOM provides encapsulation for your component's styles and markup. When using DDE with Shadow DOM,
you get the best of both worlds: encapsulation plus declarative DOM creation.
`),
el("div", { class: "dde-illustration" }).append(
el("h4", t`Shadow DOM Encapsulation`),
el("pre").append(el("code", `
┌─────────────────────────────────┐
│ <my-custom-element> │
│ │
│ ┌─────────────────────────┐ │
│ │ #shadow-root │ │
│ │ │ │
│ │ Created with DDE: │ │
│ │ ┌─────────────────┐ │ │
│ │ │ <div> │ │ │
│ │ │ <h2>Title</h2> │ │ │
│ │ │ <p>Content</p> │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────┘
`))
),
el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }),
el("p").append(...T`
For more information on Shadow DOM, see
${el("a", { textContent: t`Using Shadow DOM`, ...references.mdn_shadow_dom_depth })}, or the comprehensive
${el("a", { textContent: t`Shadow DOM in Depth`, ...references.shadow_dom_depth })}.
`),
el(h3, t`Working with Slots`),
el("p").append(...T`
Slots allow users of your component to insert content inside it. When using DDE, you can simulate the
slot mechanism with the ${el("code", "simulateSlots")} function:
`),
el("div", { class: "dde-function-table" }).append(
el("h4", t`simulateSlots`),
el("dl").append(
el("dt", t`Purpose`),
el("dd", t`Provides slot functionality when you cannot/do not want to use shadow DOM`),
el("dt", t`Parameters`),
el("dd", t`A mapping object of slot names to DOM elements`)
)
),
el(example, { src: fileURL("./components/examples/customElement/simulateSlots.js"), page_id }),
el("div", { class: "dde-tip" }).append(
el("p").append(...T`
${el("strong", "When to use simulateSlots:")} This approach is useful when you need to distribute
content from the light DOM into specific locations in the shadow DOM, particularly in environments
where native slots might not be fully supported.
`)
),
el(h3, t`Best Practices for Web Components with DDE`),
el("p").append(...T`
When combining DDE with Web Components, follow these recommendations:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Always use customElementWithDDE")} to enable event integration
`),
el("li").append(...T`
${el("strong", "Prefer S.observedAttributes")} for reactive attribute connections
`),
el("li").append(...T`
${el("strong", "Create reusable component functions")} that your custom elements render
`),
el("li").append(...T`
${el("strong", "Use scope.host()")} to clean up event listeners and subscriptions
`),
el("li").append(...T`
${el("strong", "Add setters and getters")} for better property access to your element
`)
),
el("div", { class: "dde-troubleshooting" }).append(
el("h4", t`Common Issues`),
el("dl").append(
el("dt", t`Events not firing properly`),
el("dd", t`Make sure you called customElementWithDDE before defining the element`),
el("dt", t`Attributes not updating`),
el("dd", t`Check that you've properly listed them in static observedAttributes`),
el("dt", t`Component not rendering`),
el("dd", t`Verify customElementRender is called in connectedCallback, not constructor`)
)
),
el(mnemonic)

186
docs/p07-debugging.html.js Normal file
View File

@ -0,0 +1,186 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Debugging`,
description: t`Techniques for debugging applications using deka-dom-el, especially 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 { code } from "./components/code.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("h2", t`Debugging applications with deka-dom-el`),
el("p").append(...T`
Debugging is an essential part of application development. This guide provides techniques
and best practices for debugging applications built with deka-dom-el, with a focus on signals.
`),
el(h3, t`Debugging signals`),
el("p").append(...T`
Signals are reactive primitives that update the UI when their values change. When debugging signals,
you need to track their values, understand their dependencies, and identify why updates are or aren't happening.
`),
el("h4", t`Inspecting signal values`),
el("p").append(...T`
The simplest way to debug a signal is to log its current value by calling the get method:
`),
el(code, { content: "const signal = S(0);\nconsole.log('Current value:', signal.get());", page_id }),
el("p").append(...T`
You can also monitor signal changes by adding a listener:
`),
el(code, {
content:
"// Log every time the signal changes\nS.on(signal, value => console.log('Signal changed:', value));",
page_id }),
el("h4", t`Debugging derived signals`),
el("p").append(...T`
With derived signals (created with S(() => computation)), debugging is a bit more complex
because the value depends on other signals. To understand why a derived signal isn't updating correctly:
`),
el("ol").append(
el("li", t`Check that all dependency signals are updating correctly`),
el("li", t`Add logging inside the computation function to see when it runs`),
el("li", t`Verify that the computation function actually accesses the signal values with .get()`)
),
el(example, { src: fileURL("./components/examples/debugging/consoleLog.js"), page_id }),
el(h3, t`Common signal debugging issues`),
el("h4", t`Signal updates not triggering UI changes`),
el("p").append(...T`
If signal updates aren't reflected in the UI, check:
`),
el("ul").append(
el("li", t`That you're using signal.set() to update the value, not modifying objects/arrays directly`),
el("li", t`For mutable objects, ensure you're using actions or making proper copies before updating`),
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
),
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),
el("h4", t`Memory leaks with signal listeners`),
el("p").append(...T`
Signal listeners can cause memory leaks if not properly cleaned up. Always use AbortSignal
to cancel listeners.
`),
el("h4", t`Performance issues with frequently updating signals`),
el("p").append(...T`
If you notice performance issues with signals that update very frequently:
`),
el("ul").append(
el("li", t`Consider debouncing or throttling signal updates`),
el("li", t`Make sure derived signals don't perform expensive calculations unnecessarily`),
el("li", t`Keep signal computations focused and minimal`)
),
el(code, { src: fileURL("./components/examples/debugging/debouncing.js"), page_id }),
el(h3, t`Browser DevTools tips for deka-dom-el`),
el("p").append(...T`
When debugging in the browser, deka-dom-el provides several helpful DevTools-friendly features:
`),
el("h4", t`Identifying components in the DOM`),
el("p").append(...T`
deka-dom-el marks components in the DOM with special comment nodes to help you identify component boundaries.
Components created with ${el("code", "el(ComponentFunction)")} are marked with comment nodes
${el("code", "<!--<dde:mark type=\"component\" name=\"MyComponent\" host=\"parentElement\"/>-->")} and
includes:
`),
el("ul").append(
el("li", "type - Identifies the type of marker (\"component\", \"reactive\", or \"later\")"),
el("li", "name - The name of the component function"),
el("li", "host - Indicates whether the host is \"this\" (for DocumentFragments) or \"parentElement\""),
),
el("h4", t`Finding reactive elements in the DOM`),
el("p").append(...T`
When using ${el("code", "S.el()")}, deka-dom-el creates reactive elements in the DOM
that are automatically updated when signal values change. These elements are wrapped in special
comment nodes for debugging (to be true they are also used internaly, so please do not edit them by hand):
`),
el(code, { src: fileURL("./components/examples/debugging/dom-reactive-mark.js"), page_id }),
el("p").append(...T`
This is particularly useful when debugging why a reactive section isn't updating as expected.
You can inspect the elements between the comment nodes to see their current state and the
signal connections through \`__dde_reactive\` of the host element.
`),
el("h4", t`DOM inspection properties`),
el("p").append(...T`
Elements created with the deka-dom-el library have special properties to aid in debugging:
`),
el("ul").append(
el("li").append(...T`
${el("code", "__dde_reactive")} - An array property on DOM elements that tracks signal-to-element relationships.
This allows you to quickly identify which elements are reactive and what signals they're bound to.
Each entry in the array contains:
`),
el("ul").append(
el("li", "A pair of signal and listener function: [signal, listener]"),
el("li", "Additional context information about the element or attribute"),
el("li", "Automatically managed by signal.el(), signal.observedAttributes(), and processReactiveAttribute()")
),
el("li").append(...T`
${el("code", "__dde_signal")} - A Symbol property used to identify and store the internal state of signal objects.
It contains the following information:
`),
el("ul").append(
el("li", "value: The current value of the signal"),
el("li", "listeners: A Set of functions called when the signal value changes"),
el("li", "actions: Custom actions that can be performed on the signal"),
el("li", "onclear: Functions to run when the signal is cleared"),
el("li", "host: Reference to the host element or scope"),
el("li", "defined: Stack trace information for debugging"),
el("li", "readonly: Boolean flag indicating if the signal is read-only")
),
),
el("p").append(...T`
These properties make it easier to understand the reactive structure of your application when inspecting elements.
`),
el(example, { src: fileURL("./components/examples/signals/debugging-dom.js"), page_id }),
el("h4", t`Examining signal connections`),
el("p").append(...T`
You can inspect signal relationships and bindings in the DevTools console using ${el("code", "$0.__dde_reactive")}.
In console you will see list of ${el("code", "[ [ signal, listener ], element, property ]")}, where:
`),
el("ul").append(
el("li", "signal — the signal triggering the changes"),
el("li", "listener — the listener function (this is internal function for dde)"),
el("li", "element — the DOM element that is bound to the signal"),
el("li", "property — the attribute or property name which is changing based on the signal"),
),
el("p").append(...T`
…the structure of \`__dde_reactive\` use the behavior of the browser that packs the first field,
so you can see the element and property that changes in the console right away
`),
el("h4", t`Debugging with breakpoints`),
el("p").append(...T`
Effective use of breakpoints can help track signal flow:
`),
el("ul").append(
el("li").append(...T`
Set breakpoints in signal update methods to track when values change
`),
el("li").append(...T`
Use conditional breakpoints to only break when specific signals change to certain values
`),
el("li").append(...T`
Set breakpoints in your signal computation functions to see when derived signals recalculate
`),
el("li").append(...T`
Use performance profiling to identify bottlenecks in signal updates
`)
),
);
}

View File

@ -0,0 +1,102 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Debugging`,
description: t`Techniques for debugging applications using deka-dom-el, especially 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 { code } from "./components/code.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("h2", t`Debugging applications with deka-dom-el`),
el("p").append(...T`
Debugging is an essential part of application development. This guide provides techniques
and best practices for debugging applications built with deka-dom-el, with a focus on signals.
`),
el(h3, t`Debugging signals`),
el("p").append(...T`
Signals are reactive primitives that update the UI when their values change. When debugging signals,
you need to track their values, understand their dependencies, and identify why updates are or aren't happening.
`),
el("h4", t`Inspecting signal values`),
el("p").append(...T`
The simplest way to debug a signal is to log its current value by calling the get method:
`),
el("pre").append(
el("code", "const signal = S(0);\nconsole.log('Current value:', signal.get());")
),
el("p").append(...T`
You can also monitor signal changes by adding a listener:
`),
el("pre").append(
el("code", "// Log every time the signal changes\nS.on(signal, value => console.log('Signal changed:', value));")
),
el("h4", t`Debugging derived signals`),
el("p").append(...T`
With derived signals (created with S(() => computation)), debugging is a bit more complex
because the value depends on other signals. To understand why a derived signal isn't updating correctly:
`),
el("ol").append(
el("li", t`Check that all dependency signals are updating correctly`),
el("li", t`Add logging inside the computation function to see when it runs`),
el("li", t`Verify that the computation function actually accesses the signal values with .get()`)
),
el(example, { src: fileURL("./components/examples/debugging/consoleLog.js"), page_id }),
el(h3, t`Common signal debugging issues`),
el("h4", t`Signal updates not triggering UI changes`),
el("p").append(...T`
If signal updates aren't reflected in the UI, check:
`),
el("ul").append(
el("li", t`That you're using signal.set() to update the value, not modifying objects/arrays directly`),
el("li", t`For mutable objects, ensure you're using actions or making proper copies before updating`),
el("li", t`That the signal is actually connected to the DOM element (check your S.el or attribute binding code)`)
),
el(code, { src: fileURL("./components/examples/debugging/mutations.js"), page_id }),
el("h4", t`Memory leaks with signal listeners`),
el("p").append(...T`
Signal listeners can cause memory leaks if not properly cleaned up. Always use AbortSignal
to cancel listeners.
`),
el("h4", t`Performance issues with frequently updating signals`),
el("p").append(...T`
If you notice performance issues with signals that update very frequently:
`),
el("ul").append(
el("li", t`Consider debouncing or throttling signal updates`),
el("li", t`Make sure derived signals don't perform expensive calculations unnecessarily`),
el("li", t`Keep signal computations focused and minimal`)
),
el(code, { src: fileURL("./components/examples/debugging/debouncing.js"), page_id }),
el(h3, t`Browser DevTools tips for deka-dom-el`),
el("p").append(...T`
When debugging in the browser, here are some helpful techniques:
`),
el("ul").append(
el("li").append(...T`
Use the Elements panel to inspect the DOM structure created by deka-dom-el
`),
el("li").append(...T`
Set breakpoints in your signal handlers and actions
`),
el("li").append(...T`
Use performance profiling to identify bottlenecks in signal updates
`),
),
);
}

129
docs/p08-ssr.html.js Normal file
View File

@ -0,0 +1,129 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Server-Side Rendering (SSR)`,
description: t`Using deka-dom-el for server-side rendering with jsdom to generate static HTML.`,
};
import { el } from "deka-dom-el";
import { simplePage } from "./layout/simplePage.html.js";
import { h3 } from "./components/pageUtils.html.js";
import { code } from "./components/code.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {import("./types.d.ts").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("h2", t`Server-Side Rendering with deka-dom-el`),
el("p").append(...T`
deka-dom-el isn't limited to browser environments. Thanks to its flexible architecture,
it can be used for server-side rendering (SSR) to generate static HTML files.
This is achieved through integration with for example ${el("a", { href: "https://github.com/tmpvar/jsdom",
textContent: "jsdom" })}, a JavaScript implementation of web standards for Node.js.
`),
el(code, { src: fileURL("./components/examples/ssr/intro.js"), page_id }),
el(h3, t`Why Server-Side Rendering?`),
el("p").append(...T`
SSR offers several benefits:
`),
el("ul").append(
el("li", t`Improved SEO - Search engines can easily index fully rendered content`),
el("li", t`Faster initial page load - Users see content immediately without waiting for JavaScript to load`),
el("li", t`Better performance on low-powered devices - Less JavaScript processing on the client`),
el("li", t`Content available without JavaScript - Useful for users who have disabled JavaScript`),
el("li", t`Static site generation - Build files once, serve them many times`)
),
el(h3, t`How jsdom Integration Works`),
el("p").append(...T`
The jsdom export in deka-dom-el provides the necessary tools to use the library in Node.js
by integrating with jsdom. Here's what it does:
`),
el("ol").append(
el("li", t`Creates a virtual DOM environment in Node.js using jsdom`),
el("li", t`Registers DOM globals like HTMLElement, document, etc. for deka-dom-el to use`),
el("li", t`Sets an SSR flag in the environment to enable SSR-specific behaviors`),
el("li", t`Provides a promise queue system for managing async operations during rendering`),
el("li", t`Handles DOM property/attribute mapping differences between browsers and jsdom`)
),
el(code, { src: fileURL("./components/examples/ssr/start.js"), page_id }),
el(h3, t`Basic SSR Example`),
el("p").append(...T`
Here's a simple example of how to use deka-dom-el for server-side rendering in a Node.js script:
`),
el(code, { src: fileURL("./components/examples/ssr/basic-example.js"), page_id }),
el(h3, t`Building a Static Site Generator`),
el("p").append(...T`
You can build a complete static site generator with deka-dom-el. In fact, this documentation site
is built using deka-dom-el for server-side rendering! Here's how the documentation build process works:
`),
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),
el(h3, t`Working with Async Content in SSR`),
el("p").append(...T`
The jsdom export includes a queue system to handle asynchronous operations during rendering.
This is crucial for components that fetch data or perform other async tasks.
`),
el(code, { src: fileURL("./components/examples/ssr/async-data.js"), page_id }),
el(h3, t`Working with Dynamic Imports for SSR`),
el("p").append(...T`
When structuring server-side rendering code, a crucial pattern to follow is using dynamic imports
for both the deka-dom-el/jsdom module and your page components.
`),
el("p").append(...T`
Why is this important?
`),
el("ul").append(
el("li").append(...T`
${el("strong", "Static imports are hoisted:")} JavaScript hoists import statements to the top of the file,
executing them before any other code
`),
el("li").append(...T`
${el("strong", "Environment registration timing:")} The jsdom module auto-registers the DOM environment
when imported, which must happen ${el("em", "after")} you've created your JSDOM instance and
${el("em", "before")} you import your components using ${el("code", "import { el } from \"deka-dom-el\";")}.
`),
el("li").append(...T`
${el("strong", "Correct initialization order:")} You need to control the exact sequence of:
create JSDOM → register environment → import components
`)
),
el("p").append(...T`
Follow this pattern when creating server-side rendered pages:
`),
el(code, { src: fileURL("./components/examples/ssr/pages.js"), page_id }),
el(h3, t`SSR Considerations and Limitations`),
el("p").append(...T`
When using deka-dom-el for SSR, keep these considerations in mind:
`),
el("ul").append(
el("li", t`Browser-specific APIs like window.localStorage are not available in jsdom by default`),
el("li", t`Event listeners added during SSR won't be functional in the final HTML unless hydrated on the client`),
el("li", t`Some DOM features may behave differently in jsdom compared to real browsers`),
el("li", t`For large sites, you may need to optimize memory usage by creating a new jsdom instance for each page`)
),
el("p").append(...T`
For advanced SSR applications, consider implementing hydration on the client-side to restore
interactivity after the initial render.
`),
el(h3, t`Real Example: How This Documentation is Built`),
el("p").append(...T`
This documentation site itself is built using deka-dom-el's SSR capabilities.
The build process collects all page components, renders them with jsdom, and outputs static HTML files.
`),
el(code, { src: fileURL("./components/examples/ssr/static-site-generator.js"), page_id }),
el("p").append(...T`
The resulting static files can be deployed to any static hosting service,
providing fast loading times and excellent SEO without the need for client-side JavaScript
to render the initial content.
`),
);
}

View File

@ -15,7 +15,7 @@ export function thirdParty(){
}, {
set(key, value){
const p= this.value[key] || S();
p(value);
p.set(value);
this.value[key]= p;
}
});
@ -32,7 +32,7 @@ export function thirdParty(){
})();
return el("input", {
className,
value: store().value(),
value: store.get().value.get(),
type: "text",
onchange: ev=> S.action(store, "set", "value", ev.target.value)
});

View File

@ -9,24 +9,24 @@ export function fullNameComponent(){
const labels= [ "Name", "Surname" ];
const name= labels.map(_=> S(""));
const full_name= S(()=>
name.map(l=> l()).filter(Boolean).join(" ") || "-");
name.map(l=> l.get()).filter(Boolean).join(" ") || "-");
scope.host(
on.connected(()=> console.log(fullNameComponent)),
on.disconnected(()=> console.log(fullNameComponent))
);
const count= S(0);
setInterval(()=> count(count()+1), 5000);
setInterval(()=> count.set(count.get()+1), 5000);
const style= { height: "80px", display: "block", fill: "currentColor" };
const elSVG= elNS("http://www.w3.org/2000/svg");
return el("div", { className }).append(
el("h2", "Simple form:"),
el("p", { textContent: S(()=> "Count: "+count()),
dataset: { count }, classList: { count: S(() => count()%2 === 0) } }),
el("p", { textContent: S(()=> "Count: "+count.get()),
dataset: { count }, classList: { count: S(() => count.get()%2 === 0) } }),
el("form", { onsubmit: ev=> ev.preventDefault() }).append(
...name.map((v, i)=>
el("label", labels[i]).append(
el("input", { type: "text", name: labels[i], value: v() }, on("change", ev=> v(ev.target.value)))
el("input", { type: "text", name: labels[i], value: v.get() }, on("change", ev=> v.set(ev.target.value)))
))
),
el("p").append(

View File

@ -58,7 +58,7 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){
),
el("div").append(
el("h3", "Output (JSON):"),
el("output", S(()=> JSON.stringify(Array.from(todosO()), null, "\t")))
el("output", S(()=> JSON.stringify(Array.from(todosO.get()), null, "\t")))
)
)
}
@ -80,13 +80,13 @@ function todoComponent({ textContent, value }){
const is_editable= S(false);
const onedited= on("change", ev=> {
const el= /** @type {HTMLInputElement} */ (ev.target);
textContent(el.value);
is_editable(false);
textContent.set(el.value);
is_editable.set(false);
});
return el("li").append(
S.el(is_editable, is=> is
? el("input", { value: textContent(), type: "text" }, onedited)
: el("span", { textContent, onclick: is_editable.bind(null, true) })
? el("input", { value: textContent.get(), type: "text" }, onedited)
: el("span", { textContent, onclick: ()=> is_editable.set(true) })
),
el("button", { type: "button", value, textContent: "-" }, onclick)
);

View File

@ -34,7 +34,7 @@ export class CustomHTMLTestElement extends HTMLElement{
text(test),
text(name),
text(preName),
el("button", { type: "button", textContent: "pre-name", onclick: ()=> preName("Ahoj") }),
el("button", { type: "button", textContent: "pre-name", onclick: ()=> preName.set("Ahoj") }),
" | ",
el("slot", { className: "test", name: "test" }),
);

View File

@ -20,7 +20,7 @@ document.body.append(
el("span", { textContent: "test", slot: "test" }),
),
el(thirdParty),
el(CustomSlottingHTMLElement.tagName, { onclick: ()=> toggle(!toggle()) }).append(
el(CustomSlottingHTMLElement.tagName, { onclick: ()=> toggle.set(!toggle.get()) }).append(
el("strong", { slot: "name", textContent: "Honzo" }),
S.el(toggle, is=> is
? el("span", "…default slot")

8
index.d.ts vendored
View File

@ -52,9 +52,13 @@ type IsReadonly<T, K extends keyof T> =
* @private
*/
type ElementAttributes<T extends SupportedElement>= Partial<{
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false
[K in keyof _fromElsInterfaces<T>]:
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=>
ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
: ddeStringable
: ddeStringable)
} & AttrsModified> & Record<string, any>;
export function classListDeclarative<El extends SupportedElement>(
element: El,

1437
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "deka-dom-el",
"version": "0.8.0",
"version": "0.9.0",
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
"author": "Jan Andrle <andrle.jan@centrum.cz>",
"license": "MIT",
@ -29,8 +29,8 @@
"types": "./jsdom.d.ts"
},
"./src/signals-lib": {
"import": "./src/signals-lib.js",
"types": "./src/signals-lib.d.ts"
"import": "./src/signals-lib/signals-lib.js",
"types": "./src/signals-lib/signals-lib.d.ts"
}
},
"files": [
@ -53,7 +53,8 @@
"globals": {
"requestIdleCallback": false,
"AbortController": false,
"AbortSignal": false
"AbortSignal": false,
"FinalizationRegistry": false
}
},
"size-limit": [
@ -65,7 +66,7 @@
},
{
"path": "./signals.js",
"limit": "12 kB",
"limit": "12.5 kB",
"gzip": false,
"brotli": false
},
@ -77,7 +78,7 @@
},
{
"path": "./index-with-signals.js",
"limit": "5.25 kB"
"limit": "5.5 kB"
}
],
"modifyEsbuildConfig": {
@ -91,11 +92,11 @@
"typescript"
],
"devDependencies": {
"@size-limit/preset-small-lib": "~11.0",
"@size-limit/preset-small-lib": "~11.2",
"dts-bundler": "~0.1",
"editorconfig-checker": "~6.0",
"esbuild": "~0.24",
"jsdom": "~25.0",
"esbuild": "~0.25",
"jsdom": "~26.0",
"jshint": "~2.13",
"nodejsscript": "^1.0.2",
"size-limit-node-esbuild": "~0.3"

12
signals.d.ts vendored
View File

@ -1,4 +1,12 @@
export type Signal<V, A>= (set?: V)=> V & A;
export interface Signal<V, A> {
/** The current value of the signal */
get(): V;
/** Set new value of the signal */
set(value: V): V;
toJSON(): V;
valueOf(): V;
}
type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void;
//type SymbolSignal= Symbol;
type SymbolOnclear= symbol;
@ -27,7 +35,7 @@ interface signal{
* ```js
* const name= S("Jan");
* const surname= S("Andrle");
* const fullname= S(()=> name()+" "+surname());
* const fullname= S(()=> name.get()+" "+surname.get());
* ```
* @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.

View File

@ -1,4 +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";
export { signal, S, isSignal } from "./src/signals-lib/signals-lib.js";
import { signals_config } from "./src/signals-lib/signals-lib.js";
import { registerReactivity } from "./src/signals-lib/common.js";
registerReactivity(signals_config);

View File

@ -1,6 +1,15 @@
import { keyLTE, evc, evd, eva } from "./dom-common.js";
import { scope } from "./dom.js";
import { c_ch_o } from "./events-observer.js";
/**
* Renders content into a custom element or shadow root
*
* @param {Element|ShadowRoot} target - The custom element or shadow root to render into
* @param {Function} render - The render function that returns content
* @param {Function|Object} [props=observedAttributes] - Props to pass to the render function
* @returns {Node} The rendered content
*/
export function customElementRender(target, render, props= observedAttributes){
const custom_element= target.host || target;
scope.push({
@ -17,6 +26,13 @@ export function customElementRender(target, render, props= observedAttributes){
scope.pop();
return target.append(out);
}
/**
* Transforms custom element lifecycle callbacks into events
*
* @param {Function|Object} class_declaration - Custom element class or instance
* @returns {Function|Object} The modified class or instance
*/
export function lifecyclesToEvents(class_declaration){
wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail){
target.apply(thisArg, detail);
@ -38,12 +54,30 @@ export function lifecyclesToEvents(class_declaration){
class_declaration.prototype[keyLTE]= true;
return class_declaration;
}
/** Public API */
export { lifecyclesToEvents as customElementWithDDE };
/**
* Wraps a method with a proxy to intercept calls
*
* @param {Object} obj - Object containing the method
* @param {string} method - Method name to wrap
* @param {Function} apply - Function to execute when method is called
* @private
*/
function wrapMethod(obj, method, apply){
obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
}
import { observedAttributes as oA } from "./helpers.js";
/**
* Gets observed attributes for a custom element
*
* @param {Element} instance - Custom element instance
* @returns {Object} Object mapping camelCase attribute names to their values
*/
export function observedAttributes(instance){
return oA(instance, (i, n)=> i.getAttribute(n));
}

View File

@ -1,3 +1,15 @@
/**
* Environment configuration and globals for the library
* @typedef {Object} Environment
* @property {typeof setDeleteAttr} setDeleteAttr - Function to safely set or delete attributes
* @property {string} ssr - Server-side rendering flag
* @property {Document} D - Document global
* @property {typeof DocumentFragment} F - DocumentFragment constructor
* @property {typeof HTMLElement} H - HTMLElement constructor
* @property {typeof SVGElement} S - SVGElement constructor
* @property {typeof MutationObserver} M - MutationObserver constructor
* @property {Function} q - Promise wrapper for Promse queue feature
*/
export const enviroment= {
setDeleteAttr,
ssr: "",
@ -8,26 +20,43 @@ export const enviroment= {
M: globalThis.MutationObserver,
q: p=> p || Promise.resolve(),
};
import { isUndef } from './helpers.js';
function setDeleteAttr(obj, prop, val){
/* Issue
For some native attrs you can unset only to set empty string.
This can be confusing as it is seen in inspector `<… id=""`.
Options:
1. Leave it, as it is native behaviour
2. Sets as empty string and removes the corresponding attribute when also has empty string
3. (*) Sets as undefined and removes the corresponding attribute when "undefined" string discovered
4. Point 2. with checks for coincidence (e.g. use special string)
import { isInstance, isUndef } from './helpers.js';
/**
* Handles attribute setting with special undefined handling
*
* @param {Object} obj - The object to set the property on
* @param {string} prop - The property name
* @param {any} val - The value to set
* @returns {void}
*
* Issue:
* For some native attrs you can unset only to set empty string.
* This can be confusing as it is seen in inspector `<… id=""`.
* Options:
* 1. Leave it, as it is native behaviour
* 2. Sets as empty string and removes the corresponding attribute when also has empty string
* 3. (*) Sets as undefined and removes the corresponding attribute when "undefined" string discovered
* 4. Point 2. with checks for coincidence (e.g. use special string)
*/
function setDeleteAttr(obj, prop, val){
Reflect.set(obj, prop, val);
if(!isUndef(val)) return;
Reflect.deleteProperty(obj, prop);
if(obj instanceof enviroment.H && obj.getAttribute(prop)==="undefined")
if(isInstance(obj, enviroment.H) && obj.getAttribute(prop)==="undefined")
return obj.removeAttribute(prop);
if(Reflect.get(obj, prop)==="undefined")
return Reflect.set(obj, prop, "");
}
/** Property key for tracking lifecycle events */
export const keyLTE= "__dde_lifecyclesToEvents"; //boolean
/** Event name for connected lifecycle event */
export const evc= "dde:connected";
/** Event name for disconnected lifecycle event */
export const evd= "dde:disconnected";
/** Event name for attribute changed lifecycle event */
export const eva= "dde:attributeChanged";

View File

@ -1,38 +1,104 @@
import { signals } from "./signals-common.js";
import { signals } from "./signals-lib/common.js";
import { enviroment as env } from './dom-common.js';
import { isInstance, isUndef, oAssign } from "./helpers.js";
//TODO: add type, docs ≡ make it public
/**
* Queues a promise, this is helpful for crossplatform components (on server side we can wait for all registered
* promises to be resolved before rendering).
* @param {Promise} promise - Promise to process
* @returns {Promise} Processed promise
*/
export function queue(promise){ return env.q(promise); }
/** @type {{ scope: object, prevent: boolean, host: function }[]} */
/**
* Array of scope contexts for tracking component hierarchies
* @type {{ scope: object, prevent: boolean, host: function }[]}
*/
const scopes= [ {
get scope(){ return env.D.body; },
host: c=> c ? c(env.D.body) : env.D.body,
prevent: true,
} ];
/**
* Scope management utility for tracking component hierarchies
*/
export const scope= {
/**
* Gets the current scope
* @returns {Object} Current scope context
*/
get current(){ return scopes[scopes.length-1]; },
/**
* Gets the host element of the current scope
* @returns {Function} Host accessor function
*/
get host(){ return this.current.host; },
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
preventDefault(){
const { current }= this;
current.prevent= true;
return current;
},
/**
* Gets a copy of the current scope stack
* @returns {Array} Copy of scope stack
*/
get state(){ return [ ...scopes ]; },
push(s= {}){ return scopes.push(Object.assign({}, this.current, { prevent: false }, s)); },
/**
* Pushes a new scope to the stack
* @param {Object} [s={}] - Scope object to push
* @returns {number} New length of the scope stack
*/
push(s= {}){ return scopes.push(oAssign({}, this.current, { prevent: false }, s)); },
/**
* Pushes the root scope to the stack
* @returns {number} New length of the scope stack
*/
pushRoot(){ return scopes.push(scopes[0]); },
/**
* Pops the current scope from the stack
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
*/
pop(){
if(scopes.length===1) return;
return scopes.pop();
},
};
//NOTE: following chainableAppend implementation is OK as the ElementPrototype.append description already is { writable: true, enumerable: true, configurable: true } // editorconfig-checker-disable-line
/**
* Chainable append function for elements
* @private
*/
function append(...els){ this.appendOriginal(...els); return this; }
/**
* Makes an element's append method chainable. NOTE: following chainableAppend implementation is OK as the
* ElementPrototype.append description already is { writable: true, enumerable: true, configurable: true }
* @param {Element} el - Element to modify
* @returns {Element} Modified element
*/
export function chainableAppend(el){
if(el.append===append) return el; el.appendOriginal= el.append; el.append= append; return el;
}
/** Current namespace for element creation */
let namespace;
/**
* Creates a DOM element with specified tag, attributes and addons
*
* @param {string|Function} tag - Element tag name or component function
* @param {Object|string|number} [attributes] - Element attributes
* @param {...Function} addons - Functions to call with the created element
* @returns {Element|DocumentFragment} Created element
*/
export function createElement(tag, attributes, ...addons){
/* jshint maxcomplexity: 15 */
const s= signals(this);
@ -47,7 +113,7 @@ export function createElement(tag, attributes, ...addons){
(scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined);
scope.push({ scope: tag, host });
el= tag(attributes || undefined);
const is_fragment= el instanceof env.F;
const is_fragment= isInstance(el, env.F);
if(el.nodeName==="#comment") break;
const el_mark= createElement.mark({
type: "component",
@ -70,10 +136,15 @@ export function createElement(tag, attributes, ...addons){
scoped= 2;
return el;
}
/**
* @param { { type: "component", name: string, host: "this" | "parentElement" } | { type: "reactive" | "later" } } attrs
* @param {boolean} [is_open=false]
* */
* Creates a marker comment for elements
*
* @param {{ type: "component"|"reactive"|"later", name?: string, host?: "this"|"parentElement" }} attrs - Marker
* attributes
* @param {boolean} [is_open=false] - Whether the marker is open-ended
* @returns {Comment} Comment node marker
*/
createElement.mark= function(attrs, is_open= false){
attrs= Object.entries(attrs).map(([ n, v ])=> n+`="${v}"`).join(" ");
const end= is_open ? "" : "/";
@ -81,8 +152,16 @@ createElement.mark= function(attrs, is_open= false){
if(is_open) out.end= env.D.createComment("</dde:mark>");
return out;
};
/** Alias for createElement */
export { createElement as el };
//TODO?: const namespaceHelper= ns=> ns==="svg" ? "http://www.w3.org/2000/svg" : ns;
/**
* Creates a namespaced element creation function
*
* @param {string} ns - Namespace URI
* @returns {Function} Element creation function for the namespace
*/
export function createElementNS(ns){
const _this= this;
return function createElementNSCurried(...rest){
@ -92,9 +171,17 @@ export function createElementNS(ns){
return el;
};
}
/** Alias for createElementNS */
export { createElementNS as elNS };
/** @param {HTMLElement} element @param {HTMLElement} [root] */
/**
* Simulates slot functionality for elements
*
* @param {HTMLElement} element - Parent element
* @param {HTMLElement} [root=element] - Root element containing slots
* @returns {HTMLElement} The root element
*/
export function simulateSlots(element, root= element){
const mark_e= "¹⁰", mark_s= "✓"; //NOTE: Markers to identify slots processed by this function. Also “prevents” native behavior as it is unlikely to use these in names. // editorconfig-checker-disable-line
const slots= Object.fromEntries(
@ -128,17 +215,34 @@ export function simulateSlots(element, root= element){
return root;
}
/** Store for element assignment contexts */
const assign_context= new WeakMap();
const { setDeleteAttr }= env;
/**
* Assigns attributes to an element
*
* @param {Element} element - Element to assign attributes to
* @param {...Object} attributes - Attribute objects to assign
* @returns {Element} The element with attributes assigned
*/
export function assign(element, ...attributes){
if(!attributes.length) return element;
assign_context.set(element, assignContext(element, this));
for(const [ key, value ] of Object.entries(Object.assign({}, ...attributes)))
for(const [ key, value ] of Object.entries(oAssign({}, ...attributes)))
assignAttribute.call(this, element, key, value);
assign_context.delete(element);
return element;
}
/**
* Assigns a single attribute to an element
*
* @param {Element} element - Element to assign attribute to
* @param {string} key - Attribute name
* @param {any} value - Attribute value
* @returns {any} Result of the attribute assignment
*/
export function assignAttribute(element, key, value){
const { setRemoveAttr, s }= assignContext(element, this);
const _this= this;
@ -170,13 +274,28 @@ export function assignAttribute(element, key, value){
}
return isPropSetter(element, key) ? setDeleteAttr(element, key, value) : setRemoveAttr(key, value);
}
/**
* Gets or creates assignment context for an element
*
* @param {Element} element - Element to get context for
* @param {Object} _this - Context object
* @returns {Object} Assignment context
* @private
*/
function assignContext(element, _this){
if(assign_context.has(element)) return assign_context.get(element);
const is_svg= element instanceof env.S;
const is_svg= isInstance(element, env.S);
const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
const s= signals(_this);
return { setRemoveAttr, s };
}
/**
* Applies a declarative classList object to an element
*
* @param {Element} element - Element to apply classes to
* @param {Object} toggle - Object with class names as keys and boolean values
* @returns {Element} The element with classes applied
*/
export function classListDeclarative(element, toggle){
const s= signals(this);
forEachEntries(s, "classList", element, toggle,
@ -184,18 +303,45 @@ export function classListDeclarative(element, toggle){
element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val)) );
return element;
}
/**
* Generic element attribute manipulation
*
* @param {Element} element - Element to manipulate
* @param {string} op - Operation ("set" or "remove")
* @param {string} key - Attribute name
* @param {any} [value] - Attribute value
* @returns {void}
*/
export function elementAttribute(element, op, key, value){
if(element instanceof env.H)
if(isInstance(element, env.H))
return element[op+"Attribute"](key, value);
return element[op+"AttributeNS"](null, key, value);
}
import { isUndef } from "./helpers.js";
//TODO: add cache? `(Map/Set)<el.tagName+key,isUndef>`
/**
* Checks if a property can be set on an element
*
* @param {Element} el - Element to check
* @param {string} key - Property name
* @returns {boolean} Whether the property can be set
* @private
*/
function isPropSetter(el, key){
if(!(key in el)) return false;
const des= getPropDescriptor(el, key);
return !isUndef(des.set);
}
/**
* Gets a property descriptor from a prototype chain
*
* @param {Object} p - Prototype object
* @param {string} key - Property name
* @returns {PropertyDescriptor} Property descriptor
* @private
*/
function getPropDescriptor(p, key){
p= Object.getPrototypeOf(p);
if(!p) return {};
@ -224,9 +370,44 @@ function forEachEntries(s, target, element, obj, cb){
});
}
/**
* Sets or removes an attribute based on value
*
* @param {Element} obj - Element to modify
* @param {string} prop - Property suffix ("Attribute")
* @param {string} key - Attribute name
* @param {any} val - Attribute value
* @returns {void}
* @private
*/
function setRemove(obj, prop, key, val){
return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val); }
return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val);
}
/**
* Sets or removes a namespaced attribute based on value
*
* @param {Element} obj - Element to modify
* @param {string} prop - Property suffix ("Attribute")
* @param {string} key - Attribute name
* @param {any} val - Attribute value
* @param {string|null} [ns=null] - Namespace URI
* @returns {void}
* @private
*/
function setRemoveNS(obj, prop, key, val, ns= null){
return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val); }
return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val);
}
/**
* Sets or deletes a property based on value
*
* @param {Object} obj - Object to modify
* @param {string} key - Property name
* @param {any} val - Property value
* @returns {void}
* @private
*/
function setDelete(obj, key, val){
Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key); }
Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key);
}

View File

@ -1,11 +1,27 @@
import { enviroment as env, evc, evd } from './dom-common.js';
import { isInstance } from "./helpers.js";
/**
* Connection changes observer for tracking element connection/disconnection
* Falls back to a dummy implementation if MutationObserver is not available
*/
export const c_ch_o= env.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get(){ return ()=> {}; }
});
/**
* Creates an observer that tracks elements being connected to and disconnected from the DOM
* @returns {Object} Observer with methods to register element listeners
*/
function connectionsChangesObserverConstructor(){
const store= new Map();
let is_observing= false;
/**
* Creates a mutation observer callback
* @param {Function} stop - Function to stop observation when no longer needed
* @returns {Function} MutationObserver callback
*/
const observerListener= stop=> function(mutations){
for(const mutation of mutations){
if(mutation.type!=="childList") continue;
@ -17,13 +33,26 @@ function connectionsChangesObserverConstructor(){
stop();
}
};
const observer= new env.M(observerListener(stop));
return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element){
const o= new env.M(observerListener(()=> {}));
o.observe(element, { childList: true, subtree: true });
return ()=> o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener){
start();
const listeners= getElementStore(element);
@ -31,6 +60,12 @@ function connectionsChangesObserverConstructor(){
listeners.connected.add(listener);
listeners.length_c+= 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener){
if(!store.has(element)) return;
const ls= store.get(element);
@ -39,6 +74,12 @@ function connectionsChangesObserverConstructor(){
ls.length_c-= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener){
start();
const listeners= getElementStore(element);
@ -46,21 +87,38 @@ function connectionsChangesObserverConstructor(){
listeners.disconnected.add(listener);
listeners.length_d+= 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener){
if(!store.has(element)) return;
const ls= store.get(element);
if(!ls.disconnected.has(listener)) return;
ls.disconnected.delete(listener);
ls.length_d-= 1;
cleanWhenOff(element, ls);
}
};
/**
* Cleanup element tracking when all listeners are removed
* @param {Element} element - Element to potentially remove from tracking
* @param {Object} ls - Element's listener store
*/
function cleanWhenOff(element, ls){
if(ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
/**
* Gets or creates a store for element listeners
* @param {Element} element - Element to get store for
* @returns {Object} Listener store for the element
*/
function getElementStore(element){
if(store.has(element)) return store.get(element);
const out= {
@ -72,32 +130,58 @@ function connectionsChangesObserverConstructor(){
store.set(element, out);
return out;
}
/**
* Start observing DOM changes
*/
function start(){
if(is_observing) return;
is_observing= true;
observer.observe(env.D.body, { childList: true, subtree: true });
}
/**
* Stop observing DOM changes when no longer needed
*/
function stop(){
if(!is_observing || store.size) return;
is_observing= false;
observer.disconnect();
}
//TODO: remount support?
/**
* Schedule a task during browser idle time
* @returns {Promise<void>} Promise that resolves when browser is idle
*/
function requestIdle(){ return new Promise(function(resolve){
(requestIdleCallback || requestAnimationFrame)(resolve);
}); }
/**
* Collects child elements from the store that are contained by the given element
* @param {Element} element - Parent element
* @returns {Promise<Element[]>} Promise resolving to array of child elements
*/
async function collectChildren(element){
if(store.size > 30)//TODO?: limit
await requestIdle();
const out= [];
if(!(element instanceof Node)) return out;
if(!isInstance(element, Node)) return out;
for(const el of store.keys()){
if(el===element || !(el instanceof Node)) continue;
if(el===element || !isInstance(el, Node)) continue;
if(element.contains(el))
out.push(el);
}
return out;
}
/**
* Process nodes added to the DOM
* @param {NodeList} addedNodes - Nodes that were added
* @param {boolean} is_root - Whether these are root-level additions
* @returns {boolean} Whether any relevant elements were processed
*/
function observerAdded(addedNodes, is_root){
let out= false;
for(const element of addedNodes){
@ -115,6 +199,13 @@ function connectionsChangesObserverConstructor(){
}
return out;
}
/**
* Process nodes removed from the DOM
* @param {NodeList} removedNodes - Nodes that were removed
* @param {boolean} is_root - Whether these are root-level removals
* @returns {boolean} Whether any relevant elements were processed
*/
function observerRemoved(removedNodes, is_root){
let out= false;
for(const element of removedNodes){
@ -128,6 +219,12 @@ function connectionsChangesObserverConstructor(){
}
return out;
}
/**
* Creates a function to dispatch the disconnect event
* @param {Element} element - Element that was removed
* @returns {Function} Function to dispatch event after confirming disconnection
*/
function dispatchRemove(element){
return ()=> {
if(element.isConnected) return;

View File

@ -1,6 +1,15 @@
export { registerReactivity } from './signals-common.js';
export { registerReactivity } from './signals-lib/common.js';
import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js';
import { oAssign, onAbort } from './helpers.js';
/**
* Creates a function to dispatch events on elements
*
* @param {string} name - Event name
* @param {Object} [options] - Event options
* @param {Element|Function} [host] - Host element or function returning host element
* @returns {Function} Function that dispatches the event
*/
export function dispatchEvent(name, options, host){
if(!options) options= {};
return function dispatch(element, ...d){
@ -9,10 +18,19 @@ export function dispatchEvent(name, options, host){
element= typeof host==="function"? host() : host;
}
//TODO: what about re-emmitting?
const event= d.length ? new CustomEvent(name, Object.assign({ detail: d[0] }, options)) : new Event(name, options);
const event= d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
};
}
/**
* Creates a function to register event listeners on elements
*
* @param {string} event - Event name
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the listener
*/
export function on(event, listener, options){
return function registerElement(element){
element.addEventListener(event, listener, options);
@ -21,10 +39,23 @@ export function on(event, listener, options){
}
import { c_ch_o } from "./events-observer.js";
import { onAbort } from './helpers.js';
const lifeOptions= obj=> Object.assign({}, typeof obj==="object" ? obj : null, { once: true });
/**
* Prepares lifecycle event options with once:true default
* @private
*/
const lifeOptions= obj=> oAssign({}, typeof obj==="object" ? obj : null, { once: true });
//TODO: cleanUp when event before abort?
//TODO: docs (e.g.) https://nolanlawson.com/2024/01/13/web-component-gotcha-constructor-vs-connectedcallback/
/**
* Creates a function to register connected lifecycle event listeners
*
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the connected listener
*/
on.connected= function(listener, options){
options= lifeOptions(options);
return function registerElement(element){
@ -37,6 +68,14 @@ on.connected= function(listener, options){
return element;
};
};
/**
* Creates a function to register disconnected lifecycle event listeners
*
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the disconnected listener
*/
on.disconnected= function(listener, options){
options= lifeOptions(options);
return function registerElement(element){
@ -48,7 +87,16 @@ on.disconnected= function(listener, options){
return element;
};
};
/** Store for disconnect abort controllers */
const store_abort= new WeakMap();
/**
* Creates an AbortController that triggers when the element disconnects
*
* @param {Element|Function} host - Host element or function taking an element
* @returns {AbortController} AbortController that aborts on disconnect
*/
on.disconnectedAsAbort= function(host){
if(store_abort.has(host)) return store_abort.get(host);
@ -57,7 +105,17 @@ on.disconnectedAsAbort= function(host){
host(on.disconnected(()=> a.abort()));
return a;
};
/** Store for elements with attribute observers */
const els_attribute_store= new WeakSet();
/**
* Creates a function to register attribute change event listeners
*
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the attribute change listener
*/
on.attributeChanged= function(listener, options){
if(typeof options !== "object")
options= {};

View File

@ -1,13 +1,43 @@
/**
* Safe method to check if an object has a specific property
* @param {...any} a - Arguments to pass to Object.prototype.hasOwnProperty.call
* @returns {boolean} Result of hasOwnProperty check
*/
export const hasOwn= (...a)=> Object.prototype.hasOwnProperty.call(...a);
/**
* Checks if a value is undefined
* @param {any} value - The value to check
* @returns {boolean} True if the value is undefined
*/
export function isUndef(value){ return typeof value==="undefined"; }
/**
* Enhanced typeof that handles null and objects better
* @param {any} v - The value to check
* @returns {string} Type as a string
*/
export function typeOf(v){
const t= typeof v;
if(t!=="object") return t;
if(v===null) return "null";
return Object.prototype.toString.call(v);
}
export function isInstance(obj, cls){ return obj instanceof cls; }
/** @type {typeof Object.prototype.isPrototypeOf.call} */
export function isProtoFrom(obj, cls){ return Object.prototype.isPrototypeOf.call(cls, obj); }
export function oCreate(proto= null, p= {}){ return Object.create(proto, p); }
export function oAssign(...o){ return Object.assign(...o); }
/**
* Handles AbortSignal registration and cleanup
* @param {AbortSignal} signal - The AbortSignal to listen to
* @param {Function} listener - The abort event listener
* @returns {Function|undefined|boolean} Cleanup function or undefined if already aborted
*/
export function onAbort(signal, listener){
if(!signal || !(signal instanceof AbortSignal))
if(!signal || !isInstance(signal, AbortSignal))
return true;
if(signal.aborted)
return;
@ -16,6 +46,13 @@ export function onAbort(signal, listener){
signal.removeEventListener("abort", listener);
};
}
/**
* Processes observed attributes for custom elements
* @param {object} instance - The custom element instance
* @param {Function} observedAttribute - Function to process each attribute
* @returns {object} Object with processed attributes
*/
export function observedAttributes(instance, observedAttribute){
const { observedAttributes= [] }= instance.constructor;
return observedAttributes
@ -24,4 +61,28 @@ export function observedAttributes(instance, observedAttribute){
return out;
}, {});
}
/**
* Converts kebab-case strings to camelCase
* @param {string} name - The kebab-case string
* @returns {string} The camelCase string
*/
function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); }
/**
* Error class for definition tracking
* Shows the correct stack trace for debugging (signal) creation
*/
export class Defined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4);
const curr_lib= curr_file.includes("src/helpers.js") ? "src/" : curr_file;
this.stack= rest.find(l=> !l.includes(curr_lib)) || curr;
}
get compact(){
const { stack }= this;
return stack.slice(0, stack.indexOf("@")+1)+"…"+stack.slice(stack.lastIndexOf("/"));
}
}

View File

@ -1,292 +0,0 @@
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<function, Set<ddeSignal<any, any>>>`
* 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<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|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 wasnt 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;
}

View File

@ -1,13 +0,0 @@
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;
}

View File

@ -1,293 +0,0 @@
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<function, Set<ddeSignal<any, any>>>`
* 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<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|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
// TODO: clear cache on disconnect
// TODO: extract cache to separate (exportable) function
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 wasnt 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;
current.host(function(element){
if(element[key_reactive])
return element[key_reactive].push([ [ s, listener ], ...notes ]);
element[key_reactive]= [];
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
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);
});
}
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;
}

44
src/signals-lib/common.js Normal file
View File

@ -0,0 +1,44 @@
import { isProtoFrom, oAssign } from "../helpers.js";
/**
* Global signals object with default implementation
* @type {Object}
*/
export const signals_global= {
/**
* Checks if a value is a signal
* @param {any} attributes - Value to check
* @returns {boolean} Whether the value is a signal
*/
isSignal(attributes){ return false; },
/**
* Processes an attribute that might be reactive
* @param {Element} obj - Element that owns the attribute
* @param {string} key - Attribute name
* @param {any} attr - Attribute value
* @param {Function} set - Function to set the attribute
* @returns {any} Processed attribute value
*/
processReactiveAttribute(obj, key, attr, set){ return attr; },
};
/**
* Registers a reactivity implementation
* @param {Object} def - Reactivity implementation
* @param {boolean} [global=true] - Whether to set globally or create a new implementation
* @returns {Object} The registered reactivity implementation
*/
export function registerReactivity(def, global= true){
if(global) return oAssign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
/**
* Gets the signals implementation from a context
* @param {unknown} _this - Context to check for signals implementation
* @returns {typeof signals_global} Signals implementation
*/
export function signals(_this){
return isProtoFrom(_this, signals_global) && _this!==signals_global ? _this : signals_global;
}

View File

@ -0,0 +1,39 @@
/**
* Symbol used to identify signals in objects
* @type {string}
*/
export const mark= "__dde_signal";
/**
* Batches signal updates to improve performance
* @type {Function}
*/
export const queueSignalWrite= (()=> {
let pendingSignals= new Set();
let scheduled= false;
/**
* Processes all pending signal updates
* @private
*/
function flushSignals() {
scheduled = false;
const todo= pendingSignals;
pendingSignals= new Set();
for(const signal of todo){
const M = signal[mark];
if(M) M.listeners.forEach(l => l(M.value));
}
}
/**
* Queues a signal for update
* @param {Object} s - Signal to queue
*/
return function(s){
pendingSignals.add(s);
if(scheduled) return;
scheduled = true;
queueMicrotask(flushSignals);
};
})();

View File

@ -1,4 +1,4 @@
import type { Action, Actions, signal as s, Signal, SymbolOnclear } from "../signals.d.ts";
import type { Action, Actions, signal as s, Signal, SymbolOnclear } from "../../signals.js";
export { Action, Actions, Signal, SymbolOnclear };
export const S: s;
export const signal: s;

View File

@ -0,0 +1,487 @@
import { queueSignalWrite, mark } from "./helpers.js";
export { mark };
import { hasOwn, Defined, oCreate, isProtoFrom, oAssign } from "../helpers.js";
const Signal = oCreate(null, {
get: { value(){ return read(this); } },
set: { value(...v){ return write(this, ...v); } },
toJSON: { value(){ return read(this); } },
valueOf: { value(){ return this[mark] && this[mark].value; } }
});
const SignalReadOnly= oCreate(Signal, {
set: { value(){ return; } },
});
/**
* Checks if a value is a signal
*
* @param {any} candidate - Value to check
* @returns {boolean} True if the value is a signal
*/
export function isSignal(candidate){
return isProtoFrom(candidate, Signal);
}
/**
* Stack for tracking nested signal computations
* @type {function[]}
*/
const stack_watch= [];
/**
* Dependencies tracking map for signals
*
* ### `WeakMap<function, Set<ddeSignal<any, any>>>`
* 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<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|function>}
*/
const deps= new WeakMap();
/**
* Creates a new signal or converts a function into a derived signal
*
* @param {any|function} value - Initial value or function that computes the value
* @param {Object} [actions] - Custom actions for the signal
* @returns {Object} Signal object with get() and set() methods
*/
export function signal(value, actions){
if(typeof value!=="function")
return create(false, value, actions);
if(isSignal(value)) return value;
const out= create(true);
/**
* Updates the derived signal when dependencies change
* @private
*/
function contextReWatch(){
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;
}
/** Alias for signal */
export { signal as S };
/**
* Calls a custom action on a signal
*
* @param {Object} s - Signal object to call action on
* @param {string} name - Action name
* @param {...any} a - Arguments to pass to the action
*/
signal.action= function(s, name, ...a){
const M= s[mark];
if(!M) return;
const { actions }= M;
if(!actions || !hasOwn(actions, name))
throw new Error(`Action "${name}" not defined. See ${mark}.actions.`);
actions[name].apply(M, a);
if(M.skip) return (delete M.skip);
queueSignalWrite(s);
};
/**
* Subscribes a listener to signal changes
*
* @param {Object|Object[]} s - Signal object or array of signal objects to subscribe to
* @param {function} listener - Callback function receiving signal value
* @param {Object} [options={}] - Subscription options
* @param {AbortSignal} [options.signal] - Signal to abort subscription
*/
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));
};
/**
* Symbol constants for signal internals
*/
signal.symbols= {
//signal: mark,
onclear: Symbol.for("Signal.onclear")
};
/**
* Cleans up signals and their dependencies
*
* @param {...Object} signals - Signal objects to clean up
*/
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];
}
/**
* Cleans up signal dependencies
* @param {function} s - Signal being cleared
* @param {Object} o - Signal metadata
* @private
*/
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);
});
}
};
/** Property key for tracking reactive elements */
const key_reactive= "__dde_reactive";
import { enviroment as env } from "../dom-common.js";
import { el } from "../dom.js";
import { scope } from "../dom.js";
import { on } from "../events.js";
export function cache(store= oCreate()){
return (key, fun)=> hasOwn(store, key) ? store[key] : (store[key]= fun());
}
/**
* Creates a reactive DOM element that re-renders when signal changes
*
* @TODO Third argument for handle `cache_tmp` in re-render
* @param {Object} s - Signal object to watch
* @param {Function} map - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements
*/
signal.el= function(s, map){
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
let cache_shared= oCreate();
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
const memo= cache(cache_shared);
cache_shared= oCreate();
scope.push(current);
let els= map(v, function useCache(key, fun){
return (cache_shared[key]= memo(key, fun));
});
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.get());
current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */
cache_shared= {}
));
return out;
};
/**
* Cleans up reactive elements that are no longer connected
*
* @param {Element} host - Host element containing reactive elements
* @private
*/
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 { observedAttributes } from "../helpers.js";
/**
* Actions for observed attribute signals
* @private
*/
const observedAttributeActions= {
_set(value){ this.value= value; },
};
/**
* Creates a function that returns signals for element attributes
*
* @param {Object} store - Storage object for attribute signals
* @returns {Function} Function creating attribute signals
* @private
*/
function observedAttribute(store){
return function(instance, name){
const varS= oCreate(Signal, {
set: { value(...v){ return instance.setAttribute(name, ...v); } }
});
const out= toSignal(varS, instance.getAttribute(name), observedAttributeActions);
store[name]= out;
return out;
};
}
/** Property key for storing attribute signals */
const key_attributes= "__dde_attributes";
/**
* Creates signals for observed attributes in custom elements
*
* @param {Element} element - Custom element instance
* @returns {Object} Object with attribute signals
*/
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';
/**
* Signal configuration for the library
* Implements processReactiveAttribute to handle signal-based attributes
*/
export const signals_config= {
isSignal,
/**
* Processes attributes that might be signals
*
* @param {Element} element - Element with the attribute
* @param {string} key - Attribute name
* @param {any} attrs - Attribute value (possibly a signal)
* @param {Function} set - Function to set attribute value
* @returns {any} Processed attribute value
*/
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.get();
}
};
/**
* Registers signal listener for cleanup when element is removed
*
* @param {Object} s - Signal object to track
* @param {Function} listener - Signal listener
* @param {...any} notes - Additional context information
* @private
*/
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
current.host(function(element){
if(element[key_reactive])
return element[key_reactive].push([ [ s, listener ], ...notes ]);
element[key_reactive]= [];
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
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);
});
}
/**
* Registry for cleaning up signals when they are garbage collected
* @type {FinalizationRegistry}
*/
const cleanUpRegistry = new FinalizationRegistry(function(s){
signal.clear({ [mark]: s });
});
/**
* Creates a new signal object
*
* @param {boolean} is_readonly - Whether the signal is readonly
* @param {any} value - Initial signal value
* @param {Object} actions - Custom actions for the signal
* @returns {Object} Signal object with get() and set() methods
* @private
*/
function create(is_readonly, value, actions){
const varS = oCreate(is_readonly ? SignalReadOnly : Signal);
const SI= toSignal(varS, value, actions, is_readonly);
cleanUpRegistry.register(SI, SI[mark]);
return SI;
}
/**
* Prototype for signal internal objects
* @private
*/
const protoSigal= oAssign(oCreate(), {
/**
* Prevents signal propagation
*/
stopPropagation(){
this.skip= true;
}
});
/**
* Transforms an object into a signal
*
* @param {Object} s - Object to transform
* @param {any} value - Initial value
* @param {Object} actions - Custom actions
* @param {boolean} [readonly=false] - Whether the signal is readonly
* @returns {Object} Signal object with get() and set() methods
* @private
*/
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: oAssign(oCreate(protoSigal), {
value, actions, onclear, host,
listeners: new Set(),
defined: new Defined().stack,
readonly
}),
enumerable: false,
writable: false,
configurable: true
});
return s;
}
/**
* Gets the current computation context
* @returns {function|undefined} Current context function
* @private
*/
function currentContext(){
return stack_watch[stack_watch.length - 1];
}
/**
* Reads a signal's value and tracks dependencies
*
* @param {Object} s - Signal object to read
* @returns {any} Signal value
* @private
*/
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;
}
/**
* Writes a new value to a signal
*
* @param {Object} s - Signal object to update
* @param {any} value - New value
* @param {boolean} [force=false] - Force update even if value is unchanged
* @returns {any} The new value
* @private
*/
function write(s, value, force){
const M= s[mark];
if(!M || (!force && M.value===value)) return;
M.value= value;
queueSignalWrite(s);
return value;
}
/**
* Adds a listener to a signal
*
* @param {Object} s - Signal object to listen to
* @param {Function} listener - Callback function
* @returns {Set} Listener set
* @private
*/
function addSignalListener(s, listener){
if(!s[mark]) return;
return s[mark].listeners.add(listener);
}
/**
* Removes a listener from a signal
*
* @param {Object} s - Signal object to modify
* @param {Function} listener - Listener to remove
* @param {boolean} [clear_when_empty] - Whether to clear the signal when no listeners remain
* @returns {boolean} Whether the listener was found and removed
* @private
*/
function removeSignalListener(s, listener, clear_when_empty){
const M= s[mark];
if(!M) return;
const { listeners: L }= M;
const out= L.delete(listener);
if(!out || !clear_when_empty || L.size) return out;
signal.clear(s);
const depList= deps.get(M);
if(!depList) return out;
const depSource= deps.get(depList);
if(!depSource) return out;
for(const sig of depSource) removeSignalListener(sig, depList, true);
return out;
}