mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-01 04:12:14 +02:00
⚡ dde and docs improvements (#27)
* ⚡ 🎉 * ⚡ wip * 🔤 * ⚡ wip * ⚡ wip * ⚡ Refatc signals to .get/.set syntax #26 * 🐛 Better types for on* * 🔤 * 🔤 * 🐛 coumputed signal * 🔤 ⚡ Docs UI/UX * ⚡ 🔤 UI enhancements * ⚡ (bs) (un)min * 🔤 adds debugging * 🔤 ssr * 🔤 * ⚡ bs/lint * 🔤 * 🔤 UI * 🔤 updates texts * 🔤UI * ⚡ dispatch * 🔤 events * 🔤 elements * 🔤 intro * 🐛 fixes completitions for el with components * 🐛 wrong file(s) in git * 🔤 logo * 🐛 🔤 types 3ps * 🔤 ui/ux * 🔤 * 🔤 * 🔤 scopes * 🔤 * 🔤 ui/ux * 🔤 * ⚡ issignal * 🔤 improvemens * ⚡ irelands * 🔤 UI/UX/wording * 🐛 npx-hint [Scrollable region must have keyboard access | Axe Rules | Deque University | Deque Systems](https://dequeuniversity.com/rules/axe/4.10/scrollable-region-focusable?application=axeAPI) * 🔤 logos * ⚡ better? dts builds * Update README.md
This commit is contained in:
@ -1,28 +1,179 @@
|
||||
import { registerClientFile, styles } from "../ssr.js";
|
||||
const host= "."+code.name;
|
||||
styles.css`
|
||||
${host}{
|
||||
--shiki-color-text: #e9eded;
|
||||
--shiki-color-background: #212121;
|
||||
/* Code block styling */
|
||||
${host} {
|
||||
/* Theme for dark mode - matches Flems/CodeMirror dark theme */
|
||||
--shiki-color-text: #f8f8f2;
|
||||
--shiki-color-background: var(--code-bg);
|
||||
--shiki-token-constant: #82b1ff;
|
||||
--shiki-token-string: #c3e88d;
|
||||
--shiki-token-comment: #546e7a;
|
||||
--shiki-token-keyword: #c792ea;
|
||||
--shiki-token-parameter: #AA0000;
|
||||
--shiki-token-parameter: #fd971f;
|
||||
--shiki-token-function: #80cbae;
|
||||
--shiki-token-string-expression: #c3e88d;
|
||||
--shiki-token-punctuation: var(--code);
|
||||
--shiki-token-link: #EE0000;
|
||||
--shiki-token-punctuation: #89ddff;
|
||||
--shiki-token-link: #82aaff;
|
||||
--shiki-token-variable: #f8f8f2;
|
||||
--shiki-token-number: #f78c6c;
|
||||
--shiki-token-boolean: #82b1ff;
|
||||
--shiki-token-tag: #f07178;
|
||||
--shiki-token-attribute: #ffcb6b;
|
||||
--shiki-token-property: #82b1ff;
|
||||
--shiki-token-operator: #89ddff;
|
||||
--shiki-token-regex: #c3e88d;
|
||||
--shiki-token-class: #ffcb6b;
|
||||
|
||||
/* Basic styling */
|
||||
white-space: pre;
|
||||
tab-size: 2; /* TODO: allow custom tab size?! */
|
||||
overflow: scroll;
|
||||
tab-size: 2;
|
||||
overflow: auto;
|
||||
border-radius: var(--border-radius);
|
||||
font-family: var(--font-mono);
|
||||
font-size: .85rem;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
margin-block: 1rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
${host}[data-js=todo]{
|
||||
|
||||
/* Light mode overrides to match GitHub-like theme */
|
||||
@media (prefers-color-scheme: light) {
|
||||
${host} {
|
||||
--shiki-color-text: #24292e;
|
||||
--shiki-color-background: var(--code-bg);
|
||||
--shiki-token-constant: #005cc5;
|
||||
--shiki-token-string: #22863a;
|
||||
--shiki-token-comment: #6a737d;
|
||||
--shiki-token-keyword: #d73a49;
|
||||
--shiki-token-parameter: #e36209;
|
||||
--shiki-token-function: #6f42c1;
|
||||
--shiki-token-string-expression: #22863a;
|
||||
--shiki-token-punctuation: #24292e;
|
||||
--shiki-token-link: #0366d6;
|
||||
--shiki-token-variable: #24292e;
|
||||
--shiki-token-number: #005cc5;
|
||||
--shiki-token-boolean: #005cc5;
|
||||
--shiki-token-tag: #22863a;
|
||||
--shiki-token-attribute: #005cc5;
|
||||
--shiki-token-property: #005cc5;
|
||||
--shiki-token-operator: #d73a49;
|
||||
--shiki-token-regex: #032f62;
|
||||
--shiki-token-class: #6f42c1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Support for theme toggles */
|
||||
html[data-theme="light"] ${host} {
|
||||
--shiki-color-text: #24292e;
|
||||
--shiki-color-background: var(--code-bg);
|
||||
--shiki-token-constant: #005cc5;
|
||||
--shiki-token-string: #22863a;
|
||||
--shiki-token-comment: #6a737d;
|
||||
--shiki-token-keyword: #d73a49;
|
||||
--shiki-token-parameter: #e36209;
|
||||
--shiki-token-function: #6f42c1;
|
||||
--shiki-token-string-expression: #22863a;
|
||||
--shiki-token-punctuation: #24292e;
|
||||
--shiki-token-link: #0366d6;
|
||||
--shiki-token-variable: #24292e;
|
||||
--shiki-token-number: #005cc5;
|
||||
--shiki-token-boolean: #005cc5;
|
||||
--shiki-token-tag: #22863a;
|
||||
--shiki-token-attribute: #005cc5;
|
||||
--shiki-token-property: #005cc5;
|
||||
--shiki-token-operator: #d73a49;
|
||||
--shiki-token-regex: #032f62;
|
||||
--shiki-token-class: #6f42c1;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] ${host} {
|
||||
--shiki-color-text: #f8f8f2;
|
||||
--shiki-color-background: var(--code-bg);
|
||||
--shiki-token-constant: #82b1ff;
|
||||
--shiki-token-string: #c3e88d;
|
||||
--shiki-token-comment: #546e7a;
|
||||
--shiki-token-keyword: #c792ea;
|
||||
--shiki-token-parameter: #fd971f;
|
||||
--shiki-token-function: #80cbae;
|
||||
--shiki-token-string-expression: #c3e88d;
|
||||
--shiki-token-punctuation: #89ddff;
|
||||
--shiki-token-link: #82aaff;
|
||||
--shiki-token-variable: #f8f8f2;
|
||||
--shiki-token-number: #f78c6c;
|
||||
--shiki-token-boolean: #82b1ff;
|
||||
--shiki-token-tag: #f07178;
|
||||
--shiki-token-attribute: #ffcb6b;
|
||||
--shiki-token-property: #82b1ff;
|
||||
--shiki-token-operator: #89ddff;
|
||||
--shiki-token-regex: #c3e88d;
|
||||
--shiki-token-class: #ffcb6b;
|
||||
}
|
||||
|
||||
/* Code block with syntax highlighting waiting for JS */
|
||||
${host}[data-js=todo] {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--standard-border-radius);
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 18.4px; /* to fix shift when → dataJS=done */
|
||||
padding: 1rem 1.4rem;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
background-color: var(--code-bg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add a subtle loading indicator */
|
||||
${host}[data-js=todo]::before {
|
||||
content: "Loading syntax highlighting...";
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-light);
|
||||
background-color: var(--bg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* All code blocks should have consistent font and sizing */
|
||||
${host} code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: inherit;
|
||||
line-height: 1.5;
|
||||
padding: 0;
|
||||
}
|
||||
${host} pre {
|
||||
margin-block: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Ensure line numbers (if added) are styled appropriately */
|
||||
${host} .line-number {
|
||||
user-select: none;
|
||||
opacity: 0.5;
|
||||
margin-right: 1rem;
|
||||
min-width: 1.5rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* If there's a copy button, style it */
|
||||
${host} .copy-button {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
${host}:hover .copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
import { el } from "deka-dom-el";
|
||||
@ -43,8 +194,8 @@ export function code({ id, src, content, language= "js", className= host.slice(1
|
||||
registerClientPart(page_id);
|
||||
dataJS= "todo";
|
||||
}
|
||||
return el("div", { id, className, dataJS }).append(
|
||||
el("code", { className: "language-"+language, textContent: content })
|
||||
return el("div", { id, className, dataJS, tabIndex: 0 }).append(
|
||||
el("code", { className: "language-"+language, textContent: content.trim() })
|
||||
);
|
||||
}
|
||||
let is_registered= {};
|
||||
@ -52,12 +203,45 @@ let is_registered= {};
|
||||
function registerClientPart(page_id){
|
||||
if(is_registered[page_id]) return;
|
||||
|
||||
// Add Shiki with a more reliable loading method
|
||||
document.head.append(
|
||||
el("script", { src: "https://cdn.jsdelivr.net/npm/shiki@0.9", defer: true }),
|
||||
// Use a newer version of Shiki with better performance
|
||||
el("script", { src: "https://cdn.jsdelivr.net/npm/shiki@0.14.3/dist/index.unpkg.iife.js", defer: true }),
|
||||
// Make sure we can match Flems styling in dark/light mode
|
||||
el("style", `
|
||||
/* Ensure CodeMirror and Shiki use the same font */
|
||||
.CodeMirror *, .shiki * {
|
||||
font-family: var(--font-mono) !important;
|
||||
}
|
||||
|
||||
/* Style Shiki's output to match our theme */
|
||||
.shiki {
|
||||
background-color: var(--shiki-color-background) !important;
|
||||
color: var(--shiki-color-text) !important;
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
/* Ensure Shiki code tokens use our CSS variables */
|
||||
.shiki .keyword { color: var(--shiki-token-keyword) !important; }
|
||||
.shiki .constant { color: var(--shiki-token-constant) !important; }
|
||||
.shiki .string { color: var(--shiki-token-string) !important; }
|
||||
.shiki .comment { color: var(--shiki-token-comment) !important; }
|
||||
.shiki .function { color: var(--shiki-token-function) !important; }
|
||||
.shiki .operator, .shiki .punctuation { color: var(--shiki-token-punctuation) !important; }
|
||||
.shiki .parameter { color: var(--shiki-token-parameter) !important; }
|
||||
.shiki .variable { color: var(--shiki-token-variable) !important; }
|
||||
.shiki .property { color: var(--shiki-token-property) !important; }
|
||||
`),
|
||||
);
|
||||
|
||||
registerClientFile(
|
||||
new URL("./code.js.js", import.meta.url),
|
||||
el("script", { type: "module" })
|
||||
{
|
||||
head: el("script", { type: "module" }),
|
||||
}
|
||||
);
|
||||
|
||||
is_registered[page_id]= true;
|
||||
}
|
||||
|
@ -1,12 +1,61 @@
|
||||
const highlighter= await globalThis.shiki.getHighlighter({
|
||||
theme: "css-variables",
|
||||
langs: ["js", "ts", "css", "html", "shell"],
|
||||
});
|
||||
const codeBlocks= document.querySelectorAll('code[class*="language-"]');
|
||||
try {
|
||||
// Initialize Shiki with our custom theme
|
||||
const highlighter = await globalThis.shiki.getHighlighter({
|
||||
theme: "css-variables",
|
||||
langs: ["javascript", "typescript", "css", "html", "shell"],
|
||||
});
|
||||
|
||||
codeBlocks.forEach((block)=> {
|
||||
const lang= block.className.replace("language-", "");
|
||||
block.parentElement.dataset.js= "done";
|
||||
const html= highlighter.codeToHtml(block.textContent, { lang });
|
||||
block.innerHTML= html;
|
||||
});
|
||||
// Find all code blocks that need highlighting
|
||||
const codeBlocks = document.querySelectorAll('div[data-js="todo"] code[class*="language-"]');
|
||||
|
||||
// Process each code block
|
||||
codeBlocks.forEach((block) => {
|
||||
try {
|
||||
// Get the language from the class
|
||||
const langClass = block.className.match(/language-(\w+)/);
|
||||
|
||||
// Map the language to Shiki format
|
||||
let lang = langClass ? langClass[1] : 'javascript';
|
||||
if (lang === 'js') lang = 'javascript';
|
||||
if (lang === 'ts') lang = 'typescript';
|
||||
|
||||
// Mark the container as processed
|
||||
block.parentElement.dataset.js = "done";
|
||||
|
||||
// Highlight the code
|
||||
const code = block.textContent;
|
||||
const html = highlighter.codeToHtml(code, { lang });
|
||||
|
||||
// Insert the highlighted HTML
|
||||
block.innerHTML = html;
|
||||
|
||||
// Add copy button functionality
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'copy-button';
|
||||
copyBtn.textContent = 'Copy';
|
||||
copyBtn.setAttribute('aria-label', 'Copy code to clipboard');
|
||||
copyBtn.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
copyBtn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
copyBtn.textContent = 'Copy';
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
// Add the copy button to the code block container
|
||||
block.parentElement.appendChild(copyBtn);
|
||||
} catch (err) {
|
||||
console.error('Error highlighting code block:', err);
|
||||
// Make sure we don't leave the block in a pending state
|
||||
block.parentElement.dataset.js = "error";
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize Shiki:', err);
|
||||
|
||||
// Fallback: at least mark blocks as processed so they don't show loading indicator
|
||||
document.querySelectorAll('div[data-js="todo"]').forEach(block => {
|
||||
block.dataset.js = "error";
|
||||
});
|
||||
}
|
||||
|
@ -1,16 +1,100 @@
|
||||
import { styles } from "../ssr.js";
|
||||
const host= "."+example.name;
|
||||
styles.css`
|
||||
${host}{
|
||||
${host} {
|
||||
grid-column: full-main;
|
||||
width: 100%;
|
||||
max-width: calc(9/5 * var(--body-max-width));
|
||||
height: calc(3/5 * var(--body-max-width));
|
||||
margin-inline: auto;
|
||||
width: calc(100% - .75em);
|
||||
height: calc(4/6 * var(--body-max-width));
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
${host} .runtime {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
/* CodeMirror styling to match our theme */
|
||||
.CodeMirror {
|
||||
height: 100% !important;
|
||||
font-family: var(--font-mono) !important;
|
||||
font-size: .85rem !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
/* Dark mode styles for CodeMirror */
|
||||
.CodeMirror, .CodeMirror-gutters {
|
||||
background: #212121 !important;
|
||||
border: 1px solid white;
|
||||
background: var(--code-bg) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
/* Light mode adjustments for CodeMirror - using CSS variables */
|
||||
@media (prefers-color-scheme: light) {
|
||||
/* Core syntax elements */
|
||||
.cm-s-material .cm-keyword { color: var(--shiki-token-keyword, #d73a49) !important; }
|
||||
.cm-s-material .cm-atom { color: var(--shiki-token-constant, #005cc5) !important; }
|
||||
.cm-s-material .cm-number { color: var(--shiki-token-number, #005cc5) !important; }
|
||||
.cm-s-material .cm-def { color: var(--shiki-token-function, #6f42c1) !important; }
|
||||
.cm-s-material .cm-variable { color: var(--shiki-token-variable, #24292e) !important; }
|
||||
.cm-s-material .cm-variable-2 { color: var(--shiki-token-variable, #24292e) !important; }
|
||||
.cm-s-material .cm-variable-3 { color: var(--shiki-token-variable, #24292e) !important; }
|
||||
.cm-s-material .cm-property { color: var(--shiki-token-property, #005cc5) !important; }
|
||||
.cm-s-material .cm-operator { color: var(--shiki-token-operator, #d73a49) !important; }
|
||||
.cm-s-material .cm-comment { color: var(--shiki-token-comment, #6a737d) !important; }
|
||||
.cm-s-material .cm-string { color: var(--shiki-token-string, #22863a) !important; }
|
||||
.cm-s-material .cm-string-2 { color: var(--shiki-token-string, #22863a) !important; }
|
||||
.cm-s-material .cm-tag { color: var(--shiki-token-tag, #22863a) !important; }
|
||||
.cm-s-material .cm-attribute { color: var(--shiki-token-attribute, #005cc5) !important; }
|
||||
.cm-s-material .cm-bracket { color: var(--shiki-token-punctuation, #24292e) !important; }
|
||||
.cm-s-material .cm-punctuation { color: var(--shiki-token-punctuation, #24292e) !important; }
|
||||
.cm-s-material .cm-link { color: var(--shiki-token-link, #0366d6) !important; }
|
||||
.cm-s-material .cm-error { color: #f44336 !important; }
|
||||
}
|
||||
|
||||
/* Handle theme toggle */
|
||||
html[data-theme="light"] .CodeMirror {
|
||||
background: #f5f7fa !important;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .CodeMirror-gutters {
|
||||
background: #f5f7fa !important;
|
||||
border-right: 1px solid #e5e7eb !important;
|
||||
}
|
||||
|
||||
/* Also apply the same styles to CodeMirror with data-theme */
|
||||
html[data-theme="light"] .cm-s-material .cm-keyword { color: var(--shiki-token-keyword, #d73a49) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-atom { color: var(--shiki-token-constant, #005cc5) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-number { color: var(--shiki-token-number, #005cc5) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-def { color: var(--shiki-token-function, #6f42c1) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-variable { color: var(--shiki-token-variable, #24292e) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-variable-2 { color: var(--shiki-token-variable, #24292e) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-variable-3 { color: var(--shiki-token-variable, #24292e) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-property { color: var(--shiki-token-property, #005cc5) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-operator { color: var(--shiki-token-operator, #d73a49) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-comment { color: var(--shiki-token-comment, #6a737d) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-string { color: var(--shiki-token-string, #22863a) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-string-2 { color: var(--shiki-token-string, #22863a) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-tag { color: var(--shiki-token-tag, #22863a) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-attribute { color: var(--shiki-token-attribute, #005cc5) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-bracket { color: var(--shiki-token-punctuation, #24292e) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-punctuation { color: var(--shiki-token-punctuation, #24292e) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-link { color: var(--shiki-token-link, #0366d6) !important; }
|
||||
html[data-theme="light"] .cm-s-material .cm-error { color: #f44336 !important; }
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 767px) {
|
||||
${host} {
|
||||
height: 50vh;
|
||||
max-width: 100%;
|
||||
}
|
||||
${host} main {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
${host} main > * {
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -26,7 +26,7 @@ function ddeComponent({ attr }){
|
||||
on.connected(e=> console.log(( /** @type {HTMLParagraphElement} */ (e.target)).outerHTML)),
|
||||
);
|
||||
return el().append(
|
||||
el("p", S(()=> `Hello from Custom Element with attribute '${attr()}'`))
|
||||
el("p", S(()=> `Hello from Custom Element with attribute '${attr.get()}'`))
|
||||
);
|
||||
}
|
||||
customElementWithDDE(HTMLCustomElement);
|
||||
|
15
docs/components/examples/debugging/consoleLog.js
Normal file
15
docs/components/examples/debugging/consoleLog.js
Normal file
@ -0,0 +1,15 @@
|
||||
// Debugging a (derived) signal with `console.log`
|
||||
import { S } from "deka-dom-el/signals";
|
||||
const name= S("Alice");
|
||||
const greeting = S(() => {
|
||||
// log derived signals
|
||||
const log = "Hello, " + name.get();
|
||||
console.log(log);
|
||||
console.log(name.valueOf());
|
||||
return log;
|
||||
});
|
||||
|
||||
// log signals in general
|
||||
S.on(greeting, value => console.log("Greeting changed to:", value));
|
||||
|
||||
name.set("Bob"); // Should trigger computation and listener`)
|
15
docs/components/examples/debugging/debouncing.js
Normal file
15
docs/components/examples/debugging/debouncing.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { S } from "deka-dom-el/signals";
|
||||
// Debouncing signal updates
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return (...args)=> {
|
||||
clearTimeout(timeout);
|
||||
timeout= setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
const inputSignal= S("");
|
||||
const debouncedSet= debounce(value => inputSignal.set(value), 300);
|
||||
|
||||
// In your input handler
|
||||
inputElement.addEventListener("input", e=> debouncedSet(e.target.value));
|
4
docs/components/examples/debugging/dom-reactive-mark.js
Normal file
4
docs/components/examples/debugging/dom-reactive-mark.js
Normal file
@ -0,0 +1,4 @@
|
||||
// Example of reactive element marker
|
||||
<!--<dde:mark type=\"reactive\" source=\"...\">-->
|
||||
<!-- content that updates when signal changes -->
|
||||
<!--</dde:mark>-->
|
15
docs/components/examples/debugging/mutations.js
Normal file
15
docs/components/examples/debugging/mutations.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { S } from "deka-dom-el/signals";
|
||||
// Wrong - direct mutation doesn't trigger updates
|
||||
const todos1 = S([{ text: "Learn signals", completed: false }]);
|
||||
todos1.get().push({ text: "Debug signals", completed: false }); // Won't trigger updates!
|
||||
|
||||
// Correct - using .set() with a new array
|
||||
todos1.set([...todos1.get(), { text: "Debug signals", completed: false }]);
|
||||
|
||||
// Better - using actions
|
||||
const todos2 = S([], {
|
||||
add(text) {
|
||||
this.value.push({ text, completed: false });
|
||||
}
|
||||
});
|
||||
S.action(todos2, "add", "Debug signals");
|
14
docs/components/examples/elements/dde-dom-create.js
Normal file
14
docs/components/examples/elements/dde-dom-create.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { el } from "deka-dom-el";
|
||||
|
||||
// Create element with properties
|
||||
const button = el("button", {
|
||||
textContent: "Click me",
|
||||
className: "primary",
|
||||
disabled: true
|
||||
});
|
||||
|
||||
// Shorter and more expressive
|
||||
// than the native approach
|
||||
|
||||
// Add to DOM
|
||||
document.body.append(button);
|
11
docs/components/examples/elements/dde-dom-tree.js
Normal file
11
docs/components/examples/elements/dde-dom-tree.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { el } from "deka-dom-el";
|
||||
|
||||
// Chainable, natural nesting
|
||||
// append() returns parent element
|
||||
// making chains easy and intuitive
|
||||
document.body.append(
|
||||
el("div").append(
|
||||
el("h1", "Title"),
|
||||
el("p", "Paragraph")
|
||||
)
|
||||
);
|
19
docs/components/examples/elements/native-dom-create.js
Normal file
19
docs/components/examples/elements/native-dom-create.js
Normal file
@ -0,0 +1,19 @@
|
||||
// Create element with properties
|
||||
const button = document.createElement('button');
|
||||
button.textContent = "Click me";
|
||||
button.className = "primary";
|
||||
button.disabled = true;
|
||||
|
||||
// Or using Object.assign()
|
||||
const button2 = Object.assign(
|
||||
document.createElement('button'),
|
||||
{
|
||||
textContent: "Click me",
|
||||
className: "primary",
|
||||
disabled: true
|
||||
}
|
||||
);
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(button2);
|
15
docs/components/examples/elements/native-dom-tree.js
Normal file
15
docs/components/examples/elements/native-dom-tree.js
Normal file
@ -0,0 +1,15 @@
|
||||
// Verbose, needs temp variables
|
||||
const div = document.createElement('div');
|
||||
const h1 = document.createElement('h1');
|
||||
h1.textContent = 'Title';
|
||||
div.appendChild(h1);
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'Paragraph';
|
||||
div.appendChild(p);
|
||||
|
||||
// appendChild doesn't return parent
|
||||
// so chaining is not possible
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(div);
|
8
docs/components/examples/events/append-event.js
Normal file
8
docs/components/examples/events/append-event.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { el, on } from "deka-dom-el";
|
||||
|
||||
// Third approach - append with on addon
|
||||
el("button", {
|
||||
textContent: "click me"
|
||||
}).append(
|
||||
on("click", (e) => console.log("Clicked!", e))
|
||||
);
|
7
docs/components/examples/events/attribute-event.js
Normal file
7
docs/components/examples/events/attribute-event.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { el } from "deka-dom-el";
|
||||
|
||||
// Using events with HTML attribute style
|
||||
el("button", {
|
||||
textContent: "click me",
|
||||
"=onclick": "console.log(event)"
|
||||
});
|
8
docs/components/examples/events/chain-event.js
Normal file
8
docs/components/examples/events/chain-event.js
Normal file
@ -0,0 +1,8 @@
|
||||
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))
|
||||
);
|
2
docs/components/examples/events/native-event.js
Normal file
2
docs/components/examples/events/native-event.js
Normal file
@ -0,0 +1,2 @@
|
||||
// Standard DOM event listener approach
|
||||
element.addEventListener('click', callback, options);
|
7
docs/components/examples/events/property-event.js
Normal file
7
docs/components/examples/events/property-event.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { el } from "deka-dom-el";
|
||||
|
||||
// Using events with property assignment
|
||||
el("button", {
|
||||
textContent: "click me",
|
||||
onclick: console.log
|
||||
});
|
14
docs/components/examples/introducing/3ps-before.js
Normal file
14
docs/components/examples/introducing/3ps-before.js
Normal file
@ -0,0 +1,14 @@
|
||||
// pseudocode
|
||||
// Mixed concerns make code hard to maintain
|
||||
const button = document.querySelector('button');
|
||||
let count = 0;
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
count++;
|
||||
document.querySelector('p').textContent =
|
||||
'Clicked ' + count + ' times';
|
||||
|
||||
if (count > 10) {
|
||||
button.disabled = true;
|
||||
}
|
||||
});
|
@ -1,6 +1,14 @@
|
||||
// pseudo code!
|
||||
const onchage=
|
||||
event=>
|
||||
console.log("Reacting to the:", event); // A
|
||||
input.addEventListener("change", onchange); // B
|
||||
input.dispatchEvent(new Event("change")); // C
|
||||
// pseudocode
|
||||
// 1. Create state
|
||||
const count = S(0);
|
||||
|
||||
// 2. React to state changes
|
||||
S.on(count, value => {
|
||||
updateUI(value);
|
||||
if (value > 10) disableButton();
|
||||
});
|
||||
|
||||
// 3. Update state on events
|
||||
button.addEventListener('click', () => {
|
||||
count.set(count.get() + 1);
|
||||
});
|
||||
|
@ -1,19 +1,30 @@
|
||||
import { el } from "deka-dom-el";
|
||||
import { el, on } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
const threePS= ({ emoji= "🚀" })=> {
|
||||
const clicks= S(0); // A
|
||||
return el().append(
|
||||
el("p", S(()=>
|
||||
"Hello World "+emoji.repeat(clicks()) // B
|
||||
)),
|
||||
el("button", {
|
||||
type: "button",
|
||||
onclick: ()=> clicks(clicks()+1), // C
|
||||
textContent: "Fire",
|
||||
})
|
||||
);
|
||||
};
|
||||
document.body.append(
|
||||
el(threePS, { emoji: "🎉" }),
|
||||
);
|
||||
|
||||
// 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)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Use the component in your app
|
||||
document.body.append(
|
||||
el(HelloWorld, { emoji: "🎉" })
|
||||
);
|
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: "+",
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
14
docs/components/examples/scopes/custom-scope.js
Normal file
14
docs/components/examples/scopes/custom-scope.js
Normal file
@ -0,0 +1,14 @@
|
||||
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
|
||||
|
||||
// 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();
|
||||
}
|
@ -1,35 +1,26 @@
|
||||
/* PSEUDO-CODE!!! */
|
||||
import { el } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
function component(){
|
||||
/* prepare changeable data */
|
||||
const dataA= S("data");
|
||||
const dataB= S("data");
|
||||
/* define data flow (can be asynchronous) */
|
||||
fetchAPI().then(data_new=> dataA(data_new));
|
||||
setTimeout(()=> dataB("DATA"));
|
||||
/* declarative UI */
|
||||
return el().append(
|
||||
el("h1", {
|
||||
textContent: "Example",
|
||||
/* declarative attribute(s) */
|
||||
classList: { declarative: dataB }
|
||||
}),
|
||||
el("ul").append(
|
||||
/* declarative element(s) */
|
||||
S.el(dataA, data=> data.map(d=> el("li", d)))
|
||||
),
|
||||
el("ul").append(
|
||||
/* declarative component(s) */
|
||||
S.el(dataA, data=> data.map(d=> el(subcomponent, d)))
|
||||
)
|
||||
function Counter() {
|
||||
// Define state
|
||||
const count = S(0);
|
||||
|
||||
// Define behavior
|
||||
const increment = () => count.set(count.get() + 1);
|
||||
|
||||
// Define data flow
|
||||
setTimeout(increment, 1000);
|
||||
// or fetchAPI().then(increment);
|
||||
|
||||
// Declarative UI (how to render data/`count`)
|
||||
// …automatically updates when changes
|
||||
return el("div").append(
|
||||
// declarative element(s)
|
||||
el("p", S(() => "Count: " + count.get())),
|
||||
el("button", {
|
||||
onclick: increment,
|
||||
textContent: "Increment",
|
||||
// declarative attribute(s)
|
||||
disabled: S(() => count.get() >= 10)
|
||||
})
|
||||
);
|
||||
}
|
||||
function subcomponent({ id }){
|
||||
/* prepare changeable data */
|
||||
const textContent= S("…");
|
||||
/* define data flow (can be asynchronous) */
|
||||
fetchAPI(id).then(text=> textContent(text));
|
||||
/* declarative UI */
|
||||
return el("li", { textContent, dataId: id });
|
||||
}
|
||||
|
@ -1,31 +1,25 @@
|
||||
/* PSEUDO-CODE!!! */
|
||||
import { el, on, scope } from "deka-dom-el";
|
||||
function component(){
|
||||
const { host }= scope;
|
||||
const ul= el("ul");
|
||||
const ac= new AbortController();
|
||||
fetchAPI({ signal: ac.signal }).then(data=> {
|
||||
data.forEach(d=> ul.append(el("li", d)));
|
||||
});
|
||||
host(
|
||||
/* element was remove before data fetched */
|
||||
on.disconnected(()=> ac.abort())
|
||||
import { el, scope } from "deka-dom-el";
|
||||
function Counter() {
|
||||
const { host } = scope;
|
||||
|
||||
let count = 0;
|
||||
const counterText = el("p", "Count: 0");
|
||||
|
||||
// Manually update DOM element
|
||||
const increment = () => {
|
||||
count++;
|
||||
counterText.textContent = "Count: " + count;
|
||||
host().querySelector("button").disabled = count >= 10;
|
||||
};
|
||||
setTimeout(increment, 1000);
|
||||
// or fetchAPI().then(increment);
|
||||
|
||||
return el("div").append(
|
||||
counterText,
|
||||
el("button", {
|
||||
onclick: increment,
|
||||
textContent: "Increment"
|
||||
})
|
||||
);
|
||||
return ul;
|
||||
/**
|
||||
* NEVER EVER!!
|
||||
* let data;
|
||||
* fetchAPI().then(d=> data= O(d));
|
||||
*
|
||||
* OR NEVER EVER!!
|
||||
* const ul= el("ul");
|
||||
* fetchAPI().then(d=> {
|
||||
* const data= O("data");
|
||||
* ul.append(el("li", data));
|
||||
* });
|
||||
*
|
||||
* // THE HOST IS PROBABLY DIFFERENT THAN
|
||||
* // YOU EXPECT AND OBSERVABLES MAY BE
|
||||
* // UNEXPECTEDLY REMOVED!!!
|
||||
* */
|
||||
}
|
||||
|
36
docs/components/examples/scopes/mixed.js
Normal file
36
docs/components/examples/scopes/mixed.js
Normal file
@ -0,0 +1,36 @@
|
||||
/* PSEUDO-CODE!!! */
|
||||
import { el, scope } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
function Counter() {
|
||||
const { host } = scope;
|
||||
|
||||
let count = S(0);
|
||||
const counterText = el("p", "Count: 0");
|
||||
S.on(count, c=> counterText.textContent= "Count: " + c);
|
||||
|
||||
// Manually update DOM element
|
||||
const increment = () => {
|
||||
count.set(count.get() + 1);
|
||||
// NEVER EVER
|
||||
// count = S(count.get() + 1);
|
||||
// THE HOST IS PROBABLY DIFFERENT THAN
|
||||
// YOU EXPECT AND SIGNAL MAY BE
|
||||
// UNEXPECTEDLY REMOVED!!!
|
||||
host().querySelector("button").disabled = count.get() >= 10;
|
||||
};
|
||||
setTimeout(()=> {
|
||||
// ok, BUT consider extract to separate function
|
||||
// see section below for more info
|
||||
const ok= S(0);
|
||||
S.on(ok, console.log);
|
||||
setInterval(()=> ok.set(ok.get() + 1), 100);
|
||||
}, 100);
|
||||
|
||||
return el("div").append(
|
||||
counterText,
|
||||
el("button", {
|
||||
onclick: increment,
|
||||
textContent: "Increment"
|
||||
})
|
||||
);
|
||||
}
|
45
docs/components/examples/scopes/with-scope.js
Normal file
45
docs/components/examples/scopes/with-scope.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { el, scope } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
function CounterWithIsolatedTimer() {
|
||||
const { host } = scope;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
@ -15,4 +15,4 @@ setTimeout(
|
||||
clearInterval,
|
||||
10*interval,
|
||||
setInterval(oninterval, interval)
|
||||
);
|
||||
);
|
@ -22,9 +22,9 @@ const onsubmit= on("submit", function(event){
|
||||
S.action(todos, "push", data.get("todo"));
|
||||
break;
|
||||
case "E"/*dit*/: {
|
||||
const last= todos().at(-1);
|
||||
const last= todos.get().at(-1);
|
||||
if(!last) break;
|
||||
last(data.get("todo"));
|
||||
last.set(data.get("todo"));
|
||||
break;
|
||||
}
|
||||
case "R"/*emove*/:
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { S } from "deka-dom-el/signals";
|
||||
const signal= S(0);
|
||||
// computation pattern
|
||||
const double= S(()=> 2*signal());
|
||||
const double= S(()=> 2*signal.get());
|
||||
|
||||
const ac= new AbortController();
|
||||
S.on(signal, v=> console.log("signal", v), { signal: ac.signal });
|
||||
S.on(double, v=> console.log("double", v), { signal: ac.signal });
|
||||
|
||||
signal(signal()+1);
|
||||
signal.set(signal.get()+1);
|
||||
const interval= 5 * 1000;
|
||||
const id= setInterval(()=> signal(signal()+1), interval);
|
||||
const id= setInterval(()=> signal.set(signal.get()+1), interval);
|
||||
ac.signal.addEventListener("abort",
|
||||
()=> setTimeout(()=> clearInterval(id), 2*interval));
|
||||
|
||||
setTimeout(()=> ac.abort(), 3*interval)
|
||||
setTimeout(()=> ac.abort(), 3*interval)
|
20
docs/components/examples/signals/debugging-console.js
Normal file
20
docs/components/examples/signals/debugging-console.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
// Debugging a derived signal
|
||||
const name = S('Alice');
|
||||
const greeting = S(() => {
|
||||
console.log('Computing greeting...');
|
||||
return 'Hello, ' + name.get();
|
||||
});
|
||||
|
||||
// Monitor the derived signal
|
||||
S.on(greeting, value => console.log('Greeting changed to:', value));
|
||||
|
||||
// Later update the dependency
|
||||
name.set('Bob'); // Should trigger computation and listener
|
||||
|
||||
// Console output:
|
||||
// Computing greeting...
|
||||
// Greeting changed to: Hello, Alice
|
||||
// Computing greeting...
|
||||
// Greeting changed to: Hello, Bob
|
38
docs/components/examples/signals/debugging-dom.js
Normal file
38
docs/components/examples/signals/debugging-dom.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { el, on, scope } from "deka-dom-el";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
// Create a component with reactive elements
|
||||
function ReactiveCounter() {
|
||||
const count = S(0);
|
||||
scope.host(on.connected(ev=>
|
||||
console.log(ev.target.__dde_reactive)
|
||||
));
|
||||
|
||||
const counter = el('div', {
|
||||
// This element will be added into the __dde_reactive property
|
||||
textContent: count,
|
||||
});
|
||||
|
||||
const incrementBtn = el('button', {
|
||||
textContent: 'Increment',
|
||||
onclick: () => count.set(count.get() + 1)
|
||||
});
|
||||
|
||||
// Dynamic section will be added into __dde_signal property
|
||||
const counterInfo = S.el(count, value =>
|
||||
el('p', `Current count is ${value}`)
|
||||
);
|
||||
|
||||
return el('div', { id: 'counter' }).append(
|
||||
counter,
|
||||
incrementBtn,
|
||||
counterInfo
|
||||
);
|
||||
}
|
||||
document.body.append(
|
||||
el(ReactiveCounter),
|
||||
);
|
||||
|
||||
// In DevTools console:
|
||||
const counter = document.querySelector('#counter');
|
||||
setTimeout(()=> console.log(counter.__dde_reactive), 1000); // See reactive bindings
|
13
docs/components/examples/signals/derived.js
Normal file
13
docs/components/examples/signals/derived.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { S } from "deka-dom-el/signals";
|
||||
|
||||
// Create base signals
|
||||
const firstName = S("John");
|
||||
const lastName = S("Doe");
|
||||
|
||||
// Create a derived signal
|
||||
const fullName = S(() => firstName.get() + " " + lastName.get());
|
||||
|
||||
// The fullName signal updates automatically when either dependency changes
|
||||
S.on(fullName, name => console.log("Name changed to:", name));
|
||||
|
||||
firstName.set("Jane"); // logs: "Name changed to: Jane Doe"
|
@ -3,8 +3,8 @@ const count= S(0);
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
document.body.append(
|
||||
el("p", S(()=> "Currently: "+count())),
|
||||
el("p", { classList: { red: S(()=> count()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }),
|
||||
el("p", S(()=> "Currently: "+count.get())),
|
||||
el("p", { classList: { red: S(()=> count.get()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }),
|
||||
);
|
||||
document.head.append(
|
||||
el("style", ".red { color: red; }")
|
||||
@ -12,4 +12,4 @@ document.head.append(
|
||||
|
||||
const interval= 5 * 1000;
|
||||
setTimeout(clearInterval, 10*interval,
|
||||
setInterval(()=> count(count()+1), interval));
|
||||
setInterval(()=> count.set(count.get()+1), interval));
|
@ -2,7 +2,7 @@ import { S } from "deka-dom-el/signals";
|
||||
const count= S(0, {
|
||||
add(){ this.value= this.value + Math.round(Math.random()*10); }
|
||||
});
|
||||
const numbers= S([ count() ], {
|
||||
const numbers= S([ count.get() ], {
|
||||
push(next){ this.value.push(next); }
|
||||
});
|
||||
|
||||
@ -22,5 +22,5 @@ document.body.append(
|
||||
const interval= 5*1000;
|
||||
setTimeout(clearInterval, 10*interval, setInterval(function(){
|
||||
S.action(count, "add");
|
||||
S.action(numbers, "push", count());
|
||||
}, interval));
|
||||
S.action(numbers, "push", count.get());
|
||||
}, interval));
|
@ -1,10 +1,10 @@
|
||||
import { S } from "deka-dom-el/signals";
|
||||
// α — `signal` represents a reactive value
|
||||
// PART 1 — `signal` represents a reactive value
|
||||
const signal= S(0);
|
||||
// β — just reacts on signal changes
|
||||
// PART 2 — just reacts on signal changes
|
||||
S.on(signal, console.log);
|
||||
// γ — just updates the value
|
||||
const update= ()=> signal(signal()+1);
|
||||
// PART 3 — just updates the value
|
||||
const update= ()=> signal.set(signal.get()+1);
|
||||
|
||||
update();
|
||||
const interval= 5*1000;
|
||||
|
43
docs/components/examples/ssr/async-data.js
Normal file
43
docs/components/examples/ssr/async-data.js
Normal file
@ -0,0 +1,43 @@
|
||||
// Handling async data in SSR
|
||||
import { JSDOM } from "jsdom";
|
||||
import { S } from "deka-dom-el/signals";
|
||||
import { register, queue } from "deka-dom-el/jsdom";
|
||||
|
||||
async function renderWithAsyncData() {
|
||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
|
||||
const { el } = await register(dom);
|
||||
|
||||
// Create a component that fetches data
|
||||
function AsyncComponent() {
|
||||
const title= S("-");
|
||||
const description= S("-");
|
||||
|
||||
// Use the queue to track the async operation
|
||||
queue(fetch("https://api.example.com/data")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
title.set(data.title);
|
||||
description.set(data.description);
|
||||
}));
|
||||
|
||||
return el("div", { className: "async-content" }).append(
|
||||
el("h2", title),
|
||||
el("p", description)
|
||||
);
|
||||
}
|
||||
|
||||
// Render the page
|
||||
dom.window.document.body.append(
|
||||
el("h1", "Page with Async Data"),
|
||||
el(AsyncComponent)
|
||||
);
|
||||
|
||||
// IMPORTANT: Wait for all queued operations to complete
|
||||
await queue();
|
||||
|
||||
// Now the HTML includes all async content
|
||||
const html = dom.serialize();
|
||||
console.log(html);
|
||||
}
|
||||
|
||||
renderWithAsyncData();
|
47
docs/components/examples/ssr/basic-example.js
Normal file
47
docs/components/examples/ssr/basic-example.js
Normal file
@ -0,0 +1,47 @@
|
||||
// Basic SSR Example
|
||||
import { JSDOM } from "jsdom";
|
||||
import { register, queue } from "deka-dom-el/jsdom";
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
async function renderPage() {
|
||||
// Create a jsdom instance
|
||||
const dom = new JSDOM("<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head><body></body></html>");
|
||||
|
||||
// Register with deka-dom-el and get the el function
|
||||
const { el } = await register(dom);
|
||||
|
||||
// Create a simple header component
|
||||
function Header({ title }) {
|
||||
return el("header").append(
|
||||
el("h1", title),
|
||||
el("nav").append(
|
||||
el("ul").append(
|
||||
el("li").append(el("a", { href: "/" }, "Home")),
|
||||
el("li").append(el("a", { href: "/about" }, "About")),
|
||||
el("li").append(el("a", { href: "/contact" }, "Contact"))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Create the page content
|
||||
dom.window.document.body.append(
|
||||
el(Header, { title: "My Static Site" }),
|
||||
el("main").append(
|
||||
el("h2", "Welcome!"),
|
||||
el("p", "This page was rendered with deka-dom-el on the server.")
|
||||
),
|
||||
el("footer", "© 2025 My Company")
|
||||
);
|
||||
|
||||
// Wait for any async operations
|
||||
await queue();
|
||||
|
||||
// Get the HTML and write it to a file
|
||||
const html = dom.serialize();
|
||||
writeFileSync("index.html", html);
|
||||
|
||||
console.log("Page rendered successfully!");
|
||||
}
|
||||
|
||||
renderPage().catch(console.error);
|
2
docs/components/examples/ssr/intro.js
Normal file
2
docs/components/examples/ssr/intro.js
Normal file
@ -0,0 +1,2 @@
|
||||
// use NPM or for example https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/esm-with-signals.js
|
||||
import { register, unregister, queue } from "deka-dom-el/jsdom";
|
35
docs/components/examples/ssr/pages.js
Normal file
35
docs/components/examples/ssr/pages.js
Normal file
@ -0,0 +1,35 @@
|
||||
// ❌ WRONG: Static imports are hoisted and will register before JSDOM is created
|
||||
import { register } from "deka-dom-el/jsdom";
|
||||
import { el } from "deka-dom-el";
|
||||
import { Header } from "./components/Header.js";
|
||||
|
||||
// ✅ CORRECT: Use dynamic imports to ensure proper initialization order
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
async function renderPage() {
|
||||
// 1. Create JSDOM instance first
|
||||
const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`);
|
||||
|
||||
// 2. Dynamically import jsdom module
|
||||
const { register, queue } = await import("deka-dom-el/jsdom");
|
||||
|
||||
// 3. Register and get el function
|
||||
const { el } = await register(dom);
|
||||
|
||||
// 4. Dynamically import page components
|
||||
const { Header } = await import("./components/Header.js");
|
||||
const { Content } = await import("./components/Content.js");
|
||||
|
||||
// 5. Render components
|
||||
const body = dom.window.document.body;
|
||||
el(body).append(
|
||||
el(Header, { title: "My Page" }),
|
||||
el(Content, { text: "This is server-rendered content" })
|
||||
);
|
||||
|
||||
// 6. Wait for async operations
|
||||
await queue();
|
||||
|
||||
// 7. Get HTML and clean up
|
||||
return dom.serialize();
|
||||
}
|
27
docs/components/examples/ssr/start.js
Normal file
27
docs/components/examples/ssr/start.js
Normal file
@ -0,0 +1,27 @@
|
||||
// Basic jsdom integration example
|
||||
import { JSDOM } from "jsdom";
|
||||
import { register, unregister, queue } from "deka-dom-el/jsdom.js";
|
||||
|
||||
// Create a jsdom instance
|
||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
|
||||
|
||||
// Register the dom with deka-dom-el
|
||||
const { el } = await register(dom);
|
||||
|
||||
// Use deka-dom-el normally
|
||||
dom.window.document.body.append(
|
||||
el("div", { className: "container" }).append(
|
||||
el("h1", "Hello, SSR World!"),
|
||||
el("p", "This content was rendered on the server.")
|
||||
)
|
||||
);
|
||||
|
||||
// Wait for any async operations to complete
|
||||
await queue();
|
||||
|
||||
// Get the rendered HTML
|
||||
const html = dom.serialize();
|
||||
console.log(html);
|
||||
|
||||
// Clean up when done
|
||||
unregister();
|
44
docs/components/examples/ssr/static-site-generator.js
Normal file
44
docs/components/examples/ssr/static-site-generator.js
Normal file
@ -0,0 +1,44 @@
|
||||
// Building a simple static site generator
|
||||
import { JSDOM } from "jsdom";
|
||||
import { register, queue } from "deka-dom-el/jsdom";
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
|
||||
async function buildSite() {
|
||||
// Define pages to build
|
||||
const pages = [
|
||||
{ id: "index", title: "Home", component: "./pages/home.js" },
|
||||
{ id: "about", title: "About", component: "./pages/about.js" },
|
||||
{ id: "docs", title: "Documentation", component: "./pages/docs.js" }
|
||||
];
|
||||
|
||||
// Create output directory
|
||||
mkdirSync("./dist", { recursive: true });
|
||||
|
||||
// Build each page
|
||||
for (const page of pages) {
|
||||
// Create a fresh jsdom instance for each page
|
||||
const dom = new JSDOM("<!DOCTYPE html><html><head><meta charset=\"utf-8\"></head><body></body></html>");
|
||||
|
||||
// Register with deka-dom-el
|
||||
const { el } = await register(dom);
|
||||
|
||||
// Import the page component
|
||||
const { default: PageComponent } = await import(page.component);
|
||||
|
||||
// Render the page with its metadata
|
||||
dom.window.document.body.append(
|
||||
el(PageComponent, { title: page.title, pages })
|
||||
);
|
||||
|
||||
// Wait for any async operations
|
||||
await queue();
|
||||
|
||||
// Write the HTML to a file
|
||||
const html = dom.serialize();
|
||||
writeFileSync(`./dist/${page.id}.html`, html);
|
||||
|
||||
console.log(`Built page: ${page.id}.html`);
|
||||
}
|
||||
}
|
||||
|
||||
buildSite().catch(console.error);
|
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: "later", 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));
|
||||
})
|
||||
});
|
||||
}
|
@ -17,8 +17,14 @@ export function mnemonic(){
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>[, <options>])(element, detail)"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail, ...<options> }))")
|
||||
el("code", "dispatchEvent(<event>, <element>)([<detail>])"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>))"), " or ",
|
||||
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))")
|
||||
),
|
||||
el("li").append(
|
||||
el("code", "dispatchEvent(<event>[, <options>])(<element>[, <detail>])"),
|
||||
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>] ))"), " or ",
|
||||
el("code", "<element>.dispatchEvent(new CustomEvent(<event>, { detail: <detail> }))")
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,18 +1,67 @@
|
||||
import { pages, styles } from "../ssr.js";
|
||||
const host= "."+prevNext.name;
|
||||
styles.css`
|
||||
${host}{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
margin-top: 1rem;
|
||||
/* Previous/Next navigation */
|
||||
${host} {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 3rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
${host} [rel=prev]{
|
||||
grid-column: 1;
|
||||
|
||||
${host} a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--primary-dark); /* Darker background for better contrast */
|
||||
color: white;
|
||||
font-weight: 600; /* Bolder text for better readability */
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||
max-width: 45%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); /* Subtle shadow for better visibility */
|
||||
}
|
||||
${host} [rel=next]{
|
||||
grid-column: 3;
|
||||
text-align: right;
|
||||
|
||||
${host} a:hover {
|
||||
background-color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
text-decoration: none;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Enhanced shadow on hover */
|
||||
}
|
||||
|
||||
${host} [rel=prev] {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
${host} [rel=next] {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
${host} [rel=prev]::before {
|
||||
content: "←";
|
||||
margin-right: 0.75rem;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
${host} [rel=next]::after {
|
||||
content: "→";
|
||||
margin-left: 0.75rem;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* If there's no previous/next, ensure the spacing still works */
|
||||
${host} a:only-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
${host} a {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
import { el } from "../../index.js";
|
||||
@ -22,21 +71,28 @@ import { el } from "../../index.js";
|
||||
* @param {string} [attrs.id]
|
||||
* */
|
||||
export function h3({ textContent, id }){
|
||||
if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, "");
|
||||
return el("h3", { id }).append(
|
||||
el("a", { textContent: "#", href: "#"+id, tabIndex: -1 }),
|
||||
" ", textContent
|
||||
);
|
||||
if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, "");
|
||||
return el("h3", { id }).append(
|
||||
el("a", {
|
||||
className: "heading-anchor",
|
||||
href: "#"+id,
|
||||
textContent: "#",
|
||||
title: `Link to this section: ${textContent}`,
|
||||
"aria-label": `Link to section ${textContent}`
|
||||
}),
|
||||
" ",
|
||||
textContent,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {import("../types.d.ts").Info} page
|
||||
* */
|
||||
export function prevNext(page){
|
||||
const page_index= pages.indexOf(page);
|
||||
return el("div", { className: prevNext.name }).append(
|
||||
el(pageLink, { rel: "prev", page: pages[page_index-1] }),
|
||||
el(pageLink, { rel: "next", page: pages[page_index+1] })
|
||||
);
|
||||
const page_index= pages.indexOf(page);
|
||||
return el("div", { className: prevNext.name }).append(
|
||||
el(pageLink, { rel: "prev", page: pages[page_index-1] }),
|
||||
el(pageLink, { rel: "next", page: pages[page_index+1] })
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {Object} attrs
|
||||
@ -44,11 +100,22 @@ export function prevNext(page){
|
||||
* @param {import("../types.d.ts").Info} [attrs.page]
|
||||
* */
|
||||
function pageLink({ rel, page }){
|
||||
if(!page) return el();
|
||||
let { href, title, description }= page;
|
||||
return el("a", { rel, href, title: description }).append(
|
||||
rel==="next" ?"(next) " : "",
|
||||
title,
|
||||
rel==="prev" ? " (previous)" : "",
|
||||
);
|
||||
if(!page) return el();
|
||||
let { href, title, description }= page;
|
||||
|
||||
// Find the page index to show numbering
|
||||
const pageIndex = pages.findIndex(p => p === page);
|
||||
const pageNumber = pageIndex + 1;
|
||||
|
||||
const linkTitle = rel === "prev"
|
||||
? `Previous: ${pageNumber}. ${title}`
|
||||
: `Next: ${pageNumber}. ${title}`;
|
||||
|
||||
return el("a", {
|
||||
rel,
|
||||
href,
|
||||
title: description || linkTitle
|
||||
}).append(
|
||||
title
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user