mirror of
				https://github.com/jaandrle/deka-dom-el
				synced 2025-11-04 07:09:15 +01:00 
			
		
		
		
	🔤
This commit is contained in:
		@@ -13,18 +13,20 @@ import { S } from "deka-dom-el/signals";
 | 
				
			|||||||
 * @returns {HTMLElement} The root TodoMVC application element
 | 
					 * @returns {HTMLElement} The root TodoMVC application element
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function Todos(){
 | 
					function Todos(){
 | 
				
			||||||
	const pageS = routerSignal(S);
 | 
						const { signal } = scope;
 | 
				
			||||||
 | 
						const pageS = routerSignal(S, signal);
 | 
				
			||||||
	const todosS = todosSignal();
 | 
						const todosS = todosSignal();
 | 
				
			||||||
	/** Derived signal that filters todos based on current route */
 | 
						/** Derived signal that filters todos based on current route */
 | 
				
			||||||
	const filteredTodosS = S(()=> {
 | 
						const todosFilteredS = S(()=> {
 | 
				
			||||||
		const todos = todosS.get();
 | 
							const todos = todosS.get();
 | 
				
			||||||
		const filter = pageS.get();
 | 
							const filter = pageS.get();
 | 
				
			||||||
 | 
							if (filter === "all") return todos;
 | 
				
			||||||
		return todos.filter(todo => {
 | 
							return todos.filter(todo => {
 | 
				
			||||||
			if (filter === "active") return !todo.completed;
 | 
								if (filter === "active") return !todo.completed;
 | 
				
			||||||
			if (filter === "completed") return todo.completed;
 | 
								if (filter === "completed") return todo.completed;
 | 
				
			||||||
			return true; // "all"
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
						const todosRemainingS = S(()=> todosS.get().filter(todo => !todo.completed).length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/** @type {ddeElementAddon<HTMLInputElement>} */
 | 
						/** @type {ddeElementAddon<HTMLInputElement>} */
 | 
				
			||||||
	const onToggleAll = on("change", event => {
 | 
						const onToggleAll = on("change", event => {
 | 
				
			||||||
@@ -73,7 +75,7 @@ function Todos(){
 | 
				
			|||||||
				}, onToggleAll),
 | 
									}, onToggleAll),
 | 
				
			||||||
				el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
 | 
									el("label", { htmlFor: "toggle-all", title: "Mark all as complete" }),
 | 
				
			||||||
				el("ul", { className: "todo-list" }).append(
 | 
									el("ul", { className: "todo-list" }).append(
 | 
				
			||||||
					S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
 | 
										S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
 | 
				
			||||||
						memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
 | 
											memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
 | 
				
			||||||
					)
 | 
										)
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
@@ -83,7 +85,7 @@ function Todos(){
 | 
				
			|||||||
			? el()
 | 
								? el()
 | 
				
			||||||
			: el("footer", { className: "footer" }).append(
 | 
								: el("footer", { className: "footer" }).append(
 | 
				
			||||||
				el("span", { className: "todo-count" }).append(
 | 
									el("span", { className: "todo-count" }).append(
 | 
				
			||||||
					noOfLeft(todos)
 | 
										noOfLeft()
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
				memo("filters", ()=>
 | 
									memo("filters", ()=>
 | 
				
			||||||
					el("ul", { className: "filters" }).append(
 | 
										el("ul", { className: "filters" }).append(
 | 
				
			||||||
@@ -98,15 +100,18 @@ function Todos(){
 | 
				
			|||||||
						)
 | 
											)
 | 
				
			||||||
					),
 | 
										),
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
				!todos.some(todo => todo.completed)
 | 
									todos.length - todosRemainingS.get() === 0
 | 
				
			||||||
					? el()
 | 
										? el()
 | 
				
			||||||
					: el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
 | 
										: memo("delete", () =>
 | 
				
			||||||
 | 
											el("button",
 | 
				
			||||||
 | 
												{ textContent: "Clear completed", className: "clear-completed" },
 | 
				
			||||||
 | 
												onClearCompleted)
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
	/** @param {Todo[]} todos */
 | 
						function noOfLeft(){
 | 
				
			||||||
	function noOfLeft(todos){
 | 
							const length = todosRemainingS.get();
 | 
				
			||||||
		const { length }= todos.filter(todo => !todo.completed);
 | 
					 | 
				
			||||||
		return el("strong").append(
 | 
							return el("strong").append(
 | 
				
			||||||
			length + " ",
 | 
								length + " ",
 | 
				
			||||||
			length === 1 ? "item left" : "items left"
 | 
								length === 1 ? "item left" : "items left"
 | 
				
			||||||
@@ -194,7 +199,7 @@ function TodoItem({ id, title, completed }) {
 | 
				
			|||||||
				checked: completed
 | 
									checked: completed
 | 
				
			||||||
			}, onToggleCompleted),
 | 
								}, onToggleCompleted),
 | 
				
			||||||
			el("label", { textContent: title }, onStartEdit),
 | 
								el("label", { textContent: title }, onStartEdit),
 | 
				
			||||||
			el("button", { className: "destroy" }, onDelete)
 | 
								el("button", { ariaLabel: "Delete todo", className: "destroy" }, onDelete)
 | 
				
			||||||
		),
 | 
							),
 | 
				
			||||||
		S.el(isEditing, editing => !editing
 | 
							S.el(isEditing, editing => !editing
 | 
				
			||||||
			? el()
 | 
								? el()
 | 
				
			||||||
@@ -328,6 +333,7 @@ function todosSignal(){
 | 
				
			|||||||
			localStorage.setItem(store_key, JSON.stringify(value));
 | 
								localStorage.setItem(store_key, JSON.stringify(value));
 | 
				
			||||||
		} catch (e) {
 | 
							} catch (e) {
 | 
				
			||||||
			console.error("Failed to save todos to localStorage", e);
 | 
								console.error("Failed to save todos to localStorage", e);
 | 
				
			||||||
 | 
								// Optionally, provide user feedback
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	return out;
 | 
						return out;
 | 
				
			||||||
@@ -336,9 +342,10 @@ function todosSignal(){
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Creates a signal for managing route state
 | 
					 * Creates a signal for managing route state
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param {typeof S} signal - The signal constructor
 | 
					 * @param {typeof S} signal - The signal constructor from a library
 | 
				
			||||||
 | 
					 * @param {AbortSignal} abortSignal
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function routerSignal(signal){
 | 
					function routerSignal(signal, abortSignal){
 | 
				
			||||||
	const initial = location.hash.replace("#", "") || "all";
 | 
						const initial = location.hash.replace("#", "") || "all";
 | 
				
			||||||
	const out = signal(initial, {
 | 
						const out = signal(initial, {
 | 
				
			||||||
		/**
 | 
							/**
 | 
				
			||||||
@@ -347,15 +354,16 @@ function routerSignal(signal){
 | 
				
			|||||||
		 */
 | 
							 */
 | 
				
			||||||
		set(hash){
 | 
							set(hash){
 | 
				
			||||||
			location.hash = hash;
 | 
								location.hash = hash;
 | 
				
			||||||
			this.value = hash;
 | 
								//this.value = hash;
 | 
				
			||||||
		}
 | 
							},
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Setup hash change listener
 | 
						// Setup hash change listener
 | 
				
			||||||
	window.addEventListener("hashchange", () => {
 | 
						window.addEventListener("hashchange", () => {
 | 
				
			||||||
		const hash = location.hash.replace("#", "") || "all";
 | 
							const hash = location.hash.replace("#", "") || "all";
 | 
				
			||||||
		S.action(out, "set", /** @type {"all"|"active"|"completed"} */(hash));
 | 
							//S.action(out, "set", /** @type {"all"|"active"|"completed"} */(hash));
 | 
				
			||||||
	});
 | 
							out.set(hash);
 | 
				
			||||||
 | 
						}, { signal: abortSignal });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return out;
 | 
						return out;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -101,21 +101,23 @@ export function page({ pkg, info }){
 | 
				
			|||||||
		`),
 | 
							`),
 | 
				
			||||||
		el(code, { content: `
 | 
							el(code, { content: `
 | 
				
			||||||
			// Signal for current route (all/active/completed)
 | 
								// Signal for current route (all/active/completed)
 | 
				
			||||||
			const pageS = routerSignal(S);
 | 
								const { signal } = scope;
 | 
				
			||||||
 | 
								const pageS = routerSignal(S, signal);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Signal for the todos collection with custom actions
 | 
								// Signal for the todos collection with custom actions
 | 
				
			||||||
			const todosS = todosSignal();
 | 
								const todosS = todosSignal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Derived signal that filters todos based on current route
 | 
								// Derived signal that filters todos based on current route
 | 
				
			||||||
			const filteredTodosS = S(()=> {
 | 
								const todosFilteredS = S(()=> {
 | 
				
			||||||
				const todos = todosS.get();
 | 
									const todos = todosS.get();
 | 
				
			||||||
				const filter = pageS.get();
 | 
									const filter = pageS.get();
 | 
				
			||||||
 | 
									if (filter === "all") return todos;
 | 
				
			||||||
				return todos.filter(todo => {
 | 
									return todos.filter(todo => {
 | 
				
			||||||
					if (filter === "active") return !todo.completed;
 | 
										if (filter === "active") return !todo.completed;
 | 
				
			||||||
					if (filter === "completed") return todo.completed;
 | 
										if (filter === "completed") return todo.completed;
 | 
				
			||||||
					return true; // "all"
 | 
					 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
								const todosRemainingS = S(()=> todosS.get().filter(todo => !todo.completed).length);
 | 
				
			||||||
		`, page_id }),
 | 
							`, page_id }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		el("p").append(T`
 | 
							el("p").append(T`
 | 
				
			||||||
@@ -200,6 +202,7 @@ export function page({ pkg, info }){
 | 
				
			|||||||
						localStorage.setItem(store_key, JSON.stringify(value));
 | 
											localStorage.setItem(store_key, JSON.stringify(value));
 | 
				
			||||||
					} catch (e) {
 | 
										} catch (e) {
 | 
				
			||||||
						console.error("Failed to save todos to localStorage", e);
 | 
											console.error("Failed to save todos to localStorage", e);
 | 
				
			||||||
 | 
											// Optionally, provide user feedback
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				return out;
 | 
									return out;
 | 
				
			||||||
@@ -222,19 +225,19 @@ export function page({ pkg, info }){
 | 
				
			|||||||
		el("h4", t`1. Derived Signals for Filtering`),
 | 
							el("h4", t`1. Derived Signals for Filtering`),
 | 
				
			||||||
		el(code, { content: `
 | 
							el(code, { content: `
 | 
				
			||||||
			/** Derived signal that filters todos based on current route */
 | 
								/** Derived signal that filters todos based on current route */
 | 
				
			||||||
			const filteredTodosS = S(()=> {
 | 
								const todosFilteredS = S(()=> {
 | 
				
			||||||
				const todos = todosS.get();
 | 
									const todos = todosS.get();
 | 
				
			||||||
				const filter = pageS.get();
 | 
									const filter = pageS.get();
 | 
				
			||||||
 | 
									if (filter === "all") return todos;
 | 
				
			||||||
				return todos.filter(todo => {
 | 
									return todos.filter(todo => {
 | 
				
			||||||
					if (filter === "active") return !todo.completed;
 | 
										if (filter === "active") return !todo.completed;
 | 
				
			||||||
					if (filter === "completed") return todo.completed;
 | 
										if (filter === "completed") return todo.completed;
 | 
				
			||||||
					return true; // "all"
 | 
					 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Using the derived signal in the UI
 | 
								// Using the derived signal in the UI
 | 
				
			||||||
			el("ul", { className: "todo-list" }).append(
 | 
								el("ul", { className: "todo-list" }).append(
 | 
				
			||||||
				S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
 | 
									S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
 | 
				
			||||||
					memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
 | 
										memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
@@ -334,7 +337,7 @@ export function page({ pkg, info }){
 | 
				
			|||||||
		el("h4", t`Memoizing Todo Items`),
 | 
							el("h4", t`Memoizing Todo Items`),
 | 
				
			||||||
		el(code, { content: `
 | 
							el(code, { content: `
 | 
				
			||||||
			el("ul", { className: "todo-list" }).append(
 | 
								el("ul", { className: "todo-list" }).append(
 | 
				
			||||||
				S.el(filteredTodosS, filteredTodos => filteredTodos.map(todo =>
 | 
									S.el(todosFilteredS, filteredTodos => filteredTodos.map(todo =>
 | 
				
			||||||
					memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
 | 
										memo(todo.id, ()=> el(TodoItem, todo, onDelete, onEdit)))
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
@@ -543,10 +546,12 @@ export function page({ pkg, info }){
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		el("h4", t`Conditional Clear Completed Button`),
 | 
							el("h4", t`Conditional Clear Completed Button`),
 | 
				
			||||||
		el(code, { content: `
 | 
							el(code, { content: `
 | 
				
			||||||
			S.el(S(() => todosS.get().some(todo => todo.completed)),
 | 
								todos.length - todosRemainingS.get() === 0
 | 
				
			||||||
				hasTodosCompleted=> hasTodosCompleted
 | 
									? el()
 | 
				
			||||||
				? el("button", { textContent: "Clear completed", className: "clear-completed" }, onClearCompleted)
 | 
									: memo("delete", () =>
 | 
				
			||||||
				: el()
 | 
										el("button",
 | 
				
			||||||
 | 
											{ textContent: "Clear completed", className: "clear-completed" },
 | 
				
			||||||
 | 
											onClearCompleted)
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
		`, page_id }),
 | 
							`, page_id }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -625,7 +630,7 @@ export function page({ pkg, info }){
 | 
				
			|||||||
				${el("strong", "Declarative Class Management:")} Using the classList property for cleaner class handling
 | 
									${el("strong", "Declarative Class Management:")} Using the classList property for cleaner class handling
 | 
				
			||||||
			`),
 | 
								`),
 | 
				
			||||||
			el("li").append(T`
 | 
								el("li").append(T`
 | 
				
			||||||
				${el("strong", "Focus Management:")} Reliable input focus with requestAnimationFrame
 | 
									${el("strong", "Focus Management:")} Reliable input focus with setTimeout
 | 
				
			||||||
			`),
 | 
								`),
 | 
				
			||||||
			el("li").append(T`
 | 
								el("li").append(T`
 | 
				
			||||||
				${el("strong", "Persistent Storage:")} Automatically saving application state with signal listeners
 | 
									${el("strong", "Persistent Storage:")} Automatically saving application state with signal listeners
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user