Solution: Todo List + Dark Mode Toggle
Try the Complete App
Section titled “Try the Complete App”ลองเล่น Todo List ที่สมบูรณ์แล้ว — ลองเพิ่ม, ลบ, mark complete, และสลับ Dark Mode ดู
Final Project Structure
Section titled “Final Project Structure”Directorytodo-app/
- index.html โครงสร้าง HTML + link CSS/JS
- style.css ตกแต่ง style + dark mode theme
- script.js State, Render, Events, localStorage
Solution Code
Section titled “Solution Code”Show index.html
<!DOCTYPE html><html lang="th"><head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Todo App</title> <link rel="stylesheet" href="style.css" /></head><body> <div class="app"> <header class="header"> <h1>Todo List</h1> <button id="theme-toggle" class="theme-btn" aria-label="Toggle dark mode"> <span class="icon-sun">☀</span> <span class="icon-moon">☾</span> </button> </header>
<form id="todo-form" class="todo-form"> <input type="text" id="todo-input" class="todo-input" placeholder="What needs to be done?" autocomplete="off" /> <button type="submit" class="add-btn">Add</button> </form>
<ul id="todo-list" class="todo-list"></ul>
<footer class="footer"> <span id="todo-count">0 items left</span> </footer> </div>
<script src="script.js"></script></body></html>Show style.css
/* === Reset & Base === */* { margin: 0; padding: 0; box-sizing: border-box;}
/* === CSS Variables (Light Theme) === */body { --bg: #f5f5f5; --text: #1a1a2e; --card-bg: #ffffff; --input-bg: #ffffff; --input-border: #d4d4d8; --input-focus: #8b5cf6; --btn-bg: #8b5cf6; --btn-hover: #7c3aed; --btn-text: #ffffff; --item-border: #e4e4e7; --item-hover: #f9fafb; --completed-text: #a1a1aa; --delete-color: #ef4444; --footer-text: #71717a; --shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
font-family: "Inter", "Noto Sans Thai", system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; display: flex; align-items: flex-start; justify-content: center; padding: 2rem 1rem; transition: background 0.3s ease, color 0.3s ease;}
/* === Dark Theme === */body.dark { --bg: #0f0f1a; --text: #e4e4e7; --card-bg: #1a1a2e; --input-bg: #222240; --input-border: #3f3f5c; --input-focus: #a78bfa; --btn-bg: #8b5cf6; --btn-hover: #a78bfa; --btn-text: #ffffff; --item-border: #2e2e4a; --item-hover: #222240; --completed-text: #52525b; --delete-color: #f87171; --footer-text: #71717a; --shadow: 0 1px 3px rgba(0, 0, 0, 0.3);}
/* === App Container === */.app { width: 100%; max-width: 520px; background: var(--card-bg); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; transition: background 0.3s ease, box-shadow 0.3s ease;}
/* === Header === */.header { display: flex; align-items: center; justify-content: space-between; padding: 1.5rem 1.5rem 1rem;}
.header h1 { font-size: 1.5rem; font-weight: 700;}
.theme-btn { background: none; border: 2px solid var(--input-border); border-radius: 8px; padding: 0.4rem 0.6rem; cursor: pointer; font-size: 1.1rem; color: var(--text); transition: border-color 0.2s, background 0.2s; line-height: 1;}
.theme-btn:hover { border-color: var(--input-focus); background: var(--item-hover);}
/* Sun/Moon icon toggling */body .icon-moon { display: none;}body .icon-sun { display: inline;}body.dark .icon-moon { display: inline;}body.dark .icon-sun { display: none;}
/* === Form === */.todo-form { display: flex; gap: 0.5rem; padding: 0 1.5rem 1rem;}
.todo-input { flex: 1; padding: 0.65rem 0.9rem; border: 2px solid var(--input-border); border-radius: 8px; background: var(--input-bg); color: var(--text); font-size: 0.95rem; font-family: inherit; outline: none; transition: border-color 0.2s, background 0.2s;}
.todo-input::placeholder { color: var(--footer-text);}
.todo-input:focus { border-color: var(--input-focus);}
.add-btn { padding: 0.65rem 1.2rem; background: var(--btn-bg); color: var(--btn-text); border: none; border-radius: 8px; font-size: 0.95rem; font-weight: 600; font-family: inherit; cursor: pointer; transition: background 0.2s;}
.add-btn:hover { background: var(--btn-hover);}
/* === Todo List === */.todo-list { list-style: none; max-height: 400px; overflow-y: auto;}
.todo-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1.5rem; border-top: 1px solid var(--item-border); transition: background 0.15s;}
.todo-item:hover { background: var(--item-hover);}
.todo-item input[type="checkbox"] { width: 1.15rem; height: 1.15rem; accent-color: var(--btn-bg); cursor: pointer; flex-shrink: 0;}
.todo-item .todo-text { flex: 1; font-size: 0.95rem; transition: color 0.2s, text-decoration 0.2s; word-break: break-word;}
.todo-item.completed .todo-text { text-decoration: line-through; color: var(--completed-text);}
.delete-btn { background: none; border: none; color: var(--footer-text); cursor: pointer; font-size: 1.2rem; padding: 0.15rem 0.4rem; border-radius: 4px; line-height: 1; opacity: 0; transition: opacity 0.15s, color 0.15s, background 0.15s;}
.todo-item:hover .delete-btn { opacity: 1;}
.delete-btn:hover { color: var(--delete-color); background: rgba(239, 68, 68, 0.1);}
/* === Footer === */.footer { padding: 0.9rem 1.5rem; border-top: 1px solid var(--item-border); text-align: center; font-size: 0.85rem; color: var(--footer-text);}Show script.js
// =============================================// Todo List + Dark Mode Toggle// Vanilla JS — State → Render → Events pattern// =============================================
// === Helper: Safe localStorage ===const loadFromStorage = (key, fallback) => { try { const saved = localStorage.getItem(key); return saved ? JSON.parse(saved) : fallback; } catch (error) { console.error(`Failed to load "${key}" from localStorage:`, error.message); return fallback; }};
const saveToStorage = (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error(`Failed to save "${key}" to localStorage:`, error.message); }};
// === State ===let todos = loadFromStorage("todos", []);let isDark = localStorage.getItem("theme") === "dark";
// === DOM Elements ===const todoForm = document.querySelector("#todo-form");const todoInput = document.querySelector("#todo-input");const todoList = document.querySelector("#todo-list");const todoCount = document.querySelector("#todo-count");const themeToggle = document.querySelector("#theme-toggle");
// === Render ===const render = () => { // Clear list todoList.innerHTML = "";
// Build todo items todos.forEach((todo) => { const li = document.createElement("li"); li.className = `todo-item${todo.completed ? " completed" : ""}`;
// Checkbox const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = todo.completed; checkbox.addEventListener("change", () => { todo.completed = !todo.completed; save(); render(); });
// Text const span = document.createElement("span"); span.className = "todo-text"; span.textContent = todo.text;
// Delete button const deleteBtn = document.createElement("button"); deleteBtn.className = "delete-btn"; deleteBtn.textContent = "\u00d7"; deleteBtn.setAttribute("aria-label", `Delete "${todo.text}"`); deleteBtn.addEventListener("click", () => { todos = todos.filter((t) => t.id !== todo.id); save(); render(); });
li.appendChild(checkbox); li.appendChild(span); li.appendChild(deleteBtn); todoList.appendChild(li); });
// Update count const remaining = todos.filter((t) => !t.completed).length; todoCount.textContent = `${remaining} item${remaining !== 1 ? "s" : ""} left`;};
// === Save State ===const save = () => { saveToStorage("todos", todos);};
// === Theme ===const applyTheme = () => { document.body.classList.toggle("dark", isDark); localStorage.setItem("theme", isDark ? "dark" : "light");};
// === Events ===
// Add todotodoForm.addEventListener("submit", (e) => { e.preventDefault(); const text = todoInput.value.trim(); if (!text) return;
todos.push({ id: Date.now(), text, completed: false, });
todoInput.value = ""; todoInput.focus(); save(); render();});
// Toggle themethemeToggle.addEventListener("click", () => { isDark = !isDark; applyTheme();});
// === Initialize ===applyTheme();render();Code Explanation
Section titled “Code Explanation”-
HTML Structure
โครงสร้างเรียบง่าย:
.appcontainer ครอบทั้งหมด,headerมีชื่อ app + ปุ่ม theme toggle,formสำหรับเพิ่ม todo,ul#todo-listแสดงรายการ, และfooterแสดงจำนวน ไฟล์ CSS/JS ถูก link แยกเป็นไฟล์ต่างหากPreview OutputTodo ListWhat needs to be done?Add☐ Learn HTML ×☑ Setup project ×☐ Add dark mode ×<header> <form> <ul> <li> <footer> -
CSS Variables + Dark Mode
ใช้ CSS Custom Properties (
--bg,--text,--card-bgฯลฯ) ประกาศค่าสีทั้งหมดบนbodyเมื่อ toggle dark mode จะเพิ่ม classdarkบนbodyแล้ว override ค่า variables ทั้งหมด ทำให้เปลี่ยน theme ได้ง่ายและ smooth ด้วยtransitionPreview OutputLight ModeHello World--bg: #f5f5f5--text: #1a1a2e--btn-bg: #8b5cf6body.darkDark ModeHello World--bg: #0f0f1a--text: #e4e4e7--btn-bg: #8b5cf6 -
State Pattern
JavaScript จัดโครงสร้างตาม State → Render → Events:
- State:
todosarray +isDarkboolean - Render: ฟังก์ชัน
render()อ่าน state แล้วสร้าง DOM ใหม่ทั้งหมด - Events: form submit เพิ่ม todo, checkbox toggle completed, button ลบ todo, theme toggle
Preview OutputStatetodos: [...]isDark: boolrender()RenderDOM UpdateaddEventListenerEventclick / submitsetState↻ Loop - State:
-
createElement แทน innerHTML
ใช้
document.createElement()+textContentสร้าง elements แทน innerHTML เพื่อป้องกัน XSS (Cross-Site Scripting) — user input จะไม่ถูก parse เป็น HTMLPreview OutputinnerHTML = userInputXSS Risk!vscreateElement() + textContentSafe -
localStorage + Error Handling
ฟังก์ชัน
loadFromStorage()และsaveToStorage()ครอบด้วยtry/catchเพื่อป้องกัน JSON.parse error ถ้าข้อมูลเสียหาย Todo IDs ใช้Date.now()เพื่อให้ unique ข้าม sessionsPreview OutputlocalStorageKeyValue"todos" [{id:1, text:'Learn JS'...}]"theme" "dark"↓ saveToStorage()↑ loadFromStorage() -
Theme Toggle
applyTheme()ใช้classList.toggle("dark", isDark)— parameter ที่สองบอกว่าจะเพิ่มหรือลบ class แล้วบันทึก theme ลง localStorage ทุกครั้งPreview Output<body>LightclassList.toggle("dark")<body class="dark">Dark -
Accessible UI
Delete button มี
aria-labelสำหรับ screen reader, checkbox ใช้ native<input type="checkbox">, และ form ใช้<form>+submitevent ที่รองรับทั้งการกดปุ่มและ EnterPreview Outputaria-labelScreen reader support✓Native <input>Built-in checkbox a11y✓Form submitEnter key support✓
What You Learned
Section titled “What You Learned”- ใช้ JavaScript พื้นฐาน: variables (
const/let), functions (arrow), arrays (.push(),.filter(),.forEach()) - จัดการ DOM:
querySelector,createElement,textContent,classList,addEventListener - เข้าใจ State pattern: State → Render → Event → State loop
- ทำ interactive features: เพิ่ม/ลบ/toggle todo, dark mode toggle
- ใช้ localStorage: บันทึกและโหลด data +
try/catcherror handling - เขียน CSS Variables: สร้าง themeable UI ที่เปลี่ยน dark/light mode ได้ง่าย