1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-04-11 23:25:34 +02:00
This commit is contained in:
Jan Andrle 2025-03-13 16:07:16 +01:00
parent 8756dabc55
commit 9251e70015
Signed by: jaandrle
GPG Key ID: B3A25AED155AFFAB
2 changed files with 85 additions and 60 deletions

View File

@ -26,28 +26,23 @@ function Todos(){
}); });
}); });
// 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>} */ /** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleAll = on("change", event => { const onToggleAll = on("change", event => {
const checked = /** @type {HTMLInputElement} */ (event.target).checked; const checked = /** @type {HTMLInputElement} */ (event.target).checked;
S.action(todosS, "completeAll", checked); S.action(todosS, "completeAll", checked);
}); });
const formNewTodo = "newTodo";
/** @type {ddeElementAddon<HTMLFormElement>} */ /** @type {ddeElementAddon<HTMLFormElement>} */
const onSubmitNewTodo = on("submit", event => { const onSubmitNewTodo = on("submit", event => {
event.preventDefault(); event.preventDefault();
const input = /** @type {HTMLInputElement} */( const input = /** @type {HTMLInputElement} */(
/** @type {HTMLFormElement} */(event.target).elements.namedItem("newTodo") /** @type {HTMLFormElement} */(event.target).elements.namedItem(formNewTodo)
); );
const title = input.value.trim(); const title = input.value.trim();
if (title) { if (!title) return;
S.action(todosS, "add", title); S.action(todosS, "add", title);
input.value = ""; input.value = "";
}
}); });
const onClearCompleted = on("click", () => S.action(todosS, "clearCompleted")); const onClearCompleted = on("click", () => S.action(todosS, "clearCompleted"));
const onDelete = on("todo:delete", ev => const onDelete = on("todo:delete", ev =>
@ -61,15 +56,16 @@ function Todos(){
el("form", null, onSubmitNewTodo).append( el("form", null, onSubmitNewTodo).append(
el("input", { el("input", {
className: "new-todo", className: "new-todo",
name: "newTodo", name: formNewTodo,
placeholder: "What needs to be done?", placeholder: "What needs to be done?",
autocomplete: "off", autocomplete: "off",
autofocus: true autofocus: true
}) })
) )
), ),
S.el(todosS, todos => todos.length S.el(todosS, todos => !todos.length
? el("main", { className: "main" }).append( ? el()
: el("main", { className: "main" }).append(
el("input", { el("input", {
id: "toggle-all", id: "toggle-all",
className: "toggle-all", className: "toggle-all",
@ -82,50 +78,40 @@ function Todos(){
) )
) )
) )
: el()
), ),
S.el(todosS, todos => memo(todos.length, length=> length S.el(todosS, todos => !todos.length
? el("footer", { className: "footer" }).append( ? el()
: el("footer", { className: "footer" }).append(
el("span", { className: "todo-count" }).append( el("span", { className: "todo-count" }).append(
S.el(S(() => todosS.get().filter(todo => !todo.completed).length), noOfLeft(todos)
length=> el("strong").append( ),
memo("filters", ()=>
el("ul", { className: "filters" }).append(
...[ "All", "Active", "Completed" ].map(textContent =>
el("li").append(
el("a", {
textContent,
classList: { selected: S(()=> pageS.get() === textContent.toLowerCase()) },
href: `#${textContent.toLowerCase()}`
})
)
)
),
),
!todos.some(todo => todo.completed)
? el()
: el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
)
)
);
/** @param {Todo[]} todos */
function noOfLeft(todos){
const { length }= todos.filter(todo => !todo.completed);
return el("strong").append(
length + " ", length + " ",
length === 1 ? "item left" : "items left" 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()
))
);
} }
/** /**
@ -177,10 +163,11 @@ function TodoItem({ id, title, completed }) {
} }
isEditing.set(false); isEditing.set(false);
}); });
const formEdit = "edit";
/** @type {ddeElementAddon<HTMLFormElement>} */ /** @type {ddeElementAddon<HTMLFormElement>} */
const onSubmitEdit = on("submit", event => { const onSubmitEdit = on("submit", event => {
event.preventDefault(); event.preventDefault();
const input = /** @type {HTMLFormElement} */(event.target).elements.namedItem("edit"); const input = /** @type {HTMLFormElement} */(event.target).elements.namedItem(formEdit);
const value = /** @type {HTMLInputElement} */(input).value.trim(); const value = /** @type {HTMLInputElement} */(input).value.trim();
if (value) { if (value) {
dispatchEdit({ id, title: value }); dispatchEdit({ id, title: value });
@ -209,16 +196,15 @@ function TodoItem({ id, title, completed }) {
el("label", { textContent: title }, onStartEdit), el("label", { textContent: title }, onStartEdit),
el("button", { className: "destroy" }, onDelete) el("button", { className: "destroy" }, onDelete)
), ),
S.el(isEditing, editing => editing S.el(isEditing, editing => !editing
? el("form", null, onSubmitEdit).append( ? el()
: el("form", null, onSubmitEdit).append(
el("input", { el("input", {
className: "edit", className: "edit",
name: "edit", name: formEdit,
value: title, value: title,
"data-id": id
}, onBlurEdit, onKeyDown, addFocus) }, onBlurEdit, onKeyDown, addFocus)
) )
: el()
) )
); );
} }
@ -354,7 +340,7 @@ function todosSignal(){
*/ */
function routerSignal(signal){ function routerSignal(signal){
const initial = location.hash.replace("#", "") || "all"; const initial = location.hash.replace("#", "") || "all";
return signal(initial, { const out = signal(initial, {
/** /**
* Set the current route * Set the current route
* @param {"all"|"active"|"completed"} hash - The route to set * @param {"all"|"active"|"completed"} hash - The route to set
@ -364,6 +350,14 @@ function routerSignal(signal){
this.value = hash; this.value = hash;
} }
}); });
// Setup hash change listener
window.addEventListener("hashchange", () => {
const hash = location.hash.replace("#", "") || "all";
S.action(out, "set", /** @type {"all"|"active"|"completed"} */(hash));
});
return out;
} }
/** /**

View File

@ -177,6 +177,13 @@ export function page({ pkg, info }){
clearCompleted() { clearCompleted() {
this.value = this.value.filter(todo => !todo.completed); this.value = this.value.filter(todo => !todo.completed);
}, },
/**
* Mark all todos as completed or active
* @param {boolean} state - Whether to mark todos as completed or active
*/
completeAll(state = true) {
this.value.forEach(todo => todo.completed = state);
},
/** /**
* Handle cleanup when signal is cleared * Handle cleanup when signal is cleared
*/ */
@ -238,7 +245,31 @@ export function page({ pkg, info }){
ensuring the UI always shows the correct filtered todos. ensuring the UI always shows the correct filtered todos.
`), `),
el("h4", t`2. Local Component State`), el("h4", t`2. Toggle All Functionality`),
el(code, { content: `
/** @type {ddeElementAddon<HTMLInputElement>} */
const onToggleAll = on("change", event => {
const checked = /** @type {HTMLInputElement} */ (event.target).checked;
S.action(todosS, "completeAll", checked);
});
// Using the toggle-all functionality in the UI
el("input", {
id: "toggle-all",
className: "toggle-all",
type: "checkbox"
}, onToggleAll),
el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
`, page_id }),
el("p").append(T`
The "toggle all" checkbox allows users to mark all todos as completed or active. When the checkbox
is toggled, it calls the ${el("code", "completeAll")} action on the todos signal, passing the current
checked state. This is a good example of how signals and actions can be used to manage application
state in a clean, declarative way.
`),
el("h4", t`3. Local Component State`),
el(code, { content: ` el(code, { content: `
function TodoItem({ id, title, completed }) { function TodoItem({ id, title, completed }) {
const { host }= scope; const { host }= scope;
@ -273,7 +304,7 @@ export function page({ pkg, info }){
UI feedback while still communicating changes to the parent via events. UI feedback while still communicating changes to the parent via events.
`), `),
el("h4", t`3. Reactive Properties`), el("h4", t`4. Reactive Properties`),
el(code, { content: ` el(code, { content: `
// Dynamic class attributes // Dynamic class attributes
el("a", { el("a", {