تخطَّ إلى المحتوى

مشروع JavaScript: قائمة مهام تحفظ بياناتك

الدرس 28 من 28 · JavaScript

وصلت إلى الدرس الأخير، وحان وقت أن تجمع كل ما تعلمته في تطبيق حقيقي: قائمة مهام تضيف إليها وتشطب منها وتحذف، وتجد مهامك كما هي عندما تعود في الغد. ستستخدم المصفوفات والكائنات والدوال و DOM والأحداث و JSON و localStorage، كلها معاً.

  • حقل تكتب فيه المهمة، وتضيفها بالزر أو بمفتاح Enter.
  • مربع اختيار بجانب كل مهمة يشطبها عند إنهائها.
  • زر يحذف المهمة.
  • سطر يعرض عدد المهام المتبقية.
  • حفظ تلقائي، فلا تضيع المهام عند تحديث الصفحة.

والخطة هي نفسها التي استخدمتها في مشروع العدّاد، وتُبنى بها أغلب التطبيقات:

  1. البيانات في مصفوفة واحدة اسمها tasks، وكل مهمة كائن بهذا الشكل: { id: 1, text: "مذاكرة", done: false }.
  2. دالة render ترسم الصفحة كلها من هذه المصفوفة.
  3. كل عملية تغيّر المصفوفة، ثم تحفظ، ثم تستدعي render.

بهذا لا تختلف الصفحة عن البيانات أبداً، لأن الصفحة مجرد صورة للمصفوفة.

أنشئ مجلداً للمشروع، وفيه ملف index.html:

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: البيانات والحفظ”
app.js
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. السبب أن المخزن شيء خارج عن سيطرتك: قد يمنعه المتصفح، وقد يمتلئ، وقد يكون النص المحفوظ تالفاً. في كل هذه الحالات يكمل التطبيق عمله بقائمة فارغة بدلاً من أن يتوقف.

app.js
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: الإضافة والشطب والحذف”
app.js
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() يرسم المهام المحفوظة عند فتح الصفحة.

هذا هو التطبيق كاملاً. أضف مهمتين أو ثلاثاً، واشطب واحدة، واحذف أخرى:

جرّب بنفسكJavaScript
أو اضغط Ctrl + Enter

الناتج

ستظهر في الأسفل رسالة «تعذّر حفظ المهام في هذا المتصفح». هذا متوقع: صندوق التجربة معزول ولا يُسمح له بالتخزين، فالتقطت catch الخطأ وأكمل التطبيق عمله. انسخ الملفين إلى جهازك وافتح index.html، ثم أضف مهاماً وحدّث الصفحة: ستجدها في مكانها.

وبعد أن يعمل التطبيق عندك، طوّره بهذه التحديات:

  1. أضف زراً «احذف المنتهية» يحذف كل المهام المشطوبة مرة واحدة.
  2. أضف ثلاثة أزرار للتصفية: الكل، والمتبقية، والمنتهية.
  3. امنع إضافة مهمة مكررة باستخدام some.
  4. قسّم الكود إلى وحدتين: storage.js للتحميل والحفظ، و app.js للباقي.
  • تغيير البيانات ونسيان الرسم: تتغير المصفوفة والصفحة كما هي. اجعل كل عملية تنتهي بـ update.
  • تعديل الصفحة بدلاً من البيانات: لو حذفت عنصر li من الصفحة فقط، فستعود المهمة عند أول رسم أو تحديث. غيّر المصفوفة، واترك الصفحة لـ render.
  • نسيان trim: المستخدم الذي يضغط المسافة ثم Enter يضيف مهمة فارغة.

سؤال

لماذا تنتهي كل عملية (إضافة، شطب، حذف) باستدعاء render؟

سؤال

ماذا يفعل السطر tasks = tasks.filter((item) => item.id !== id)؟

  • البيانات في مصفوفة واحدة، ودالة render ترسم الصفحة منها.
  • كل عملية تغيّر المصفوفة، ثم تحفظ، ثم ترسم.
  • لكل مهمة id فريد تعرف به الأزرار مهمتها.
  • try و catch حول المخزن تجعل التطبيق يعمل حتى لو فشل الحفظ.
  • نص المستخدم يُعرض بـ textContent دائماً.

أنهيت كورس JavaScript كاملاً: من أول console.log إلى تطبيق يعمل ويحفظ بياناته. والمهارة الآن تثبت بالتطبيق، فابنِ مشاريع من أفكارك:

  • تطبيق طقس يجلب البيانات بـ fetch ويعرضها.
  • لعبة أسئلة تحسب النتيجة وتحفظ أعلى رقم وصلت إليه.
  • دفتر مصروفات يجمع ما أنفقته بـ reduce.

وإلى جانب المشاريع، قوِّ أساسك في بناء الصفحات وتنسيقها من كورس HTML و كورس CSS، وتعلّم حفظ نسخ مشاريعك ومشاركتها من كورس Git.