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

🔤 UI/UX/wording

This commit is contained in:
Jan Andrle 2025-03-07 10:40:57 +01:00
parent 57a5ff2dfe
commit 7f4787d704
27 changed files with 514 additions and 359 deletions

View File

@ -3,7 +3,7 @@
| [*mirrored* on Gitea](https://gitea.jaandrle.cz/jaandrle/deka-dom-el)
<p align="center">
<img src="docs/assets/logo.svg" alt="Deka DOM Elements Logo" width="180" height="180">
<img src="docs/assets/logo.svg" alt="Deka DOM Elements Logo" width="180" height="180">
</p>
# Deka DOM Elements
@ -15,40 +15,40 @@
```javascript
// 🌟 Reactive component with clear separation of concerns
document.body.append(
el(EmojiCounter, { initial: "🚀" })
el(EmojiCounter, { initial: "🚀" })
);
function EmojiCounter({ initial }) {
// ✨ State - Define reactive data
const count = S(0);
const emoji = S(initial);
// ✨ State - Define reactive data
const count = S(0);
const emoji = S(initial);
/** @param {HTMLOptionElement} el */
const isSelected= el=> (el.selected= el.value===initial);
// 🔄 View - UI updates automatically when signals change
return el().append(
el("p", {
className: "output",
textContent: S(() =>
`Hello World ${emoji.get().repeat(clicks.get())}`),
}),
// 🎮 Controls - Update state on events
el("button", { textContent: "Add Emoji" },
on("click", () => count.set(count.get() + 1))
),
el("select", null, on("change", e => emoji.set(e.target.value)))
/** @param {HTMLOptionElement} el */
const isSelected= el=> (el.selected= el.value===initial);
// 🔄 View - UI updates automatically when signals change
return el().append(
el("p", {
className: "output",
textContent: S(() =>
`Hello World ${emoji.get().repeat(clicks.get())}`),
}),
// 🎮 Controls - Update state on events
el("button", { textContent: "Add Emoji" },
on("click", () => count.set(count.get() + 1))
),
el("select", null, on("change", e => emoji.set(e.target.value)))
.append(
el(Option, "🎉", isSelected),
el(Option, "🚀", isSelected),
el(Option, "💖", isSelected),
)
);
el(Option, "🎉", isSelected),
el(Option, "🚀", isSelected),
el(Option, "💖", isSelected),
)
);
}
function Option({ textContent }){
return Ol("option", { value: textContent, textContent });
return Ol("option", { value: textContent, textContent });
}
```
@ -68,11 +68,16 @@ Creating reactive elements, components, and Web Components using the native
## Why Another Library?
This library bridges the gap between minimal solutions like van/hyperscript and more comprehensive frameworks like [solid-js](https://github.com/solidjs/solid), offering a balanced trade-off between size, complexity, and usability.
This library bridges the gap between minimal solutions like van/hyperscript and more comprehensive frameworks like
[solid-js](https://github.com/solidjs/solid), offering a balanced trade-off between size, complexity, and usability.
Following functional programming principles, Deka DOM Elements starts with pure JavaScript (DOM API) and gradually adds auxiliary functions. These range from minor improvements to advanced features for building complete declarative reactive UI templates.
Following functional programming principles, Deka DOM Elements starts with pure JavaScript (DOM API) and gradually adds
auxiliary functions. These range from minor improvements to advanced features for building complete declarative
reactive UI templates.
A key advantage: any internal function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, `S`, etc.) can be used independently while also working seamlessly together. This modular approach makes it easier to integrate the library into existing projects.
A key advantage: any internal function (`assign`, `classListDeclarative`, `on`, `dispatchEvent`, `S`, etc.) can be used
independently while also working seamlessly together. This modular approach makes it easier to integrate the library
into existing projects.
## Getting Started
@ -88,7 +93,7 @@ A key advantage: any internal function (`assign`, `classListDeclarative`, `on`,
```html
<script src="https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/dde-with-signals.min.js"></script>
<script type="module">
const { el, S } = dde;
const { el, S } = dde;
</script>
```
@ -112,4 +117,5 @@ Signals are the reactive backbone of Deka DOM Elements:
- [adamhaile/S](https://github.com/adamhaile/S) - Simple, clean, fast reactive programming
- [hyperhype/hyperscript](https://github.com/hyperhype/hyperscript) - Create HyperText with JavaScript
- [potch/signals](https://github.com/potch/signals) - A small reactive signals library
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) - Functional DOM components without JSX/virtual DOM
- [jaandrle/dollar_dom_component](https://github.com/jaandrle/dollar_dom_component) -
Functional DOM components without JSX/virtual DOM

View File

@ -42,7 +42,7 @@ s.echo(styles.content).to(path_target.css+styles.name);
// Copy assets
echo("Copying assets…");
if(s.test("-d", "docs/assets")) {
s.cp("-r", "docs/assets/*", path_target.assets);
s.cp("-r", "docs/assets/*", path_target.assets);
}
dispatchEvent("onssrend");

View File

@ -72,6 +72,20 @@ 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 attrs - Marker attributes
* @param [is_open=false] - Whether the marker is open-ended
* @returns Comment node marker
*/
export function mark(
attrs: { type: "component"|"reactive"|"later", name?: string, host?: "this"|"parentElement" },
is_open?: boolean
): Comment;
}
export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
@ -251,6 +265,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

@ -72,6 +72,20 @@ 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 attrs - Marker attributes
* @param [is_open=false] - Whether the marker is open-ended
* @returns Comment node marker
*/
export function mark(
attrs: { type: "component"|"reactive"|"later", name?: string, host?: "this"|"parentElement" },
is_open?: boolean
): Comment;
}
export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
@ -251,6 +265,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;

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

@ -72,6 +72,20 @@ 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 attrs - Marker attributes
* @param [is_open=false] - Whether the marker is open-ended
* @returns Comment node marker
*/
export function mark(
attrs: { type: "component"|"reactive"|"later", name?: string, host?: "this"|"parentElement" },
is_open?: boolean
): Comment;
}
export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
@ -251,6 +265,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;

35
dist/esm.d.ts vendored
View File

@ -72,6 +72,20 @@ 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 attrs - Marker attributes
* @param [is_open=false] - Whether the marker is open-ended
* @returns Comment node marker
*/
export function mark(
attrs: { type: "component"|"reactive"|"later", name?: string, host?: "this"|"parentElement" },
is_open?: boolean
): Comment;
}
export function el<
A extends ddeComponentAttributes,
EL extends SupportedElement | ddeDocumentFragment
@ -251,6 +265,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

@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#333333" />
<stop offset="100%" stop-color="#222222" />
</linearGradient>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e32c2c" />
<stop offset="100%" stop-color="#ff5252" />
</linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="0.5" stdDeviation="0.5" flood-color="#000" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Square background with rounded corners -->
<rect x="2" y="2" width="28" height="28" rx="4" ry="4" fill="url(#bgGradient)" />
<!-- Subtle code brackets as background element -->
<g opacity="0.15" fill="#fff">
<path d="M10,7.5 L6.25,16 L10,24.5" stroke="#fff" stroke-width="1" fill="none"/>
<path d="M22,7.5 L25.75,16 L22,24.5" stroke="#fff" stroke-width="1" fill="none"/>
</g>
<!-- lowercase dde letters -->
<text x="16" y="21" text-anchor="middle" font-family="'Fira Code', 'JetBrains Mono', 'Source Code Pro', 'SF Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace" font-size="14" font-weight="bold" fill="url(#textGradient)" filter="url(#shadow)">dde</text>
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#333333" />
<stop offset="100%" stop-color="#222222" />
</linearGradient>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e32c2c" />
<stop offset="100%" stop-color="#ff5252" />
</linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="0.5" stdDeviation="0.5" flood-color="#000" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Square background with rounded corners -->
<rect x="2" y="2" width="28" height="28" rx="4" ry="4" fill="url(#bgGradient)" />
<!-- Subtle code brackets as background element -->
<g opacity="0.15" fill="#fff">
<path d="M10,7.5 L6.25,16 L10,24.5" stroke="#fff" stroke-width="1" fill="none"/>
<path d="M22,7.5 L25.75,16 L22,24.5" stroke="#fff" stroke-width="1" fill="none"/>
</g>
<!-- lowercase dde letters -->
<text x="16" y="21" text-anchor="middle" font-family="'Fira Code', 'JetBrains Mono', 'Source Code Pro', 'SF Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace" font-size="14" font-weight="bold" fill="url(#textGradient)" filter="url(#shadow)">dde</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,33 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<!-- Gradients and effects -->
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#333333" />
<stop offset="100%" stop-color="#222222" />
</linearGradient>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e32c2c" />
<stop offset="100%" stop-color="#ff5252" />
</linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.3"/>
</filter>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<!-- Square background with rounded corners -->
<rect x="18" y="18" width="220" height="220" rx="20" ry="20" fill="url(#bgGradient)" />
<!-- Subtle code brackets as background element -->
<g opacity="0.15" fill="#fff" filter="url(#glow)">
<path d="M80,60 L50,128 L80,196" stroke="#fff" stroke-width="8" fill="none"/>
<path d="M176,60 L206,128 L176,196" stroke="#fff" stroke-width="8" fill="none"/>
</g>
<!-- lowercase dde letters with shadow effect -->
<text x="128" y="154" text-anchor="middle" font-family="'Fira Code', 'JetBrains Mono', 'Source Code Pro', 'SF Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace" font-size="100" font-weight="bold" fill="url(#textGradient)" filter="url(#shadow)">dde</text>
<!-- Gradients and effects -->
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#333333" />
<stop offset="100%" stop-color="#222222" />
</linearGradient>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e32c2c" />
<stop offset="100%" stop-color="#ff5252" />
</linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.3"/>
</filter>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<!-- Square background with rounded corners -->
<rect x="18" y="18" width="220" height="220" rx="20" ry="20" fill="url(#bgGradient)" />
<!-- Subtle code brackets as background element -->
<g opacity="0.15" fill="#fff" filter="url(#glow)">
<path d="M80,60 L50,128 L80,196" stroke="#fff" stroke-width="8" fill="none"/>
<path d="M176,60 L206,128 L176,196" stroke="#fff" stroke-width="8" fill="none"/>
</g>
<!-- lowercase dde letters with shadow effect -->
<text x="128" y="154" text-anchor="middle" font-family="'Fira Code', 'JetBrains Mono', 'Source Code Pro', 'SF Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace" font-size="100" font-weight="bold" fill="url(#textGradient)" filter="url(#shadow)">dde</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -2,9 +2,9 @@ import { el } from "deka-dom-el";
// Create element with properties
const button = el("button", {
textContent: "Click me",
className: "primary",
disabled: true
textContent: "Click me",
className: "primary",
disabled: true
});
// Shorter and more expressive

View File

@ -6,12 +6,12 @@ button.disabled = true;
// Or using Object.assign()
const button2 = Object.assign(
document.createElement('button'),
{
textContent: "Click me",
className: "primary",
disabled: true
}
document.createElement('button'),
{
textContent: "Click me",
className: "primary",
disabled: true
}
);
// Add to DOM

View File

@ -2,7 +2,7 @@ import { el, on } from "deka-dom-el";
// Third approach - append with on addon
el("button", {
textContent: "click me"
textContent: "click me"
}).append(
on("click", (e) => console.log("Clicked!", e))
on("click", (e) => console.log("Clicked!", e))
);

View File

@ -2,6 +2,6 @@ import { el } from "deka-dom-el";
// Using events with HTML attribute style
el("button", {
textContent: "click me",
"=onclick": "console.log(event)"
textContent: "click me",
"=onclick": "console.log(event)"
});

View File

@ -2,7 +2,7 @@ import { el, on } from "deka-dom-el";
// Using events as addons - chainable approach
el("button", {
textContent: "click me",
},
on("click", (e) => console.log("Clicked!", e))
textContent: "click me",
},
on("click", (e) => console.log("Clicked!", e))
);

View File

@ -2,6 +2,6 @@ import { el } from "deka-dom-el";
// Using events with property assignment
el("button", {
textContent: "click me",
onclick: console.log
textContent: "click me",
onclick: console.log
});

View File

@ -4,11 +4,11 @@ const button = document.querySelector('button');
let count = 0;
button.addEventListener('click', () => {
count++;
document.querySelector('p').textContent =
'Clicked ' + count + ' times';
count++;
document.querySelector('p').textContent =
'Clicked ' + count + ' times';
if (count > 10) {
button.disabled = true;
}
if (count > 10) {
button.disabled = true;
}
});

View File

@ -4,11 +4,11 @@ const count = S(0);
// 2. React to state changes
S.on(count, value => {
updateUI(value);
if (value > 10) disableButton();
updateUI(value);
if (value > 10) disableButton();
});
// 3. Update state on events
button.addEventListener('click', () => {
count.set(count.get() + 1);
count.set(count.get() + 1);
});

View File

@ -3,28 +3,28 @@ import { S } from "deka-dom-el/signals";
// A HelloWorld component using the 3PS pattern
function HelloWorld({ emoji = "🚀" }) {
// PART 1: Create reactive state
const clicks = S(0);
return el().append(
// PART 2: Bind state to UI elements
el("p", {
className: "greeting",
// This paragraph automatically updates when clicks changes
textContent: S(() => `Hello World ${emoji.repeat(clicks.get())}`)
}),
// PART 3: Update state in response to events
el("button", {
type: "button",
textContent: "Add emoji",
// When clicked, update the state
onclick: () => clicks.set(clicks.get() + 1)
})
);
// PART 1: Create reactive state
const clicks = S(0);
return el().append(
// PART 2: Bind state to UI elements
el("p", {
className: "greeting",
// This paragraph automatically updates when clicks changes
textContent: S(() => `Hello World ${emoji.repeat(clicks.get())}`)
}),
// PART 3: Update state in response to events
el("button", {
type: "button",
textContent: "Add emoji",
// When clicked, update the state
onclick: () => clicks.set(clicks.get() + 1)
})
);
}
// Use the component in your app
document.body.append(
el(HelloWorld, { emoji: "🎉" })
el(HelloWorld, { emoji: "🎉" })
);

View File

@ -2,13 +2,13 @@ import { scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
function customSignalLogic() {
// Create an isolated scope for a specific operation
scope.push(); // Start new scope
// Create an isolated scope for a specific operation
scope.push(); // Start new scope
// These signals are in the new scope
const isolatedCount = S(0);
const isolatedDerived = S(() => isolatedCount.get() * 2);
// These signals are in the new scope
const isolatedCount = S(0);
const isolatedDerived = S(() => isolatedCount.get() * 2);
// Clean up by returning to previous scope
scope.pop();
// Clean up by returning to previous scope
scope.pop();
}

View File

@ -2,44 +2,44 @@ import { el, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
function CounterWithIsolatedTimer() {
const { host } = scope;
const { host } = scope;
// Main component state
const count = S(0);
// Main component state
const count = S(0);
// Create a timer in an isolated scope
scope.isolate(() => {
// These subscriptions won't be tied to the component lifecycle
// They would continue to run even if the component was removed
const timer = S(0);
// Create a timer in an isolated scope
scope.isolate(() => {
// These subscriptions won't be tied to the component lifecycle
// They would continue to run even if the component was removed
const timer = S(0);
// Not recommended for real applications!
// Just demonstrating scope isolation
setInterval(() => {
timer.set(timer.get() + 1);
console.log(`Timer: ${timer.get()}`);
}, 1000);
});
// Not recommended for real applications!
// Just demonstrating scope isolation
setInterval(() => {
timer.set(timer.get() + 1);
console.log(`Timer: ${timer.get()}`);
}, 1000);
});
// Normal component functionality within main scope
function increment() {
count.set(count.get() + 1);
}
// Normal component functionality within main scope
function increment() {
count.set(count.get() + 1);
}
return el("div").append(
el("p").append(
"Count: ",
el("#text", S(() => count.get()))
),
el("button", {
textContent: "Increment",
onclick: increment
}),
el("p", "An isolated timer runs in console")
);
return el("div").append(
el("p").append(
"Count: ",
el("#text", S(() => count.get()))
),
el("button", {
textContent: "Increment",
onclick: increment
}),
el("p", "An isolated timer runs in console")
);
}
// Usage
document.body.append(
el(CounterWithIsolatedTimer)
el(CounterWithIsolatedTimer)
);

View File

@ -20,7 +20,7 @@ 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 });
const element = el.mark({ type: "later", name: ireland.name });
queue(import(path).then(module => {
const component = module[exportName];
element.replaceWith(el(component, props, mark(id)));

View File

@ -104,7 +104,8 @@ export function page({ pkg, info }){
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", "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`
@ -112,4 +113,4 @@ export function page({ pkg, info }){
Let's get started with the basics of creating elements!
`),
);
}
}

View File

@ -55,21 +55,21 @@ export function page({ pkg, info }){
el(MyComponent);
function MyComponent() {
// 2. access the host element
const { host } = scope;
  // 2. access the host element
  const { host } = scope;
// 3. Add behavior to host
host(
on.click(handleClick)
);
  // 3. Add behavior to host
  host(
  on.click(handleClick)
  );
// 4. Return the host element
return el("div", {
className: "my-component"
}).append(
el("h2", "Title"),
el("p", "Content")
);
  // 4. Return the host element
  return el("div", {
  className: "my-component"
  }).append(
  el("h2", "Title"),
  el("p", "Content")
  );
}
`.trim()))
),
@ -118,9 +118,9 @@ function MyComponent() {
3. Component interactions happen
4. Component removed from DOM disconnected event
5. Automatic cleanup of:
- Event listeners
- Signal subscriptions
- Custom cleanup code
  - Event listeners
  - Signal subscriptions
  - Custom cleanup code
`))
),
el(example, { src: fileURL("./components/examples/scopes/cleaning.js"), page_id }),
@ -167,6 +167,9 @@ function MyComponent() {
el("li").append(...T`
${el("strong", "Capture host early:")} Use ${el("code", "const { host } = scope")} at component start
`),
el("li").append(...T`
${el("strong", "Define signals as constants:")} ${el("code", "const counter = S(0);")}
`),
el("li").append(...T`
${el("strong", "Prefer declarative patterns:")} Use signals to drive UI updates rather than manual DOM manipulation
`),

View File

@ -202,15 +202,14 @@ export function page({ pkg, info }){
el("h4", t`Shadow DOM Encapsulation`),
el("pre").append(el("code", `
<my-custom-element>
  
    #shadow-root
#shadow-root
Created with DDE:
<div>
<h2>Title</h2>
<p>Content</p>
      Created with DDE:
    
      <div>
       <h2>Title</h2>
       <p>Content</p>
`))
),
el(example, { src: fileURL("./components/examples/customElement/shadowRoot.js"), page_id }),

View File

@ -42,8 +42,8 @@ export function page({ pkg, info }){
// Basic structure of an addon
function myAddon(config) {
return function(element) {
// Apply functionality to element
element.dataset.myAddon = config.option;
// Apply functionality to element
element.dataset.myAddon = config.option;
};
}
@ -67,18 +67,15 @@ el("div", { id: "example" }, myAddon({ option: "value" }));
// 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);
// Initialize the third-party library
const instance = new ExternalLibrary(element, config);
// Set up cleanup when the element is removed
signal.addEventListener('abort', () => {
instance.destroy();
});
// Set up cleanup when the element is removed
signal.addEventListener('abort', () => {
instance.destroy();
});
return element;
return element;
};
}
// dde component
@ -157,8 +154,8 @@ function createEnhancedSignal(initialValue) {
// Return the original signal with added methods
return Object.assign(signal, {
increment,
decrement
increment,
decrement
});
}
@ -240,10 +237,12 @@ console.log(doubled.get()); // 10`, page_id }),
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("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("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`),

View File

@ -18,10 +18,11 @@ export function page({ pkg, info }){
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.
This part of the documentation is primarily intended for technical enthusiasts and authors of
3rd-party libraries. 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`

View File

@ -2,7 +2,8 @@ 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.`,
description: t`Creating live, interactive component examples in documentation with server-side
rendering and client-side hydration.`,
};
import { el } from "deka-dom-el";
@ -19,10 +20,11 @@ export function page({ pkg, info }){
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.
This part of the documentation is primarily intended for technical enthusiasts and authors of
3rd-party libraries. 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.
`)
),
@ -61,14 +63,15 @@ export function page({ pkg, info }){
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")}.
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",
src: fileURL("./components/examples/path/to/component.js"),
exportName: "NamedExport", // optional, defaults to "default",
})`, page_id }),
el("p").append(...T`
@ -96,27 +99,27 @@ 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");
// 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
@ -126,107 +129,107 @@ dispatchEvent("onssrend");
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);
// Ensure folder path ends with a slash
if(folder && !folder.endsWith("/")) folder += "/";
// 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);
// 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)));
}));
// Calculate relative path for imports
const path = "./"+relative(dir, src.pathname);
// 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,
});
// Generate unique ID for this component instance
const id = "ireland-" + generateComponentId(src);
return element;
// Create placeholder element
const element = el.mark({ type: "later", 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(\`
// 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"
}
"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(\`
\`.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())
);
\`.trim())
);
}
`, page_id }),
el("h4", t`Client-Side Hydration`),
@ -235,22 +238,22 @@ loadIrelands(new Map(JSON.parse(\${store})));
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));
});
});
// 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 }),
@ -287,7 +290,7 @@ export function loadIrelands(store) {
${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

19
index.d.ts vendored
View File

@ -73,14 +73,17 @@ export function assignAttribute<El extends SupportedElement, ATT extends keyof E
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;
/**
* Creates a marker comment for elements
*
* @param attrs - Marker attributes
* @param [is_open=false] - Whether the marker is open-ended
* @returns Comment node marker
*/
export function mark(
attrs: { type: "component"|"reactive"|"later", name?: string, host?: "this"|"parentElement" },
is_open?: boolean
): Comment;
}
export function el<