mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-04-01 19:55:53 +02:00
Compare commits
7 Commits
17e40fdd9c
...
57a5ff2dfe
Author | SHA1 | Date | |
---|---|---|---|
57a5ff2dfe | |||
b3356afa88 | |||
da4e3e52d9 | |||
6c297672c1 | |||
66fdee2c05 | |||
963ed53c84 | |||
59efa84494 |
@ -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);
|
||||
|
9
dist/dde-with-signals.js
vendored
9
dist/dde-with-signals.js
vendored
@ -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();
|
||||
|
14
dist/dde-with-signals.min.js
vendored
14
dist/dde-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
7
dist/dde.js
vendored
7
dist/dde.js
vendored
@ -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
8
dist/dde.min.js
vendored
File diff suppressed because one or more lines are too long
6
dist/esm-with-signals.d.min.ts
vendored
6
dist/esm-with-signals.d.min.ts
vendored
@ -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-with-signals.d.ts
vendored
6
dist/esm-with-signals.d.ts
vendored
@ -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<
|
||||
|
9
dist/esm-with-signals.js
vendored
9
dist/esm-with-signals.js
vendored
@ -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();
|
||||
|
8
dist/esm-with-signals.min.js
vendored
8
dist/esm-with-signals.min.js
vendored
File diff suppressed because one or more lines are too long
6
dist/esm.d.min.ts
vendored
6
dist/esm.d.min.ts
vendored
@ -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
6
dist/esm.d.ts
vendored
@ -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
7
dist/esm.js
vendored
@ -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
2
dist/esm.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -5,6 +5,7 @@ const greeting = S(() => {
|
||||
// log derived signals
|
||||
const log = "Hello, " + name.get();
|
||||
console.log(log);
|
||||
console.log(name.valueOf());
|
||||
return log;
|
||||
});
|
||||
|
||||
|
37
docs/components/examples/ireland-test/counter.js
Normal file
37
docs/components/examples/ireland-test/counter.js
Normal 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: "+",
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
@ -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);
|
||||
|
88
docs/components/ireland.html.js
Normal file
88
docs/components/ireland.html.js
Normal 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;
|
||||
}
|
13
docs/components/ireland.js.js
Normal file
13
docs/components/ireland.js.js
Normal 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));
|
||||
})
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 (~10–15kB 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!
|
||||
`),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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";
|
||||
|
@ -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
256
docs/p08-extensions.html.js
Normal 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`)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
@ -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
360
docs/p10-ireland.html.js
Normal 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.
|
||||
`)
|
||||
);
|
||||
}
|
19
docs/ssr.js
19
docs/ssr.js
@ -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
38
index.d.ts
vendored
@ -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;
|
||||
|
2
jsdom.js
2
jsdom.js
@ -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;
|
||||
|
||||
|
26
nohup.out
26
nohup.out
@ -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
|
@ -100,5 +100,6 @@
|
||||
"jshint": "~2.13",
|
||||
"nodejsscript": "^1.0.2",
|
||||
"size-limit-node-esbuild": "~0.3"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user