مشروع JavaScript: قائمة مهام تحفظ بياناتك
وصلت إلى الدرس الأخير، وحان وقت أن تجمع كل ما تعلمته في تطبيق حقيقي: قائمة مهام تضيف إليها وتشطب منها وتحذف، وتجد مهامك كما هي عندما تعود في الغد. ستستخدم المصفوفات والكائنات والدوال و DOM والأحداث و JSON و localStorage، كلها معاً.
ماذا سنبني؟
Section titled “ماذا سنبني؟”- حقل تكتب فيه المهمة، وتضيفها بالزر أو بمفتاح Enter.
- مربع اختيار بجانب كل مهمة يشطبها عند إنهائها.
- زر يحذف المهمة.
- سطر يعرض عدد المهام المتبقية.
- حفظ تلقائي، فلا تضيع المهام عند تحديث الصفحة.
والخطة هي نفسها التي استخدمتها في مشروع العدّاد، وتُبنى بها أغلب التطبيقات:
- البيانات في مصفوفة واحدة اسمها
tasks، وكل مهمة كائن بهذا الشكل:{ id: 1, text: "مذاكرة", done: false }. - دالة
renderترسم الصفحة كلها من هذه المصفوفة. - كل عملية تغيّر المصفوفة، ثم تحفظ، ثم تستدعي
render.
بهذا لا تختلف الصفحة عن البيانات أبداً، لأن الصفحة مجرد صورة للمصفوفة.
الخطوة 1: الصفحة
Section titled “الخطوة 1: الصفحة”أنشئ مجلداً للمشروع، وفيه ملف index.html:
<!DOCTYPE html><html lang="ar" dir="rtl"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>قائمة مهامي</title> <style> body { font-family: sans-serif; max-width: 480px; margin: 24px auto; padding: 0 16px; } .add-row { display: flex; gap: 8px; } #task-input { flex: 1; padding: 10px; font-size: 16px; } button { padding: 8px 14px; font-size: 16px; cursor: pointer; } ul { list-style: none; padding: 0; } li { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #ddd; } li label { flex: 1; } .done { text-decoration: line-through; color: #666; } </style></head><body> <h1>قائمة مهامي</h1>
<div class="add-row"> <input id="task-input" type="text" placeholder="اكتب مهمة جديدة" aria-label="مهمة جديدة"> <button id="add-button">أضف</button> </div>
<p id="summary"></p> <ul id="task-list"></ul>
<script src="app.js"></script></body></html>القائمة ul فارغة عن قصد: JavaScript هي التي ستملؤها. وباقي الخطوات كلها في ملف app.js.
الخطوة 2: البيانات والحفظ
Section titled “الخطوة 2: البيانات والحفظ”const input = document.querySelector("#task-input");const addButton = document.querySelector("#add-button");const list = document.querySelector("#task-list");const summary = document.querySelector("#summary");
function loadTasks() { try { const text = localStorage.getItem("tasks"); return text === null ? [] : JSON.parse(text); } catch (error) { return []; }}
function saveTasks() { try { localStorage.setItem("tasks", JSON.stringify(tasks)); } catch (error) { console.log("تعذّر حفظ المهام في هذا المتصفح"); }}
let tasks = loadTasks();هاتان الدالتان تعرفهما من درس التخزين المحلي، وأضفنا إليهما try و catch. السبب أن المخزن شيء خارج عن سيطرتك: قد يمنعه المتصفح، وقد يمتلئ، وقد يكون النص المحفوظ تالفاً. في كل هذه الحالات يكمل التطبيق عمله بقائمة فارغة بدلاً من أن يتوقف.
الخطوة 3: رسم المهام
Section titled “الخطوة 3: رسم المهام”function render() { list.textContent = "";
tasks.forEach((task) => { const li = document.createElement("li"); const label = document.createElement("label"); const checkbox = document.createElement("input"); const text = document.createElement("span"); const removeButton = document.createElement("button");
checkbox.type = "checkbox"; checkbox.checked = task.done; checkbox.addEventListener("change", () => toggleTask(task.id));
text.textContent = task.text; if (task.done) { text.classList.add("done"); }
removeButton.textContent = "حذف"; removeButton.addEventListener("click", () => deleteTask(task.id));
label.append(checkbox, text); li.append(label, removeButton); list.append(li); });
const remaining = tasks.filter((task) => !task.done).length; summary.textContent = tasks.length === 0 ? "لا توجد مهام بعد. أضف أول مهمة!" : `المتبقي: ${remaining} من ${tasks.length}`;}الدالة تمسح القائمة، ثم تبني لكل مهمة عنصر li فيه مربع اختيار ونص وزر حذف. وهذه أهم النقاط فيها:
checkbox.checkedتحدد هل المربع مُعلَّم، والحدثchangeيقع عندما يغيّره المستخدم.- كل زر يعرف مهمته من
task.id، فعند الضغط نمرر الرقم إلى الدالة المناسبة. - وضعنا المربع والنص داخل
label، فيكفي الضغط على النص نفسه لتعليم المربع.
الخطوة 4: الإضافة والشطب والحذف
Section titled “الخطوة 4: الإضافة والشطب والحذف”function update() { saveTasks(); render();}
function addTask() { const text = input.value.trim(); if (text === "") { return; } tasks.push({ id: Date.now(), text: text, done: false }); input.value = ""; update();}
function toggleTask(id) { const task = tasks.find((item) => item.id === id); task.done = !task.done; update();}
function deleteTask(id) { tasks = tasks.filter((item) => item.id !== id); update();}
addButton.addEventListener("click", addTask);input.addEventListener("keydown", (event) => { if (event.key === "Enter") { addTask(); }});
render();update: كل عملية تنتهي بالحفظ ثم الرسم، فجمعناهما في دالة واحدة.addTask: تنظف النص بـtrimوترفض المهمة الفارغة. ورقم المهمة هوDate.now()، أي الوقت الحالي بالملي ثانية، فلا يتكرر بين مهمتين.toggleTask: تجد المهمة بـfindوتعكس قيمةdone.deleteTask: تبقي بـfilterكل المهام ما عدا صاحبة هذا الرقم.event.keyتعطيك اسم المفتاح المضغوط، فنضيف المهمة عند Enter.
والسطر الأخير render() يرسم المهام المحفوظة عند فتح الصفحة.
جرّب بنفسك
Section titled “جرّب بنفسك”هذا هو التطبيق كاملاً. أضف مهمتين أو ثلاثاً، واشطب واحدة، واحذف أخرى:
الناتج
ستظهر في الأسفل رسالة «تعذّر حفظ المهام في هذا المتصفح». هذا متوقع: صندوق التجربة معزول ولا يُسمح له بالتخزين، فالتقطت catch الخطأ وأكمل التطبيق عمله. انسخ الملفين إلى جهازك وافتح index.html، ثم أضف مهاماً وحدّث الصفحة: ستجدها في مكانها.
وبعد أن يعمل التطبيق عندك، طوّره بهذه التحديات:
- أضف زراً «احذف المنتهية» يحذف كل المهام المشطوبة مرة واحدة.
- أضف ثلاثة أزرار للتصفية: الكل، والمتبقية، والمنتهية.
- امنع إضافة مهمة مكررة باستخدام
some. - قسّم الكود إلى وحدتين:
storage.jsللتحميل والحفظ، وapp.jsللباقي.
أخطاء شائعة
Section titled “أخطاء شائعة”- تغيير البيانات ونسيان الرسم: تتغير المصفوفة والصفحة كما هي. اجعل كل عملية تنتهي بـ
update. - تعديل الصفحة بدلاً من البيانات: لو حذفت عنصر
liمن الصفحة فقط، فستعود المهمة عند أول رسم أو تحديث. غيّر المصفوفة، واترك الصفحة لـrender. - نسيان
trim: المستخدم الذي يضغط المسافة ثم Enter يضيف مهمة فارغة.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
لماذا تنتهي كل عملية (إضافة، شطب، حذف) باستدعاء render؟
البيانات هي المصدر الوحيد للحقيقة، والصفحة صورة منها. بعد أي تغيير في المصفوفة نعيد الرسم لتطابقها الصفحة.
سؤال
ماذا يفعل السطر tasks = tasks.filter((item) => item.id !== id)؟
filter تُرجع مصفوفة جديدة فيها العناصر التي يتحقق فيها الشرط، والشرط هنا أن يختلف رقم المهمة عن الرقم المطلوب حذفه.
الخلاصة
Section titled “الخلاصة”- البيانات في مصفوفة واحدة، ودالة
renderترسم الصفحة منها. - كل عملية تغيّر المصفوفة، ثم تحفظ، ثم ترسم.
- لكل مهمة
idفريد تعرف به الأزرار مهمتها. tryوcatchحول المخزن تجعل التطبيق يعمل حتى لو فشل الحفظ.- نص المستخدم يُعرض بـ
textContentدائماً.
مبروك! ماذا بعد؟
Section titled “مبروك! ماذا بعد؟”أنهيت كورس JavaScript كاملاً: من أول console.log إلى تطبيق يعمل ويحفظ بياناته. والمهارة الآن تثبت بالتطبيق، فابنِ مشاريع من أفكارك:
- تطبيق طقس يجلب البيانات بـ
fetchويعرضها. - لعبة أسئلة تحسب النتيجة وتحفظ أعلى رقم وصلت إليه.
- دفتر مصروفات يجمع ما أنفقته بـ
reduce.
وإلى جانب المشاريع، قوِّ أساسك في بناء الصفحات وتنسيقها من كورس HTML و كورس CSS، وتعلّم حفظ نسخ مشاريعك ومشاركتها من كورس Git.