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

🔤 🐛 v0.9.1-alpha (#30)

* :tap: removed on.attributeChanged and static observedAttributes

*  import optimalization

*  scope.signal

* 🔤 🐛

*  🐛 registerReactivity and types

* 🔤

* 

* 🔤

* 🐛 Node in enviroment

*  todos

* 

*  🔤

*  lint

*  memo

* 🔤 🐛 memo

*  🔤 todomvc

* 🐛 types

* 🔤 p08 signal factory

* 🔤  types

*  🔤 lint

* 🔤

* 🔤

* 🔤

* 🔤

* 📺
This commit is contained in:
2025-03-12 18:37:42 +01:00
committed by GitHub
parent e1f321004d
commit 25d475ec04
83 changed files with 4899 additions and 2182 deletions

View File

@ -189,6 +189,7 @@ import { el } from "deka-dom-el";
* */
export function code({ id, src, content, language= "js", className= host.slice(1), page_id }){
if(src) content= s.cat(src);
content= normalizeIndentation(content);
let dataJS;
if(page_id){
registerClientPart(page_id);
@ -198,6 +199,10 @@ export function code({ id, src, content, language= "js", className= host.slice(1
el("code", { className: "language-"+language, textContent: content.trim() })
);
}
export function pre({ content }){
content= normalizeIndentation(content);
return el("pre").append(el("code", content.trim()));
}
let is_registered= {};
/** @param {string} page_id */
function registerClientPart(page_id){
@ -207,33 +212,6 @@ function registerClientPart(page_id){
document.head.append(
// 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(
@ -245,3 +223,9 @@ function registerClientPart(page_id){
is_registered[page_id]= true;
}
/** @param {string} src */
function normalizeIndentation(src){
const lines= src.split("\n");
const min_indent= Math.min(...lines.map(line=> line.search(/\S/)).filter(i=> i >= 0));
return lines.map(line=> line.slice(min_indent)).join("\n");
}

View File

@ -96,6 +96,18 @@ html[data-theme="light"] .cm-s-material .cm-error { color: #f44336 !important; }
max-width: 100% !important;
}
}
${host}[data-variant=big]{
height: 100vh;
main {
flex-flow: column nowrap;
flex-grow: 1;
}
main > * {
width: 100%;
max-width: 100% !important;
}
}
`;
const dde_content= s.cat(new URL("../../dist/esm-with-signals.js", import.meta.url)).toString();
@ -108,15 +120,16 @@ import { relative } from "node:path";
* @param {object} attrs
* @param {URL} attrs.src Example code file path
* @param {"js"|"ts"|"html"|"css"} [attrs.language="js"] Language of the code
* @param {"normal"|"big"} [attrs.variant="normal"] Size of the example
* @param {string} attrs.page_id ID of the page
* */
export function example({ src, language= "js", page_id }){
export function example({ src, language= "js", variant= "normal", page_id }){
registerClientPart(page_id);
const content= s.cat(src).toString()
.replaceAll(/ from "deka-dom-el(\/signals)?";/g, ' from "./esm-with-signals.js";');
const id= "code-example-"+generateCodeId(src);
return el().append(
el(code, { id, content, language, className: example.name }),
el(code, { id, content, language, className: example.name }, el=> el.dataset.variant= variant),
elCode({ id, content, extension: "."+language })
);
}

View File

@ -8,7 +8,8 @@ export class HTMLCustomElement extends HTMLElement{
connectedCallback(){
customElementRender(
this.attachShadow({ mode: "open" }),
ddeComponent
ddeComponent,
this
);
}
set attr(value){ this.setAttribute("attr", value); }

View File

@ -2,7 +2,6 @@
import {
customElementRender,
customElementWithDDE,
observedAttributes,
} from "deka-dom-el";
/** @type {ddePublicElementTagNameMap} */
import { S } from "deka-dom-el/signals";

View File

@ -9,7 +9,7 @@ export class HTMLCustomElement extends HTMLElement{
// nice place to render custom element
}
attributeChangedCallback(name, oldValue, newValue){
// listen to attribute changes (see `observedAttributes`)
// listen to attribute changes (see `S.observedAttributes`)
}
disconnectedCallback(){
// nice place to clean up

View File

@ -1,7 +1,6 @@
import {
customElementRender,
customElementWithDDE,
observedAttributes,
el, on, scope,
} from "deka-dom-el";
import { S } from "deka-dom-el/signals";
@ -9,7 +8,6 @@ export class HTMLCustomElement extends HTMLElement{
static tagName= "custom-element";
static observedAttributes= [ "attr" ];
connectedCallback(){
console.log(observedAttributes(this));
customElementRender(
this.attachShadow({ mode: "open" }),
ddeComponent,

View File

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

View File

@ -24,7 +24,11 @@ document.body.append(
);
import { chainableAppend } from "deka-dom-el";
/** @param {keyof HTMLElementTagNameMap} tag */
/**
* @template {keyof HTMLElementTagNameMap} TAG
* @param {TAG} tag
* @returns {ddeHTMLElementTagNameMap[TAG] extends HTMLElement ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement}
* */
const createElement= tag=> chainableAppend(document.createElement(tag));
document.body.append(
createElement("p").append(

View File

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

View File

@ -2,14 +2,14 @@
const div = document.createElement('div');
const h1 = document.createElement('h1');
h1.textContent = 'Title';
div.appendChild(h1);
div.append(h1);
const p = document.createElement('p');
p.textContent = 'Paragraph';
div.appendChild(p);
div.append(p);
// appendChild doesn't return parent
// append doesn't return parent
// so chaining is not possible
// Add to DOM
document.body.appendChild(div);
document.body.append(div);

View File

@ -0,0 +1,19 @@
import { el, on, dispatchEvent, scope } from "deka-dom-el";
document.body.append(
el(component),
);
function component(){
const { host }= scope;
const dispatchExample= dispatchEvent(
"example",
{ bubbles: true },
host
);
return el("div").append(
el("p", "Dispatch events from outside of the component."),
el("button", { textContent: "Dispatch", type: "button" },
on("click", dispatchExample))
);
}

View File

@ -1,9 +1,9 @@
import { el, on } from "deka-dom-el";
const paragraph= el("p", "See live-cycle events in console.",
const paragraph= el("p", "See lifecycle events in console.",
el=> log({ type: "dde:created", detail: el }),
on.connected(log),
on.disconnected(log),
on.attributeChanged(log));
);
document.body.append(
paragraph,

View File

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

View File

@ -1,4 +1,4 @@
import { el, on } from "deka-dom-el";
import { el } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
// A HelloWorld component using the 3PS pattern
@ -27,4 +27,4 @@ function HelloWorld({ emoji = "🚀" }) {
// Use the component in your app
document.body.append(
el(HelloWorld, { emoji: "🎉" })
);
);

View File

@ -15,15 +15,15 @@ function HelloWorldComponent({ initial }){
return el().append(
el("p", {
textContent: S(() => `Hello World ${emoji().repeat(clicks())}`),
textContent: S(() => `Hello World ${emoji.get().repeat(clicks.get())}`),
className: "example",
ariaLive: "polite", //OR ariaset: { live: "polite" },
dataset: { example: "Example" }, //OR dataExample: "Example",
}),
el("button",
{ textContent: "Fire", type: "button" },
on("click", ()=> clicks(clicks() + 1)),
on("keyup", ()=> clicks(clicks() - 2)),
on("click", ()=> clicks.set(clicks.get() + 1)),
on("keyup", ()=> clicks.set(clicks.get() - 2)),
),
el("select", null, onChange).append(
el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" }

View File

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

View File

@ -0,0 +1,115 @@
// Example of how memoization improves performance with list rendering
import { el, on, memo } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
// A utility to log element creation
function logCreation(name) {
console.log(`Creating ${name} element`);
return name;
}
// Create a signal with our items
const itemsSignal = S([
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" }
], {
add() {
const { length }= this.value;
this.value.push({
id: length + 1,
name: `Item ${length + 1}`
});
},
force(){},
});
// Without memoization - creates new elements on every render
function withoutMemo() {
return el("div").append(
el("h3", "Without Memoization (check console for element creation)"),
el("p", "Elements are recreated on every render"),
S.el(itemsSignal, items =>
el("ul").append(
...items.map(item =>
el("li").append(
el("span", logCreation(item.name))
)
)
)
),
);
}
// With memoization - reuses elements when possible
function withMemo() {
return el("div").append(
el("h3", "With Memoization (check console for element creation)"),
el("p", "Elements are reused when the key (item.id) stays the same"),
S.el(itemsSignal, items =>
el("ul").append(
...items.map(item =>
// Use item.id as a stable key for memoization
memo(item.id, () =>
el("li").append(
el("span", logCreation(item.name))
)
)
)
)
),
);
}
// Using memo.scope for a custom memoized function
const renderMemoList = memo.scope(function(items) {
return el("ul").append(
...items.map(item =>
memo(item.id, () =>
el("li").append(
el("span", logCreation(`Custom memo: ${item.name}`))
)
)
)
);
});
function withCustomMemo() {
return el("div").append(
el("h3", "With Custom Memo Function"),
el("p", "Using memo.scope to create a memoized rendering function"),
S.el(itemsSignal, items =>
renderMemoList(items)
),
el("button", "Clear Cache",
on("click", () => {
renderMemoList.clear();
S.action(itemsSignal, "force");
}
)
)
);
}
// Demo component showing the difference
export function MemoDemo() {
return el("div", { style: "padding: 1em; border: 1px solid #ccc;" }).append(
el("h2", "Memoization Demo"),
el("p", "See in the console when elements are created."),
el("p").append(`
Notice that without memoization, elements are recreated on every render. With memoization,
only new elements are created.
`),
el("button", "Add Item",
on("click", () => S.action(itemsSignal, "add"))
),
el("div", { style: "display: flex; gap: 2em; margin-top: 1em;" }).append(
withoutMemo(),
withMemo(),
withCustomMemo()
)
);
}
document.body.append(el(MemoDemo));

View File

@ -0,0 +1,386 @@
import { dispatchEvent, el, memo, on, scope } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
/**
* Main TodoMVC application component
*
* Creates and manages the TodoMVC application with the following features:
* - Todo items management (add, edit, delete)
* - Filtering by status (all, active, completed)
* - Client-side routing via URL hash
* - Persistent storage with localStorage
*
* @returns {HTMLElement} The root TodoMVC application element
*/
function Todos(){
const pageS = routerSignal(S);
const todosS = todosSignal();
/** Derived signal that filters todos based on current route */
const filteredTodosS = S(()=> {
const todos = todosS.get();
const filter = pageS.get();
return todos.filter(todo => {
if (filter === "active") return !todo.completed;
if (filter === "completed") return todo.completed;
return true; // "all"
});
});
// Setup hash change listener
window.addEventListener("hashchange", () => {
const hash = location.hash.replace("#", "") || "all";
S.action(pageS, "set", /** @type {"all"|"active"|"completed"} */(hash));
});
/** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleAll = on("change", event => {
const checked = /** @type {HTMLInputElement} */ (event.target).checked;
S.action(todosS, "completeAll", checked);
});
/** @type {ddeElementAddon<HTMLFormElement>} */
const onSubmitNewTodo = on("submit", event => {
event.preventDefault();
const input = /** @type {HTMLInputElement} */(
/** @type {HTMLFormElement} */(event.target).elements.namedItem("newTodo")
);
const title = input.value.trim();
if (title) {
S.action(todosS, "add", title);
input.value = "";
}
});
const onClearCompleted = on("click", () => S.action(todosS, "clearCompleted"));
const onDelete = on("todo:delete", ev =>
S.action(todosS, "delete", /** @type {{ detail: Todo["id"] }} */(ev).detail));
const onEdit = on("todo:edit", ev =>
S.action(todosS, "edit", /** @type {{ detail: Partial<Todo> & { id: Todo["id"] } }} */(ev).detail));
return el("section", { className: "todoapp" }).append(
el("header", { className: "header" }).append(
el("h1", "todos"),
el("form", null, onSubmitNewTodo).append(
el("input", {
className: "new-todo",
name: "newTodo",
placeholder: "What needs to be done?",
autocomplete: "off",
autofocus: true
})
)
),
S.el(todosS, todos => todos.length
? el("main", { className: "main" }).append(
el("input", {
id: "toggle-all",
className: "toggle-all",
type: "checkbox"
}, onToggleAll),
el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
el("ul", { className: "todo-list" }).append(
S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
)
)
)
: el()
),
S.el(todosS, todos => memo(todos.length, length=> length
? el("footer", { className: "footer" }).append(
el("span", { className: "todo-count" }).append(
S.el(S(() => todosS.get().filter(todo => !todo.completed).length),
length=> el("strong").append(
length + " ",
length === 1 ? "item left" : "items left"
)
)
),
el("ul", { className: "filters" }).append(
el("li").append(
el("a", {
textContent: "All",
className: S(()=> pageS.get() === "all" ? "selected" : ""),
href: "#"
}),
),
el("li").append(
el("a", {
textContent: "Active",
className: S(()=> pageS.get() === "active" ? "selected" : ""),
href: "#active"
}),
),
el("li").append(
el("a", {
textContent: "Completed",
className: S(()=> pageS.get() === "completed" ? "selected" : ""),
href: "#completed"
}),
)
),
S.el(S(() => todosS.get().some(todo => todo.completed)),
hasTodosCompleted=> hasTodosCompleted
? el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
: el()
)
)
: el()
))
);
}
/**
* Todo item data structure
* @typedef {{ title: string, id: string, completed: boolean }} Todo
*/
/**
* Component for rendering an individual todo item
*
* Features:
* - Display todo with completed state
* - Toggle completion status
* - Delete todo
* - Edit todo with double-click
* - Cancel edit with Escape key
*
* @param {Todo} todo - The todo item data
* @fires {void} todo:delete - todo deletion event
* @fires {Partial<Todo>} todo:edit - todo edits event
*/
function TodoItem({ id, title, completed }) {
const { host }= scope;
const isEditing = S(false);
const isCompleted = S(completed);
/** @type {(id: string) => void} Dispatch function for deleting todo */
const dispatchDelete= dispatchEvent("todo:delete", host);
/** @type {(data: {id: string, [key: string]: any}) => void} Dispatch function for editing todo */
const dispatchEdit = dispatchEvent("todo:edit", host);
/** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleCompleted = on("change", (ev) => {
const completed= /** @type {HTMLInputElement} */(ev.target).checked;
isCompleted.set(completed);
dispatchEdit({ id, completed });
});
/** @type {ddeElementAddon<HTMLButtonElement>} */
const onDelete = on("click", () => dispatchDelete(id));
/** @type {ddeElementAddon<HTMLLabelElement>} */
const onStartEdit = on("dblclick", () => isEditing.set(true));
/** @type {ddeElementAddon<HTMLInputElement>} */
const onBlurEdit = on("blur", event => {
const value = /** @type {HTMLInputElement} */(event.target).value.trim();
if (value) {
dispatchEdit({ id, title: value });
} else {
dispatchDelete(id);
}
isEditing.set(false);
});
/** @type {ddeElementAddon<HTMLFormElement>} */
const onSubmitEdit = on("submit", event => {
event.preventDefault();
const input = /** @type {HTMLFormElement} */(event.target).elements.namedItem("edit");
const value = /** @type {HTMLInputElement} */(input).value.trim();
if (value) {
dispatchEdit({ id, title: value });
} else {
dispatchDelete(id);
}
isEditing.set(false);
});
/**
* Event handler for keyboard events in edit mode
* @type {ddeElementAddon<HTMLInputElement>}
*/
const onKeyDown = on("keydown", event => {
if (event.key !== "Escape") return;
isEditing.set(false);
});
return el("li", { classList: { completed: isCompleted, editing: isEditing } }).append(
el("div", { className: "view" }).append(
el("input", {
className: "toggle",
type: "checkbox",
checked: completed
}, onToggleCompleted),
el("label", { textContent: title }, onStartEdit),
el("button", { className: "destroy" }, onDelete)
),
S.el(isEditing, editing => editing
? el("form", null, onSubmitEdit).append(
el("input", {
className: "edit",
name: "edit",
value: title,
"data-id": id
}, onBlurEdit, onKeyDown, addFocus)
)
: el()
)
);
}
// Set up the document head
document.head.append(
el("title", "TodoMVC: dd<el>"),
el("meta", { name: "description", content: "A TodoMVC implementation using dd<el>." }),
el("link", {
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css"
}),
el("link", {
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/todomvc-app-css@2.4.2/index.css"
})
);
// Set up the document body
document.body.append(
el(Todos),
el("footer", { className: "info" }).append(
el("p", "Double-click to edit a todo"),
el("p").append(
"Created with ",
el("a", { textContent: "deka-dom-el", href: "https://github.com/jaandrle/deka-dom-el" })
),
el("p").append(
"Part of ",
el("a", { textContent: "TodoMVC", href: "http://todomvc.com" })
)
)
);
/**
* Utility function to set focus on an input element
* Uses requestAnimationFrame to ensure the element is rendered
* before trying to focus it
*
* @param {HTMLInputElement} editInput - The input element to focus
* @returns {number} The requestAnimationFrame ID
*/
function addFocus(editInput){
return requestAnimationFrame(()=> {
editInput.focus();
editInput.selectionStart = editInput.selectionEnd = editInput.value.length;
});
}
/**
* Creates a signal for managing todos with persistence
*
* Features:
* - Loads todos from localStorage on initialization
* - Automatically saves todos to localStorage on changes
* - Provides actions for adding, editing, deleting todos
*/
function todosSignal(){
const store_key = "dde-todos";
// Try to load todos from localStorage
let savedTodos = [];
try {
const stored = localStorage.getItem(store_key);
if (stored) {
savedTodos = JSON.parse(stored);
}
} catch (_) {}
const out= S(/** @type {Todo[]} */(savedTodos || []), {
/**
* Add a new todo
* @param {string} value - The title of the new todo
*/
add(value){
this.value.push({
completed: false,
title: value,
id: uuid(),
});
},
/**
* Edit an existing todo
* @param {{ id: string, [key: string]: any }} data - Object containing id and fields to update
*/
edit({ id, ...update }){
const index = this.value.findIndex(t => t.id === id);
if (index === -1) return this.stopPropagation();
Object.assign(this.value[index], update);
},
/**
* Delete a todo by id
* @param {string} id - The id of the todo to delete
*/
delete(id){
const index = this.value.findIndex(t => t.id === id);
if (index === -1) return this.stopPropagation();
this.value.splice(index, 1);
},
/**
* Remove all completed todos
*/
clearCompleted() {
this.value = this.value.filter(todo => !todo.completed);
},
completeAll(state= true) {
this.value.forEach(todo => todo.completed = state);
},
/**
* Handle cleanup when signal is cleared
*/
[S.symbols.onclear](){
}
});
/**
* Save todos to localStorage whenever the signal changes
* @param {Todo[]} value - Current todos array
*/
S.on(out, /** @param {Todo[]} value */ function saveTodos(value) {
try {
localStorage.setItem(store_key, JSON.stringify(value));
} catch (e) {
console.error("Failed to save todos to localStorage", e);
}
});
return out;
}
/**
* Creates a signal for managing route state
*
* @param {typeof S} signal - The signal constructor
*/
function routerSignal(signal){
const initial = location.hash.replace("#", "") || "all";
return signal(initial, {
/**
* Set the current route
* @param {"all"|"active"|"completed"} hash - The route to set
*/
set(hash){
location.hash = hash;
this.value = hash;
}
});
}
/**
* Generates a RFC4122 version 4 compliant UUID
* Used to create unique identifiers for todo items
*
* @returns {string} A randomly generated UUID
*/
function uuid() {
let uuid = "";
for (let i = 0; i < 32; i++) {
let random = (Math.random() * 16) | 0;
if (i === 8 || i === 12 || i === 16 || i === 20)
uuid += "-";
uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16);
}
return uuid;
}

View File

@ -14,7 +14,7 @@ function component(){
const textContent= S("Click to change text.");
const onclickChange= on("click", function redispatch(){
textContent("Text changed! "+(new Date()).toString())
textContent.set("Text changed! "+(new Date()).toString())
});
return el("p", textContent, onclickChange);
}

View File

@ -1,14 +0,0 @@
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();
}

View File

@ -16,7 +16,9 @@ function Counter() {
// THE HOST IS PROBABLY DIFFERENT THAN
// YOU EXPECT AND SIGNAL MAY BE
// UNEXPECTEDLY REMOVED!!!
host().querySelector("button").disabled = count.get() >= 10;
S.on(count, (count)=>
host().querySelector("button").disabled = count >= 10
);
};
setTimeout(()=> {
// ok, BUT consider extract to separate function

View File

@ -1,6 +1,5 @@
// 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() {
@ -8,23 +7,7 @@ async function renderWithAsyncData() {
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)
);
}
const { AsyncComponent } = await import("./components/AsyncComponent.js");
// Render the page
dom.window.document.body.append(
@ -41,3 +24,24 @@ async function renderWithAsyncData() {
}
renderWithAsyncData();
// file: components/AsyncComponent.js
import { el } from "deka-dom-el";
import { S } from "deka-dom-el/signals";
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)
);
}

View File

@ -11,6 +11,7 @@ async function renderPage() {
const { el } = await register(dom);
// Create a simple header component
// can be separated into a separate file and use `import { el } from "deka-dom-el"`
function Header({ title }) {
return el("header").append(
el("h1", title),

View File

@ -17,6 +17,7 @@ async function renderPage() {
const { el } = await register(dom);
// 4. Dynamically import page components
// use `import { el } from "deka-dom-el"`
const { Header } = await import("./components/Header.js");
const { Content } = await import("./components/Content.js");

View File

@ -1,6 +1,6 @@
// Basic jsdom integration example
import { JSDOM } from "jsdom";
import { register, unregister, queue } from "deka-dom-el/jsdom.js";
import { register, unregister, queue } from "deka-dom-el/jsdom";
// Create a jsdom instance
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");

View File

@ -12,7 +12,7 @@ async function buildSite() {
];
// Create output directory
mkdirSync("./dist", { recursive: true });
mkdirSync("./dist/docs", { recursive: true });
// Build each page
for (const page of pages) {
@ -23,6 +23,7 @@ async function buildSite() {
const { el } = await register(dom);
// Import the page component
// use `import { el } from "deka-dom-el"`
const { default: PageComponent } = await import(page.component);
// Render the page with its metadata
@ -35,7 +36,7 @@ async function buildSite() {
// Write the HTML to a file
const html = dom.serialize();
writeFileSync(`./dist/${page.id}.html`, html);
writeFileSync(`./dist/docs/${page.id}.html`, html);
console.log(`Built page: ${page.id}.html`);
}

View File

@ -11,10 +11,6 @@ export function mnemonic(){
el("code", "customElementWithDDE(<custom-element>)"),
" — register <custom-element> to DDE library, see also `lifecyclesToEvents`, can be also used as decorator",
),
el("li").append(
el("code", "observedAttributes(<custom-element>)"),
" — returns record of observed attributes (keys uses camelCase)",
),
el("li").append(
el("code", "S.observedAttributes(<custom-element>)"),
" — returns record of observed attributes (keys uses camelCase and values are signals)",
@ -32,4 +28,4 @@ export function mnemonic(){
" — simulate slots for “dde”/functional components",
),
);
}
}

View File

@ -16,15 +16,16 @@ export function mnemonic(){
el("code", "dispatchEvent(<event>[, <options>])(element)"),
" — just ", el("code", "<element>.dispatchEvent(new Event(<event>[, <options>]))")
),
el("li").append(
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> }))")
),
el("li").append(
el("code", "dispatchEvent(<event>[, <options>], <host>)([<detail>])"),
" — just ", el("code", "<host>().dispatchEvent(new Event(<event>[, <options>]))"), " or ",
el("code", "<host>().dispatchEvent(new CustomEvent(<event>, { detail: <detail> }[, <options>] ))"),
" (see scopes section of docs)"
),
);
}

View File

@ -0,0 +1,15 @@
import { el } from "deka-dom-el";
import { mnemonicUl } from "../mnemonicUl.html.js";
export function mnemonic(){
return mnemonicUl().append(
el("li").append(
el("code", "memo.scope(<function>, <argument(s)>)"),
" — Scope for memo",
),
el("li").append(
el("code", "memo(<key>, <generator>)"),
" — returns value from memo and/or generates it (and caches it)",
),
);
}

View File

@ -14,6 +14,10 @@ export function mnemonic(){
el("li").append(
el("code", "scope.host(...<addons>)"),
" — use addons to current component",
),
el("li").append(
el("code", "scope.signal"),
" — get AbortSignal that triggers when the element disconnects",
)
);
}

View File

@ -0,0 +1,39 @@
import { styles } from "../ssr.js";
styles.css`
/* Scroll to top button */
.scroll-top-button {
position: fixed;
bottom: 2rem;
left: 2rem;
display: flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: var(--primary);
color: var(--button-text);
font-size: 1.5rem;
text-decoration: none;
box-shadow: var(--shadow);
transition: background-color 0.2s ease, transform 0.2s ease;
z-index: 1000;
}
.scroll-top-button:hover {
background-color: var(--primary-dark);
transform: translateY(-4px);
text-decoration: none;
}
@media (max-width: 768px) {
.scroll-top-button {
bottom: 0.5rem;
left: unset;
right: .5rem;
width: 2.5rem;
height: 2.5rem;
}
}
`;

View File

@ -0,0 +1,14 @@
import { el } from "deka-dom-el";
export function scrollTop() {
return el("a", {
href: "#",
className: "scroll-top-button",
ariaLabel: "Scroll to top",
textContent: "↑",
onclick: (e) => {
e.preventDefault();
window.scrollTo({ top: 0, behavior: "smooth" });
}
})
}