1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-04-01 19:55:53 +02:00

Compare commits

...

7 Commits

Author SHA1 Message Date
57a5ff2dfe irelands 2025-03-06 18:33:12 +01:00
b3356afa88 🔤 improvemens 2025-03-06 14:33:47 +01:00
da4e3e52d9 issignal 2025-03-06 11:44:13 +01:00
6c297672c1 🔤 2025-03-06 11:42:56 +01:00
66fdee2c05 🔤 ui/ux 2025-03-06 10:59:41 +01:00
963ed53c84 🔤 2025-03-06 10:59:31 +01:00
59efa84494 🔤 scopes 2025-03-06 09:41:29 +01:00
40 changed files with 1027 additions and 316 deletions

View File

@ -4,7 +4,7 @@ echo("Building static documentation files…");
echo("Preparing…");
import { path_target, pages as pages_registered, styles, dispatchEvent, t } from "../docs/ssr.js";
import { createHTMl } from "./docs/jsdom.js";
import { register } from "../jsdom.js";
import { register, queue } from "../jsdom.js";
const pkg= s.cat("package.json").xargs(JSON.parse);
if(s.test("-d", path_target.root)){
@ -31,6 +31,7 @@ for(const { id, info } of pages){
serverDOM.document.body.append(
el(page, { pkg, info }),
);
await queue();
echo.use("-R", `Writing ${id}.html…`);
dispatchEvent("oneachrender", document);

View File

@ -177,11 +177,6 @@ var scope = {
pop() {
if (scopes.length === 1) return;
return scopes.pop();
},
isolate(fn) {
this.push({ prevent: true });
fn();
this.pop();
}
};
function append(...els) {
@ -647,7 +642,7 @@ on.disconnectedAsAbort = function(host) {
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a;
return a.signal;
};
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {
@ -713,7 +708,7 @@ var SignalReadOnly = oCreate(Signal, {
} }
});
function isSignal(candidate) {
return isProtoFrom(candidate, Signal);
return candidate && candidate[mark];
}
var stack_watch = [];
var deps = /* @__PURE__ */ new WeakMap();

File diff suppressed because one or more lines are too long

7
dist/dde.js vendored
View File

@ -154,11 +154,6 @@ var scope = {
pop() {
if (scopes.length === 1) return;
return scopes.pop();
},
isolate(fn) {
this.push({ prevent: true });
fn();
this.pop();
}
};
function append(...els) {
@ -624,7 +619,7 @@ on.disconnectedAsAbort = function(host) {
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a;
return a.signal;
};
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {

8
dist/dde.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -76,7 +76,7 @@ export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>,
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -86,7 +86,7 @@ export function el<
A extends { textContent: ddeStringable },
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>["textContent"],
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -237,8 +237,6 @@ export const scope: {
pushRoot(): ReturnType<Array<Scope>["push"]>,
/** Removes last/current child scope. */
pop(): ReturnType<Array<Scope>["pop"]>,
/** Runs function in a new (isolated) scope */
isolate(fn: Function): void,
};
export function customElementRender<

View File

@ -76,7 +76,7 @@ export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>,
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -86,7 +86,7 @@ export function el<
A extends { textContent: ddeStringable },
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>["textContent"],
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -237,8 +237,6 @@ export const scope: {
pushRoot(): ReturnType<Array<Scope>["push"]>,
/** Removes last/current child scope. */
pop(): ReturnType<Array<Scope>["pop"]>,
/** Runs function in a new (isolated) scope */
isolate(fn: Function): void,
};
export function customElementRender<

View File

@ -175,11 +175,6 @@ var scope = {
pop() {
if (scopes.length === 1) return;
return scopes.pop();
},
isolate(fn) {
this.push({ prevent: true });
fn();
this.pop();
}
};
function append(...els) {
@ -645,7 +640,7 @@ on.disconnectedAsAbort = function(host) {
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a;
return a.signal;
};
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {
@ -711,7 +706,7 @@ var SignalReadOnly = oCreate(Signal, {
} }
});
function isSignal(candidate) {
return isProtoFrom(candidate, Signal);
return candidate && candidate[mark];
}
var stack_watch = [];
var deps = /* @__PURE__ */ new WeakMap();

File diff suppressed because one or more lines are too long

6
dist/esm.d.min.ts vendored
View File

@ -76,7 +76,7 @@ export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>,
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -86,7 +86,7 @@ export function el<
A extends { textContent: ddeStringable },
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>["textContent"],
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -237,8 +237,6 @@ export const scope: {
pushRoot(): ReturnType<Array<Scope>["push"]>,
/** Removes last/current child scope. */
pop(): ReturnType<Array<Scope>["pop"]>,
/** Runs function in a new (isolated) scope */
isolate(fn: Function): void,
};
export function customElementRender<

6
dist/esm.d.ts vendored
View File

@ -76,7 +76,7 @@ export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>,
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -86,7 +86,7 @@ export function el<
A extends { textContent: ddeStringable },
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>["textContent"],
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -237,8 +237,6 @@ export const scope: {
pushRoot(): ReturnType<Array<Scope>["push"]>,
/** Removes last/current child scope. */
pop(): ReturnType<Array<Scope>["pop"]>,
/** Runs function in a new (isolated) scope */
isolate(fn: Function): void,
};
export function customElementRender<

7
dist/esm.js vendored
View File

@ -152,11 +152,6 @@ var scope = {
pop() {
if (scopes.length === 1) return;
return scopes.pop();
},
isolate(fn) {
this.push({ prevent: true });
fn();
this.pop();
}
};
function append(...els) {
@ -622,7 +617,7 @@ on.disconnectedAsAbort = function(host) {
const a = new AbortController();
store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a;
return a.signal;
};
var els_attribute_store = /* @__PURE__ */ new WeakSet();
on.attributeChanged = function(listener, options) {

2
dist/esm.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -35,7 +35,7 @@ ${host} {
line-height: 1.5;
position: relative;
margin-block: 1rem;
width: 100%;
max-width: 100%;
}
/* Light mode overrides to match GitHub-like theme */
@ -236,12 +236,11 @@ function registerClientPart(page_id){
`),
);
// Register our highlighting script to run after Shiki loads
const scriptElement = el("script", { type: "module" });
registerClientFile(
new URL("./code.js.js", import.meta.url),
scriptElement
{
head: el("script", { type: "module" }),
}
);
is_registered[page_id]= true;

View File

@ -5,7 +5,6 @@ ${host} {
grid-column: full-main;
width: calc(100% - .75em);
height: calc(4/6 * var(--body-max-width));
margin: 2rem auto;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
border: 1px solid var(--border);

View File

@ -5,6 +5,7 @@ const greeting = S(() => {
// log derived signals
const log = "Hello, " + name.get();
console.log(log);
console.log(name.valueOf());
return log;
});

View File

@ -0,0 +1,37 @@
import { el } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
const className = "client-side-counter";
document.body.append(
el("style").append(`
.${className} {
border: 1px dashed #ccc;
padding: 1em;
margin: 1em;
}
`.trim())
);
export function CounterStandard() {
// Create reactive state with a signal
const count = S(0);
// Create UI components that react to state changes
return el("div", { className }).append(
el("h4", "Client-Side Counter"),
el("div", {
// The textContent updates automatically when count changes
textContent: S(() => `Count: ${count.get()}`),
}),
el("div", { className: "controls" }).append(
el("button", {
onclick: () => count.set(count.get() - 1),
textContent: "-",
}),
el("button", {
onclick: () => count.set(count.get() + 1),
textContent: "+",
})
)
);
}

View File

@ -21,9 +21,7 @@ function Counter() {
setTimeout(()=> {
// ok, BUT consider extract to separate function
// see section below for more info
scope.push();
const ok= S(0);
scope.pop();
S.on(ok, console.log);
setInterval(()=> ok.set(ok.get() + 1), 100);
}, 100);

View File

@ -0,0 +1,88 @@
import { el, queue } from "deka-dom-el";
import { addEventListener, registerClientFile } from "../ssr.js";
import { relative } from "node:path";
const dir= new URL("./", import.meta.url).pathname;
const dirFE= "irelands";
// Track all component instances for client-side rehydration
const componentsRegistry = new Map();
/**
* Creates a component that shows code and its runtime output
* with server-side pre-rendering and client-side rehydration
*
* @param {object} attrs
* @param {URL} attrs.src - Path to the file containing the component
* @param {string} [attrs.exportName="default"] - Name of the export to use
* @param {string} attrs.page_id - ID of the current page
* @param {object} [attrs.props={}] - Props to pass to the component
*/
export function ireland({ src, exportName = "default", props = {} }) {
// relative src against the current directory
const path= "./"+relative(dir, src.pathname);
const id = "ireland-" + generateComponentId(src);
const element = el.mark({ type: "ireland", name: ireland.name });
queue(import(path).then(module => {
const component = module[exportName];
element.replaceWith(el(component, props, mark(id)));
}));
if(!componentsRegistry.size)
addEventListener("oneachrender", registerClientPart);
componentsRegistry.set(id, {
src,
path: dirFE+"/"+path.split("/").pop(),
exportName,
props,
});
return element;
}
function registerClientPart(){
const todo= Array.from(componentsRegistry.entries())
.map(([ id, d ]) => {
registerClientFile(d.src, {
folder: dirFE,
// not all browsers support importmap
replacer(file){
return file
.replaceAll(/ from "deka-dom-el(\/signals)?";/g, ` from "./esm-with-signals.js";`);
}
});
return [ id, d ];
});
const store = JSON.stringify(JSON.stringify(todo));
registerClientFile(new URL("./ireland.js.js", import.meta.url));
registerClientFile(new URL("../../dist/esm-with-signals.js", import.meta.url), { folder: dirFE });
document.head.append(
// not all browsers support importmap
el("script", { type: "importmap" }).append(`
{
"imports": {
"deka-dom-el": "./${dirFE}/esm-with-signals.js",
"deka-dom-el/signals": "./${dirFE}/esm-with-signals.js"
}
}
`.trim())
);
document.body.append(
el("script", { type: "module" }).append(`
import { loadIrelands } from "./ireland.js.js";
loadIrelands(new Map(JSON.parse(${store})));
`.trim())
)
}
function mark(id) { return el=> el.dataset.ddeMark= id; }
const store_prev= new Map();
/** @param {URL} src */
function generateComponentId(src){
const candidate= parseInt(relative((new URL("..", import.meta.url)).pathname, src.pathname)
.split("")
.map(ch=> ch.charCodeAt(0))
.join(""), 10)
.toString(36)
.replace(/000+/g, "");
const count= 1 + ( store_prev.get(candidate) || 0 );
store_prev.set(candidate, count);
return count.toString()+"-"+candidate;
}

View File

@ -0,0 +1,13 @@
// not all browsers support importmaps
// import { el } from "deka-dom-el";
import { el } from "./irelands/esm-with-signals.js";
export function loadIrelands(store) {
document.body.querySelectorAll("[data-dde-mark]").forEach(ireland => {
const { ddeMark }= ireland.dataset;
if(!store.has(ddeMark)) return;
const { path, exportName, props }= store.get(ddeMark);
import("./"+path).then(module => {
ireland.replaceWith(el(module[exportName], props));
})
});
}

View File

@ -121,27 +121,11 @@ html {
transform: translateX(0);
}
body {
font-family: var(--font-main);
background-color: var(--bg);
color: var(--text);
line-height: 1.6;
font-size: 1.05rem;
display: grid;
grid-template-columns: 100%;
grid-template-areas:
"header"
"sidebar"
"content";
min-height: 100vh;
margin: 0;
}
/* Typography */
::selection {
background-color: var(--selection);
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
margin-bottom: 1rem;
margin-top: 2rem;
@ -194,6 +178,7 @@ code {
background-color: var(--code-bg);
color: var(--code-text);
padding: 0.2em 0.4em;
word-wrap: break-word;
}
pre {
@ -208,14 +193,26 @@ pre code {
background-color: transparent;
padding: 0;
}
.illustration:not(:has( .comparison)) pre {
background: none;
border-style: dashed !important;
width: fit-content;
padding: 1em 2em;
}
/* Layout */
body {
font-family: var(--font-main);
background-color: var(--bg);
color: var(--text);
line-height: 1.6;
font-size: 1.05rem;
display: grid;
grid-template-columns: 100%;
grid-template-areas:
"header"
"sidebar"
"content";
min-height: 100vh;
margin: 0;
--_gap: .75em;
gap: var(--_gap);
padding: var(--_gap);
}
@media (min-width: 768px) {
body {
grid-template-rows: var(--header-height) 1fr;
@ -225,11 +222,8 @@ pre code {
"sidebar content";
}
}
/* Main content */
body > main {
grid-area: content;
padding-block: 2rem;
max-width: 100%;
overflow-x: hidden;
display: grid;
@ -238,7 +232,6 @@ body > main {
[main-start] min(var(--body-max-width), 90%) [main-end]
1fr [full-main-end];
}
body > main > *, body > main slot > * {
width: 100%;
max-width: 100%;
@ -272,11 +265,31 @@ body > main h3, body > main h4 {
}
/* Boxes */
.illustration{
grid-column: full-main;
width: calc(100% - .75em);
}
.illustration:not(:has( .comparison)){
grid-column: main;
pre {
background: none;
border-style: dashed !important;
width: fit-content;
padding: 1em 2em;
}
}
.comparison {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
gap: calc(.75em / 2);
margin: 1.5rem 0;
> div {
width: 100%;
overflow: hidden;
}
}
@media (min-width: 768px) {
@ -405,18 +418,21 @@ body > main h3, body > main h4 {
@media (min-width: 768px) {
.tabs {
display: flex;
flex-wrap: wrap;
flex-flow: row wrap;
}
.tab {
flex: 1;
min-width: 50%;
border-top: none;
border-left: 1px solid var(--border);
display: flex;
flex-flow: column nowrap;
align-items: center;
text-align: justify;
}
.tab:first-child {
border-left: none;
border-inline-end: 1px solid var(--border);
}
}

View File

@ -3,7 +3,7 @@ export const info= {
href: "./",
title: t`Introduction`,
fullTitle: t`Vanilla for flavouring — a full-fledged feast for large projects`,
description: t`A lightweight, reactive DOM library for creating dynamic UIs with a declarative syntax`,
description: t`A lightweight, reactive DOM library for creating dynamic UIs with a declarative syntax`,
};
import { el } from "deka-dom-el";
@ -28,15 +28,15 @@ export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
Welcome to Deka DOM Elements (DDE) a lightweight library for building dynamic UIs with a
declarative syntax that stays close to the native DOM API. DDE gives you powerful reactive
tools without the complexity and overhead of larger frameworks.
Welcome to Deka DOM Elements (DDE) a lightweight library for building dynamic UIs with a declarative
syntax that stays close to the native DOM API. DDE gives you powerful reactive tools without the complexity
and overhead of larger frameworks.
`),
el("div", { className: "callout" }).append(
el("h4", t`What Makes DDE Special`),
el("ul").append(
el("li", t`No build step required — use directly in the browser`),
el("li", t`Lightweight core (~10-15kB minified) with zero dependencies`),
el("li", t`Lightweight core (~1015kB minified) with zero dependencies`),
el("li", t`Natural DOM API — work with real DOM nodes, not abstractions`),
el("li", t`Built-in reactivity with powerful signals system`),
el("li", t`Clean code organization with the 3PS pattern`)
@ -47,7 +47,8 @@ export function page({ pkg, info }){
el(h3, { textContent: t`The 3PS Pattern: A Better Way to Build UIs`, id: "h-3ps" }),
el("p").append(...T`
At the heart of DDE is the 3PS (3-Part Separation) pattern. This simple yet powerful approach helps you
organize your UI code into three distinct areas, making your applications more maintainable and easier to reason about.
organize your UI code into three distinct areas, making your applications more maintainable and easier
to reason about.
`),
el("div", { className: "illustration" }).append(
el("div", { className: "tabs" }).append(
@ -77,30 +78,33 @@ export function page({ pkg, info }){
),
el("p").append(...T`
By separating these concerns, your code becomes more modular, testable, and easier to maintain. This approach
shares principles with more formal patterns like ${el("a", { textContent: "MVVM", ...references.w_mvv })} and
${el("a", { textContent: "MVC", ...references.w_mvc })}, but with less overhead and complexity.
By separating these concerns, your code becomes more modular, testable, and easier to maintain. This
approach shares principles with more formal patterns like ${el("a", { textContent: "MVVM",
...references.w_mvv })} and ${el("a", { textContent: "MVC", ...references.w_mvc })}, but with less
overhead and complexity.
`),
el("div", { className: "note" }).append(
el("p").append(...T`
The 3PS pattern becomes especially powerful when combined with components, allowing you to create
reusable pieces of UI with encapsulated state and behavior. You'll learn more about this in the
reusable pieces of UI with encapsulated state and behavior. You'll learn more about this in the
following sections.
`)
),
el(h3, t`How to Use This Documentation`),
el("p").append(...T`
This guide will take you through DDE's features step by step:
This guide will take you through DDE's features step by step:
`),
el("ol").append(
el("li").append(...T`${el("strong", "Elements")} — Creating and manipulating DOM elements`),
el("li").append(...T`${el("strong", "Events")} — Handling user interactions and lifecycle events`),
el("li").append(...T`${el("strong", "Elements")} — Creating and manipulating DOM elements`),
el("li").append(...T`${el("strong", "Events")} — Handling user interactions and lifecycle events`),
el("li").append(...T`${el("strong", "Signals")} — Adding reactivity to your UI`),
el("li").append(...T`${el("strong", "Scopes")} — Managing component lifecycles`),
el("li").append(...T`${el("strong", "Custom Elements")} — Building web components`),
el("li").append(...T`${el("strong", "Debugging")} — Tools to help you build and fix your apps`),
el("li").append(...T`${el("strong", "Extensions")} — Integrating third-party functionalities`),
el("li").append(...T`${el("strong", "Ireland Components")} — Creating interactive demos with server-side pre-rendering`),
el("li").append(...T`${el("strong", "SSR")} — Server-side rendering with DDE`)
),
el("p").append(...T`
@ -108,4 +112,4 @@ export function page({ pkg, info }){
Let's get started with the basics of creating elements!
`),
);
}
}

View File

@ -14,9 +14,7 @@ ${host} {
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);
height: fit-content;
border-radius: var(--border-radius);
}
@ -60,8 +58,9 @@ ${host_nav} .github-link {
${host_nav} {
grid-area: sidebar;
background-color: var(--bg-sidebar);
border-right: 1px solid var(--border);
padding: 1.5rem 1rem;
border: 1px solid var(--border);
border-radius: var(--border-radius);
padding: 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
@ -97,7 +96,13 @@ ${host_nav} a .nav-number {
color: rgb(from currentColor r g b / .75);
}
/* Mobile navigation */
@media (min-width: 768px) {
${host_nav} {
height: fit-content;
position: sticky;
top: .5rem;
}
}
@media (max-width: 767px) {
${host_nav} {
padding: 0.75rem;

View File

@ -50,7 +50,7 @@ export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
Building user interfaces in JavaScript often involves creating and manipulating DOM elements.
Building user interfaces in JavaScript often involves creating and manipulating DOM elements.
DDE provides a simple yet powerful approach to element creation that is declarative, chainable,
and maintains a clean syntax close to HTML structure.
`),
@ -71,7 +71,7 @@ export function page({ pkg, info }){
el("p").append(...T`
In standard JavaScript, you create DOM elements using the
${el("a", references.mdn_create).append(el("code", "document.createElement()"))} method
and then set properties individually or with Object.assign():
and then set properties individually or with Object.assign():
`),
el("div", { class: "illustration" }).append(
el("div", { class: "comparison" }).append(
@ -85,16 +85,17 @@ export function page({ pkg, info }){
)
)
),
el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js"), page_id }),
el("p").append(...T`
The ${el("code", "el")} function provides a simple wrapper around ${el("code", "document.createElement")}
with enhanced property assignment.
`),
el(example, { src: fileURL("./components/examples/elements/dekaCreateElement.js"), page_id }),
el(h3, t`Advanced Property Assignment`),
el("p").append(...T`
The ${el("code", "assign")} function is the heart of DDE's element property handling. It provides
intelligent assignment of both properties (IDL) and attributes:
The ${el("code", "assign")} function is the heart of DDE's element property handling. It is internally used
to assign properties using the ${el("code", "el")} function. ${el("code", "assign")} provides intelligent
assignment of both properties (IDL) and attributes:
`),
el("div", { class: "function-table" }).append(
el("dl").append(
@ -145,21 +146,21 @@ export function page({ pkg, info }){
)
)
),
el(example, { src: fileURL("./components/examples/elements/dekaAppend.js"), page_id }),
el("p").append(...T`
This chainable approach results in code that more closely mirrors the structure of your HTML,
making it easier to understand and maintain.
This chainable pattern is much cleaner and easier to follow, especially for deeply nested elements.
It also makes it simple to add multiple children to a parent element in a single fluent expression.
`),
el(example, { src: fileURL("./components/examples/elements/dekaAppend.js"), page_id }),
el(h3, t`Building Reusable Components`),
el(h3, t`Using Components to Build UI Fragments`),
el("p").append(...T`
DDE makes it simple to create reusable element components through regular JavaScript functions.
The ${el("code", "el()")} function accepts a component function as its first argument:
The ${el("code", "el")} function is overloaded to support both tag names and function components.
This lets you refactor complex UI trees into reusable pieces:
`),
el(example, { src: fileURL("./components/examples/elements/dekaBasicComponent.js"), page_id }),
el("p").append(...T`
Component functions receive props as their argument and return element(s). This pattern
encourages code reuse and better organization of your UI code.
Component functions receive the properties object as their first argument, just like regular elements.
This makes it easy to pass data down to components and create reusable UI fragments.
`),
el("div", { class: "tip" }).append(
el("p").append(...T`
@ -180,41 +181,21 @@ export function page({ pkg, info }){
using the same consistent interface.
`),
el(h3, t`Best Practices`),
el(h3, t`Best Practices for Declarative DOM Creation`),
el("ol").append(
el("li").append(...T`
${el("strong", "Prefer composition over complexity")}: Create small component functions that can be
combined rather than large, complex templates
${el("strong", "Use component functions for reusable UI fragments:")} Extract common UI patterns
into reusable functions that return elements.
`),
el("li").append(...T`
${el("strong", "Use meaningful component names")}: Name your component functions after the elements or
patterns they create
${el("strong", "Leverage destructuring for cleaner component code:")} Use
${el("a", { textContent: "destructuring", ...references.mdn_destruct })} to extract properties
from the props object for cleaner component code.
`),
el("li").append(...T`
${el("strong", "Destructure for better readability")}: Use ${el("code", "const { div, p, button } = el")}
to create element-specific functions
${el("strong", "Leverage chainable methods for better performance:")} Use chainable methods like
${el("code", ".append()")} to build complex DOM trees for better performance and cleaner code.
`),
el("li").append(...T`
${el("strong", "Be consistent with property usage")}: Prefer using the same pattern (property vs attribute)
throughout your code
`)
),
el("div", { class: "troubleshooting" }).append(
el("h4", t`Common Element Creation Pitfalls`),
el("dl").append(
el("dt", t`Elements not showing up in DOM`),
el("dd", t`Remember to append elements to the document or a parent that's already in the document`),
el("dt", t`Properties not being applied correctly`),
el("dd", t`Check if you're mixing up property (IDL) names with attribute names (e.g., className vs class)`),
el("dt", t`Event listeners not working`),
el("dd", t`Ensure you're using the correct event binding approach (see Events section)`),
el("dt", t`SVG elements not rendering correctly`),
el("dd", t`Make sure you're using elNS with the correct SVG namespace for SVG elements`)
)
),
el(mnemonic)

View File

@ -47,8 +47,8 @@ export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
Events are at the core of interactive web applications. DDE provides a clean, declarative approach to
handling DOM events and extends this pattern with a powerful Addon system to incorporate additional
Events are at the core of interactive web applications. DDE provides a clean, declarative approach to
handling DOM events and extends this pattern with a powerful Addon system to incorporate additional
functionalities into your UI templates.
`),
el("div", { className: "callout" }).append(
@ -82,11 +82,11 @@ export function page({ pkg, info }){
)
)
),
el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }),
el("p").append(...T`
The main benefit of DDE's approach is that it works as an Addon, making it easy to integrate
The main benefit of DDE's approach is that it works as an Addon, making it easy to integrate
directly into element declarations.
`),
el(example, { src: fileURL("./components/examples/events/compare.js"), page_id }),
el(h3, t`Removing Event Listeners`),
el("div", { className: "note" }).append(
@ -127,7 +127,7 @@ export function page({ pkg, info }){
el(h3, t`Understanding Addons`),
el("p").append(...T`
Addons are a powerful pattern in DDE that extends beyond just event handling.
An Addon is any function that accepts an HTML element as its first parameter.
An Addon is any function that accepts an HTML element as its first parameter.
`),
el("div", { className: "callout" }).append(
el("h4", t`What Can Addons Do?`),
@ -151,7 +151,7 @@ export function page({ pkg, info }){
el(h3, t`Lifecycle Events`),
el("p").append(...T`
Addons are called immediately when an element is created, even before it's connected to the live DOM.
Addons are called immediately when an element is created, even before it's connected to the live DOM.
You can think of an Addon as an "oncreate" event handler.
`),
el("p").append(...T`
@ -200,12 +200,12 @@ export function page({ pkg, info }){
),
el(h3, t`Dispatching Custom Events`),
el(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }),
el("p").append(...T`
This makes it easy to implement component communication through events,
following standard web platform patterns. The curried approach allows for easy reuse
of event dispatchers throughout your application.
`),
el(example, { src: fileURL("./components/examples/events/compareDispatch.js"), page_id }),
el(h3, t`Best Practices`),
el("ol").append(

View File

@ -45,15 +45,15 @@ export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
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.
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: "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`Clean separation between data, logic, and UI`),
el("li", t`Small runtime with minimal overhead`),
el("li").append(...T`${el("strong", "In future")} no dependencies or framework lock-in`)
)
@ -98,13 +98,13 @@ export function page({ pkg, info }){
el("div", { class: "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("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("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`),
@ -123,7 +123,7 @@ export function page({ pkg, info }){
el(h3, t`Derived Signals: Computed Values`),
el("p").append(...T`
Computed values (also called derived signals) automatically update when their dependencies change.
Create them by passing a function to S():
Create them by passing a function to S():
`),
el(example, { src: fileURL("./components/examples/signals/derived.js"), page_id }),
el("p").append(...T`
@ -254,7 +254,7 @@ S.action(items, "push", "Dragonfruit"); // List updates automatically`, page_id
`),
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("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

View File

@ -31,18 +31,23 @@ export function page({ pkg, info }){
return el(simplePage, { info, pkg }).append(
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
we may need to handle the component's life-cycle and provide JavaScript the way to properly use
the ${el("a", { textContent: t`Garbage collection`, ...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("p").append(...T`The library therefore uses ${el("em", t`scopes`)} to provide these functionalities.`),
el(h3, t`Understanding Host Elements and Scopes`),
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
The ${el("strong", "host")} is the name for the element representing the component. This is typically the
element returned by a function. To get a reference, you can use ${el("code", "scope.host()")}. To apply addons,
just use ${el("code", "scope.host(...<addons>)")}.
`),
el("p").append(...T`
Scopes are primarily needed when signals are used in DOM templates (with ${el("code", "el")}, ${el("code",
"assign")}, or ${el("code", "S.el")}). They provide a way for automatically removing signal listeners
and cleaning up unused signals when components are removed from the DOM.
`),
el("div", { className: "illustration" }).append(
el("h4", t`Component Anatomy`),
el("pre").append(el("code", `
@ -130,76 +135,32 @@ function MyComponent() {
el(h3, t`Declarative vs Imperative Components`),
el("p").append(...T`
The library DOM API and signals works best when used declaratively. It means, you split your app logic
into three parts as it was itroduced in ${el("a", { textContent: "Signals", ...references.signals })}.
The library DOM API and signals work best when used declaratively. It means you split your app's logic
into three parts as introduced in ${el("a", { textContent: "Signals", ...references.signals })}.
`),
el("div", { className: "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.
Strictly speaking, the imperative way of using the library is not prohibited. Just be careful to avoid
mixing the declarative approach (using signals) with imperative manipulation of elements.
`)
),
el("div", { className: "tabs" }).append(
el("div", { className: "tab", "data-tab": "declarative" }).append(
el("h4", t`✅ Declarative Approach`),
el("p", t`Define what your UI should look like based on state:`),
el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id }),
el(code, { src: fileURL("./components/examples/scopes/declarative.js"), page_id })
),
el("div", { className: "tab", "data-tab": "imperative" }).append(
el("h4", t`⚠️ Imperative Approach`),
el("p", t`Manually update the DOM in response to events:`),
el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id }),
el(code, { src: fileURL("./components/examples/scopes/imperative.js"), page_id })
),
el("div", { className: "tab", "data-tab": "mixed" }).append(
el("h4", t`❌ Mixed Approach`),
el("p", t`Just AVOID:`),
el(code, { src: fileURL("./components/examples/scopes/mixed.js"), page_id }),
),
),
el(h3, t`Advanced: Custom Scoping Control`),
el("p").append(...T`
In more complex applications, you may need finer control over scopes. DDE provides
manual scope control mechanisms through ${el("code", "scope.push()")} and ${el("code", "scope.pop()")}.
`),
el("div", { className: "function-table" }).append(
el("h4", t`Manual Scope Control API`),
el("dl").append(
el("dt", t`scope.current`),
el("dd", t`Returns the currently active scope object.`),
el("dt", t`scope.isolate(callback)`),
el("dd", t`Executes the callback function within a temporary scope, then automatically restores the previous scope.
Safer than manual push/pop for most use cases.`),
el("dt", t`scope.push()`),
el("dd", t`Creates a new scope and makes it the current active scope. All signals and subscriptions
created after this call will be associated with this new scope.`),
el("dt", t`scope.pop()`),
el("dd", t`Restores the previous scope that was active before the matching push() call.`),
el("p", t`This approach should be avoided:`),
el(code, { src: fileURL("./components/examples/scopes/mixed.js"), page_id })
)
),
el("p").append(...T`
Custom scoping is particularly useful for:
`),
el("ul").append(
el("li", t`Isolating signal dependencies in async operations`),
el("li", t`Creating detached reactive logic that shouldn't be tied to a component's lifecycle`),
el("li", t`Building utilities that work with signals but need scope isolation`)
),
el(example, { src: fileURL("./components/examples/scopes/custom-scope.js"), page_id }),
el(example, { src: fileURL("./components/examples/scopes/with-scope.js"), page_id }),
el("div", { className: "warning" }).append(
el("p").append(...T`
${el("strong", "Be careful with manual scope control!")} Always ensure you have matching push() and pop() calls,
preferably in the same function. Unbalanced scope management can lead to memory leaks or unexpected behavior.
`),
el("p").append(...T`
For most use cases, prefer using the automatic scope management provided by components.
Manual scope control should be considered an advanced feature.
`)
),
el(h3, t`Best Practices for Scopes and Components`),
el("ol").append(

View File

@ -2,7 +2,7 @@ import { T, t } from "./utils/index.js";
export const info= {
title: t`Web Components`,
fullTitle: t`Using Web Components with DDE: Better Together`,
description: t`Using custom elements in combinantion with DDE`,
description: t`Using custom elements in combination with DDE`,
};
import { el } from "deka-dom-el";

View File

@ -32,7 +32,12 @@ export function page({ pkg, info }){
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(code, { content: `
const signal = S(0);
console.log('Current value:', signal.get());
// without triggering updates
console.log('Current value:', signal.valueOf());
`, page_id }),
el("p").append(...T`
You can also monitor signal changes by adding a listener:
`),
@ -43,7 +48,7 @@ export function page({ pkg, info }){
el("h4", t`Debugging derived signals`),
el("p").append(...T`
With derived signals (created with S(() => computation)), debugging is a bit more complex
With derived signals (created with ${el("code", "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(
@ -104,7 +109,7 @@ export function page({ pkg, info }){
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):
comment nodes for debugging (to be true they are also used internally, 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`
@ -117,30 +122,15 @@ export function page({ pkg, info }){
el("p").append(...T`
Elements created with the deka-dom-el library have special properties to aid in debugging:
`),
el("p").append(...T`
${el("code", "<element>.__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").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("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("p").append(...T`
These properties make it easier to understand the reactive structure of your application when inspecting elements.
@ -149,18 +139,34 @@ export function page({ pkg, info }){
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("code", "<signal>.__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", "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/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`
to determine the current value of the signal, call ${el("code", "signal.valueOf()")}.
`),
el("p").append(...T`
You can inspect (host) element relationships and bindings with signals in the DevTools console using
${el("code", "$0.__dde_reactive")} (for currently selected element). In the console you will see a 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", "listener — the listener function (this is an 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
the structure of \`__dde_reactive\` utilizes the browser's behavior of packing the first field,
so you can see the element and property that changes in the console right away.
`),
el("h4", t`Debugging with breakpoints`),

256
docs/p08-extensions.html.js Normal file
View File

@ -0,0 +1,256 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Extensions and 3rd Party`,
fullTitle: t`Extending deka-dom-el with Third-Party Functionalities`,
description: t`How to extend deka-dom-el with third-party libraries and custom functionalities.`,
};
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.js").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("p").append(...T`
deka-dom-el is designed with extensibility in mind. This page covers how to separate
third-party functionalities and integrate them seamlessly with the library, focusing on
proper resource cleanup and interoperability.
`),
el(h3, t`DOM Element Extensions with Addons`),
el("p").append(...T`
The primary method for extending DOM elements in deka-dom-el is through the Addon pattern.
Addons are functions that take an element and applying some functionality to it. This pattern enables a
clean, functional approach to element enhancement.
`),
el("div", { className: "callout" }).append(
el("h4", t`What are Addons?`),
el("p").append(...T`
Addons are simply functions with the signature: (element) => void. They:
`),
el("ul").append(
el("li", t`Accept a DOM element as input`),
el("li", t`Apply some behavior, property, or attribute to the element`),
)
),
el(code, { content: `
// Basic structure of an addon
function myAddon(config) {
return function(element) {
// Apply functionality to element
element.dataset.myAddon = config.option;
};
}
// Using an addon
el("div", { id: "example" }, myAddon({ option: "value" }));
`.trim(), page_id }),
el(h3, t`Resource Cleanup with Abort Signals`),
el("p").append(...T`
When extending elements with functionality that uses resources like event listeners, timers,
or external subscriptions, it's critical to clean up these resources when the element is removed
from the DOM. deka-dom-el provides utilities for this through AbortSignal integration.
`),
el("div", { className: "tip" }).append(
el("p").append(...T`
The ${el("code", "on.disconnectedAsAbort")} utility creates an AbortSignal that automatically
triggers when an element is disconnected from the DOM, making cleanup much easier to manage.
`)
),
el(code, { content: `
// Third-party library addon with proper cleanup
function externalLibraryAddon(config, signal) {
return function(element) {
// Get an abort signal that triggers on element disconnection
const signal = on.disconnectedAsAbort(element);
// Initialize the third-party library
const instance = new ExternalLibrary(element, config);
// Set up cleanup when the element is removed
signal.addEventListener('abort', () => {
instance.destroy();
});
return element;
};
}
// dde component
function Component(){
const { host }= scope;
const signal= on.disconnectedAsAbort(host);
return el("div", null, externalLibraryAddon({ option: "value" }, signal));
}
`.trim(), page_id }),
el(h3, t`Building Library-Independent Extensions`),
el("p").append(...T`
When creating extensions, it's a good practice to make them as library-independent as possible.
This approach enables better interoperability and future-proofing.
`),
el("div", { className: "illustration" }).append(
el("h4", t`Library-Independent vs. Library-Dependent Extension`),
el("div", { className: "tabs" }).append(
el("div", { className: "tab" }).append(
el("h5", t`✅ Library-Independent`),
el(code, { content: `
function enhancementElement({ signal, ...config }) {
// do something
return function(element) {
// do something
signal.addEventListener('abort', () => {
// do cleanup
});
};
}
`.trim(), page_id })
),
el("div", { className: "tab" }).append(
el("h5", t`⚠️ Library-Dependent`),
el(code, { content: `
// Tightly coupled to deka-dom-el
function enhancementElement(config) {
return function(element) {
// do something
on.disconnected(()=> {
// do cleanup
})(element);
};
}
`.trim(), page_id })
)
)
),
el(h3, t`Signal Extensions and Future Compatibility`),
el("p").append(...T`
Unlike DOM elements, signal functionality in deka-dom-el currently lacks a standardized
way to create library-independent extensions. This is because signals are implemented
differently across libraries.
`),
el("div", { className: "note" }).append(
el("p").append(...T`
In the future, JavaScript may include built-in signals through the
${el("a", { href: "https://github.com/tc39/proposal-signals", textContent: "TC39 Signals Proposal" })}.
deka-dom-el is designed with future compatibility in mind and will hopefully support these
native signals without breaking changes when they become available.
`)
),
el("p").append(...T`
For now, when extending signals functionality, focus on clear interfaces and isolation to make
future migration easier.
`),
el(code, { content: `
// Signal extension with clear interface
function createEnhancedSignal(initialValue) {
const signal = S(initialValue);
// Extension functionality
const increment = () => signal.set(signal.get() + 1);
const decrement = () => signal.set(signal.get() - 1);
// Return the original signal with added methods
return Object.assign(signal, {
increment,
decrement
});
}
// Usage
const counter = createEnhancedSignal(0);
el("button")({ onclick: () => counter.increment() }, "Increment");
el("div", S.text\`Count: \${counter}\`);
`.trim(), page_id }),
el(h3, t`Using Signals Independently`),
el("p").append(...T`
While signals are tightly integrated with DDE's DOM elements, you can also use them independently.
This can be useful when you need reactivity in non-UI code or want to integrate with other libraries.
`),
el("p").append(...T`
There are two ways to import signals:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Standard import")}: ${el("code", "import { S } from \"deka-dom-el/signals\";")}
This automatically registers signals with DDE's DOM reactivity system
`),
el("li").append(...T`
${el("strong", "Independent import")}: ${el("code", "import { S } from \"deka-dom-el/src/signals-lib\";")}
This gives you just the signal system without DOM integration
`)
),
el(code, { content: `// Independent signals without DOM integration
import { signal as S, isSignal } from "deka-dom-el/src/signals-lib";
// Create and use signals as usual
const count = S(0);
const doubled = S(() => count.get() * 2);
// Subscribe to changes
S.on(count, value => console.log(value));
// Update signal value
count.set(5); // Logs: 5
console.log(doubled.get()); // 10`, page_id }),
el("p").append(...T`
The independent signals API includes all core functionality (${el("code", "S()")}, ${el("code", "S.on()")},
${el("code", "S.action()")}).
`),
el("div", { class: "callout" }).append(
el("h4", t`When to Use Independent Signals`),
el("ul").append(
el("li", t`For non-UI state management in your application`),
el("li", t`When integrating with other libraries or frameworks`),
el("li", t`To minimize bundle size when you don't need DOM integration`)
)
),
el(h3, t`Best Practices for Extensions`),
el("ol").append(
el("li").append(...T`
${el("strong", "Use AbortSignals for cleanup:")} Always implement proper resource cleanup with
${el("code", "on.disconnectedAsAbort")} or similar mechanisms
`),
el("li").append(...T`
${el("strong", "Separate core logic from library adaptation:")} Make your core functionality work
with standard DOM APIs when possible
`),
el("li").append(...T`
${el("strong", "Document clearly:")} Provide clear documentation on how your extension works
and what resources it uses
`),
el("li").append(...T`
${el("strong", "Follow the Addon pattern:")} Keep to the (element) => element signature for
DOM element extensions
`),
el("li").append(...T`
${el("strong", "Avoid modifying global state:")} Extensions should be self-contained and not
affect other parts of the application
`)
),
el("div", { className: "troubleshooting" }).append(
el("h4", t`Common Extension Pitfalls`),
el("dl").append(
el("dt", t`Leaking event listeners or resources`),
el("dd", t`Always use AbortSignal-based cleanup to automatically remove listeners when elements are disconnected`),
el("dt", t`Tight coupling with library internals`),
el("dd", t`Focus on standard DOM APIs and clean interfaces rather than depending on deka-dom-el implementation details`),
el("dt", t`Mutating element prototypes`),
el("dd", t`Prefer compositional approaches with addons over modifying element prototypes`),
el("dt", t`Complex initialization in addons`),
el("dd", t`Split complex logic into a separate initialization function that the addon can call`)
)
)
);
}

View File

@ -12,10 +12,18 @@ 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 */
/** @param {import("./types.js").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("div", { className: "warning" }).append(
el("p").append(...T`
This part of the documentation is primarily intended for technical enthusiasts and documentation
authors. It describes an advanced feature, not a core part of the library. Most users will not need to
implement this functionality directly in their applications. This capability will hopefully be covered
by third-party libraries or frameworks that provide simpler SSR integration using 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.
@ -29,11 +37,11 @@ export function page({ pkg, info }){
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("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`),

360
docs/p10-ireland.html.js Normal file
View File

@ -0,0 +1,360 @@
import { T, t } from "./utils/index.js";
export const info= {
title: t`Ireland Components`,
fullTitle: t`Interactive Demo Components with Server-Side Pre-Rendering`,
description: t`Creating live, interactive component examples in documentation with server-side rendering and client-side hydration.`,
};
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";
import { ireland } from "./components/ireland.html.js";
/** @param {string} url */
const fileURL= url=> new URL(url, import.meta.url);
/** @param {import("./types.js").PageAttrs} attrs */
export function page({ pkg, info }){
const page_id= info.id;
return el(simplePage, { info, pkg }).append(
el("div", { className: "warning" }).append(
el("p").append(...T`
This part of the documentation is primarily intended for technical enthusiasts and documentation
authors. It describes an advanced feature, not a core part of the library. Most users will not need to
implement this functionality directly in their applications. This capability will hopefully be covered
by third-party libraries or frameworks that provide simpler SSR integration using deka-dom-el.
`)
),
el(h3, t`What Are Ireland Components?`),
el("p").append(...T`
Ireland components are a special type of documentation component that:
`),
el("ul").append(
el("li", t`Display source code with syntax highlighting`),
el("li", t`Pre-render components on the server during documentation build`),
el("li", t`Copy component source files to the documentation output`),
el("li", t`Provide client-side rehydration for interactive demos`),
el("li", t`Allow users to run and experiment with components in real-time`)
),
el(h3, t`How Ireland Components Work`),
el("p").append(...T`
The Ireland component system consists of several parts working together:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Server-side rendering:")} Components are pre-rendered during the documentation build process
`),
el("li").append(...T`
${el("strong", "Component registration:")} Source files are copied to the documentation output directory
`),
el("li").append(...T`
${el("strong", "Client-side scripting:")} JavaScript code is generated to load and render components
`),
el("li").append(...T`
${el("strong", "User interaction:")} The "Run Component" button dynamically loads and renders the component
`)
),
el(h3, t`Implementation Architecture`),
el("p").append(...T`
The core of the Ireland system is implemented in ${el("code", "docs/components/ireland.html.js")}.
It integrates with the SSR build process using the ${el("code", "registerClientFile")} function from ${el("code", "docs/ssr.js")}.
`),
el(code, { content: `
// Basic usage of an ireland component
el(ireland, {
src: fileURL("./components/examples/path/to/component.js"),
exportName: "NamedExport", // optional, defaults to "default",
})`, page_id }),
el("p").append(...T`
During the build process (${el("code", "bs/docs.js")}), the following happens:
`),
el("ol").append(
el("li", t`Component source code is loaded and displayed with syntax highlighting`),
el("li", t`Source files are registered to be copied to the output directory`),
el("li", t`Client-side scripts are generated for each page with ireland components`),
el("li", t`The component is wrapped in a UI container with controls`)
),
el(h3, t`Core Implementation Details`),
el("p").append(...T`
Let's look at the key parts of the ireland component implementation:
`),
el("h4", t`Building SSR`),
el(code, { content: `
// From bs/docs.js - Server-side rendering engine
import { createHTMl } from "./docs/jsdom.js";
import { register, queue } from "../jsdom.js";
import { path_target, dispatchEvent } from "../docs/ssr.js";
// For each page, render it on the server
for(const { id, info } of pages) {
// Create a virtual DOM environment for server-side rendering
const serverDOM = createHTMl("");
serverDOM.registerGlobally("HTMLScriptElement");
// Register deka-dom-el with the virtual DOM
const { el } = await register(serverDOM.dom);
// Import and render the page component
const { page } = await import(\`../docs/\${id}.html.js\`);
serverDOM.document.body.append(
el(page, { pkg, info }),
);
// Process the queue of asynchronous operations
await queue();
// Trigger render event handlers
dispatchEvent("oneachrender", document);
// Write the HTML to the output file
s.echo(serverDOM.serialize()).to(path_target.root+id+".html");
}
// Final build step - trigger SSR end event
dispatchEvent("onssrend");
`, page_id }),
el("h4", t`File Registration`),
el(code, { content: `
// From docs/ssr.js - File registration system
export function registerClientFile(url, { head, folder = "", replacer } = {}) {
// Ensure folder path ends with a slash
if(folder && !folder.endsWith("/")) folder += "/";
// Extract filename from URL
const file_name = url.pathname.split("/").pop();
// Create target directory if needed
s.mkdir("-p", path_target.root+folder);
// Get file content and apply optional replacer function
let content = s.cat(url);
if(replacer) content = s.echo(replacer(content.toString()));
// Write content to the output directory
content.to(path_target.root+folder+file_name);
// If a head element was provided, add it to the document
if(!head) return;
head[head instanceof HTMLScriptElement ? "src" : "href"] = file_name;
document.head.append(head);
}
`, page_id }),
el("h4", t`Server-Side Rendering`),
el(code, { content: `
// From docs/components/ireland.html.js - Server-side component implementation
export function ireland({ src, exportName = "default", props = {} }) {
// Calculate relative path for imports
const path = "./"+relative(dir, src.pathname);
// Generate unique ID for this component instance
const id = "ireland-" + generateComponentId(src);
// Create placeholder element
const element = el.mark({ type: "ireland", name: ireland.name });
// Import and render the component during SSR
queue(import(path).then(module => {
const component = module[exportName];
element.replaceWith(el(component, props, mark(id)));
}));
// Register client-side hydration on first component
if(!componentsRegistry.size)
addEventListener("oneachrender", registerClientPart);
// Store component info for client-side hydration
componentsRegistry.set(id, {
src,
path: dirFE+"/"+path.split("/").pop(),
exportName,
props,
});
return element;
}
// Register client-side resources
function registerClientPart() {
// Process all component registrations
const todo = Array.from(componentsRegistry.entries())
.map(([ id, d ]) => {
// Copy the component source file to output directory
registerClientFile(d.src, {
folder: dirFE,
// Replace bare imports for browser compatibility
replacer(file) {
return file.replaceAll(
/ from "deka-dom-el(\/signals)?";/g,
\` from "./esm-with-signals.js";\`
);
}
});
return [ id, d ];
});
// Serialize the component registry for client-side use
const store = JSON.stringify(JSON.stringify(todo));
// Copy client-side scripts to output
registerClientFile(new URL("./ireland.js.js", import.meta.url));
registerClientFile(new URL("../../dist/esm-with-signals.js", import.meta.url), { folder: dirFE });
// Add import map for package resolution
document.head.append(
el("script", { type: "importmap" }).append(\`
{
"imports": {
"deka-dom-el": "./\${dirFE}/esm-with-signals.js",
"deka-dom-el/signals": "./\${dirFE}/esm-with-signals.js"
}
}
\`.trim())
);
// Add bootstrap script to load components
document.body.append(
el("script", { type: "module" }).append(\`
import { loadIrelands } from "./ireland.js.js";
loadIrelands(new Map(JSON.parse(\${store})));
\`.trim())
);
}
`, page_id }),
el("h4", t`Client-Side Hydration`),
el(code, { content: `
// From docs/components/ireland.js.js - Client-side hydration
import { el } from "./irelands/esm-with-signals.js";
export function loadIrelands(store) {
// Find all marked components in the DOM
document.body.querySelectorAll("[data-dde-mark]").forEach(ireland => {
const { ddeMark } = ireland.dataset;
// Skip if this component isn't in our registry
if(!store.has(ddeMark)) return;
// Get component information
const { path, exportName, props } = store.get(ddeMark);
// Dynamically import the component module
import("./" + path).then(module => {
// Replace the server-rendered element with the client-side version
ireland.replaceWith(el(module[exportName], props));
});
});
}
`, page_id }),
el(h3, t`Live Example`),
el("p").append(...T`
Here's a live example of an Ireland component showing a standard counter.
The component is defined in ${el("code", "docs/components/examples/ireland-test/counter.js")} and
rendered with the Ireland component system:
`),
el(code, {
src: fileURL("./components/examples/ireland-test/counter.js"),
page_id
}),
el(ireland, {
src: fileURL("./components/examples/ireland-test/counter.js"),
exportName: "CounterStandard",
page_id
}),
el("p").append(...T`
When the "Run Component" button is clicked, the component is loaded and rendered dynamically.
The counter state is maintained using signals, allowing for reactive updates as you click
the buttons to increment and decrement the value.
`),
el(h3, t`Creating Your Own Components`),
el("p").append(...T`
To create components for use with the Ireland system, follow these guidelines:
`),
el("ol").append(
el("li").append(...T`
${el("strong", "Export a function:")} Components should be exported as named or default functions
`),
el("li").append(...T`
${el("strong", "Return a DOM element:")} The function should return a valid DOM element created with ${el("code", "el()")}
`),
el("li").append(...T`
${el("strong", "Accept props:")} Components should accept a props object, even if not using it
`),
el("li").append(...T`
${el("strong", "Manage reactivity:")} Use signals for state management where appropriate
`),
el("li").append(...T`
${el("strong", "Handle cleanup:")} Include any necessary cleanup for event listeners or signals
`)
),
el(h3, t`Practical Considerations and Limitations`),
el("p").append(...T`
When implementing Ireland components in real documentation, there are several important
considerations to keep in mind:
`),
el("div", { className: "warning" }).append(
el("h4", t`Module Resolution and Bundling`),
el("p").append(...T`
The examples shown here use bare module specifiers like ${el("code", "import { el } from \"deka-dom-el\"")}
which aren't supported in all browsers without importmaps. In a production implementation, you would need to:
`),
el("ol").append(
el("li", t`Replace bare import paths with actual paths during the build process`),
el("li", t`Bundle component dependencies to avoid multiple requests`),
el("li", t`Ensure all module dependencies are properly resolved and copied to the output directory`)
),
el("p").append(...T`
In this documentation, we replace the paths with ${el("code", "./esm-with-signals.js")} and provide
a bundled version of the library, but more complex components might require a dedicated bundling step.
`)
),
el("div", { className: "note" }).append(
el("h4", t`Component Dependencies`),
el("p").append(...T`
Real-world components typically depend on multiple modules and assets. The Ireland system would need
to be extended to:
`),
el("ul").append(
el("li", t`Detect and analyze all dependencies of a component`),
el("li", t`Bundle these dependencies together or ensure they're properly copied to the output directory`),
el("li", t`Handle non-JavaScript assets like CSS, images, or data files`)
)
),
el(h3, t`Advanced Usage`),
el("p").append(...T`
The Ireland system can be extended in several ways to address these limitations:
`),
el("ul").append(
el("li", t`Integrate with a bundler like esbuild, Rollup, or Webpack`),
el("li", t`Add props support for configuring components at runtime`),
el("li", t`Implement module caching to reduce network requests`),
el("li", t`Add code editing capabilities for interactive experimentation`),
el("li", t`Support TypeScript and other languages through transpilation`),
el("li", t`Implement state persistence between runs`)
),
el("p").append(...T`
This documentation site itself is built using the techniques described here,
showcasing how deka-dom-el can be used to create both the documentation and
the interactive examples within it. The implementation here is simplified for clarity,
while a production-ready system would need to address the considerations above.
`)
);
}

View File

@ -13,16 +13,23 @@ export let pages= [];
* @typedef registerClientFile
* @type {function}
* @param {URL} url
* @param {HTMLScriptElement|HTMLLinkElement} [element_head]
* @param {Object} [options]
* @param {HTMLScriptElement|HTMLLinkElement} [options.head]
* @param {string} [options.folder]
* @param {function} [options.replacer]
* */
export function registerClientFile(url, element_head){
export function registerClientFile(url, { head, folder= "", replacer }= {}){
if(folder && !folder.endsWith("/")) folder+= "/";
const file_name= url.pathname.split("/").pop();
s.cat(url).to(path_target.root+file_name);
s.mkdir("-p", path_target.root+folder);
let content= s.cat(url)
if(replacer) content= s.echo(replacer(content.toString()));
content.to(path_target.root+folder+file_name);
if(!element_head) return;
element_head[element_head instanceof HTMLScriptElement ? "src" : "href"]= file_name;
if(!head) return;
head[head instanceof HTMLScriptElement ? "src" : "href"]= file_name;
document.head.append(
element_head
head
);
}

38
index.d.ts vendored
View File

@ -72,11 +72,22 @@ export function assignAttribute<El extends SupportedElement, ATT extends keyof E
): ElementAttributes<El>[ATT]
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap;
export namespace el {
/**
* Creates a marker comment for elements
*
* @param {{ type: "component"|"reactive"|"ireland"|"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
*/
export function mark(attrs: { type: "component"|"reactive"|"ireland"|"later", name?: string, host?: "this"|"parentElement" }, is_open?: boolean): Comment;
}
export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>,
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -86,7 +97,7 @@ export function el<
A extends { textContent: ddeStringable },
EL extends SupportedElement | ddeDocumentFragment
>(
component: (attr: A)=> EL,
component: (attr: A, ...rest: any[])=> EL,
attrs?: NoInfer<A>["textContent"],
...addons: ddeElementAddon<EL>[]
): EL extends ddeHTMLElementTagNameMap[keyof ddeHTMLElementTagNameMap]
@ -237,8 +248,6 @@ export const scope: {
pushRoot(): ReturnType<Array<Scope>["push"]>,
/** Removes last/current child scope. */
pop(): ReturnType<Array<Scope>["pop"]>,
/** Runs function in a new (isolated) scope */
isolate(fn: Function): void,
};
export function customElementRender<
@ -253,6 +262,27 @@ export function customElementWithDDE<EL extends (new ()=> HTMLElement)>(custom_e
export function lifecyclesToEvents<EL extends (new ()=> HTMLElement)>(custom_element: EL): EL
export function observedAttributes(custom_element: HTMLElement): Record<string, string>
/**
* This is used primarly for server side rendering. To be sure that all async operations
* are finished before the page is sent to the client.
* ```
* // on component
* function component(){
*
* queue(fetch(...).then(...));
* }
*
* // building the page
* async function build(){
* const { component }= await import("./component.js");
* document.body.append(el(component));
* await queue();
* retutn document.body.innerHTML;
* }
* ```
* */
export function queue(promise?: Promise<unknown>): Promise<unknown>;
/* TypeScript MEH */
declare global{
type ddeAppend<el>= (...nodes: (Node | string)[])=> el;

View File

@ -17,7 +17,7 @@ env.setDeleteAttr= function(obj, prop, value){
if(value) return obj.setAttribute(prop, "");
obj.removeAttribute(prop);
};
const keys= { H: "HTMLElement", S: "SVGElement", F: "DocumentFragment", M: "MutationObserver", D: "document" };
const keys= { H: "HTMLElement", S: "SVGElement", F: "DocumentFragment", D: "document" };
let env_bk= {};
let dom_last;

View File

@ -1,26 +0,0 @@
Markserv boot: starting Markserv...
(node:170089) [DEP0128] DeprecationWarning: Invalid 'main' field in '/home/jaandrle/.npm/_npx/13a70f167aa91a98/node_modules/implant/package.json' of 'implant'. Please either fix that or report it to the module author
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:170089) [DEP0128] DeprecationWarning: Invalid 'main' field in '/home/jaandrle/.npm/_npx/13a70f167aa91a98/node_modules/balanced-pairs/package.json' of 'balanced-pairs'. Please either fix that or report it to the module author
(node:170089) [DEP0128] DeprecationWarning: Invalid 'main' field in '/home/jaandrle/.npm/_npx/13a70f167aa91a98/node_modules/super-split/package.json' of 'super-split'. Please either fix that or report it to the module author
Markserv address: http://localhost:8642
Markserv path: /home/jaandrle/Vzdálené/GitHub/deka-dom-el
Markserv livereload: communicating on port: 35729
Markserv process: your pid is: 170089
Markserv stop: press [Ctrl + C] or type "sudo kill -9 170089"
GitHub Contribute on Github - github.com/markserv
Markserv upgrade: checking for upgrade...
Markserv upgrade: no upgrade available
Markserv dir: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg
Markserv markdown: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/README.md
Markserv file: /home/jaandrle/Vzdálené/GitHub/deka-dom-el/docs/assets/logo.svg

View File

@ -100,5 +100,6 @@
"jshint": "~2.13",
"nodejsscript": "^1.0.2",
"size-limit-node-esbuild": "~0.3"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -72,12 +72,6 @@ export const scope= {
if(scopes.length===1) return;
return scopes.pop();
},
isolate(fn){
this.push({ prevent: true });
fn();
this.pop();
}
};
/**
* Chainable append function for elements

View File

@ -95,8 +95,8 @@ 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
* @param {Function} host - Host element or function taking an element
* @returns {AbortSignal} AbortSignal that aborts on disconnect
*/
on.disconnectedAsAbort= function(host){
if(store_abort.has(host)) return store_abort.get(host);
@ -104,7 +104,7 @@ on.disconnectedAsAbort= function(host){
const a= new AbortController();
store_abort.set(host, a);
host(on.disconnected(()=> a.abort()));
return a;
return a.signal;
};
/** Store for elements with attribute observers */

View File

@ -1,6 +1,6 @@
import { queueSignalWrite, mark } from "./helpers.js";
export { mark };
import { hasOwn, Defined, oCreate, isProtoFrom, oAssign } from "../helpers.js";
import { hasOwn, Defined, oCreate, oAssign } from "../helpers.js";
const Signal = oCreate(null, {
get: { value(){ return read(this); } },
@ -18,7 +18,7 @@ const SignalReadOnly= oCreate(Signal, {
* @returns {boolean} True if the value is a signal
*/
export function isSignal(candidate){
return isProtoFrom(candidate, Signal);
return candidate && candidate[mark];
}
/**