مشروع To-Do List — قائمة مهام تفاعلية
هذا أول مشروع “فعلي” لك في JavaScript. سننفّذ تطبيق قائمة مهام يسمح للمستخدم أن:
- يضيف مهمة جديدة
- يحذف مهمة
- يعلّم المهمة أنها “منتهية” ✅
- يشاهد عدد المهام (الإجمالي والمنتهي)
الهدف هنا ليس النسخ فقط… الهدف أن تفهم “كيف أجزاء JavaScript تتعاون” لبناء تطبيق.
الفكرة العامة قبل كتابة الكود
- سنخزن المهام داخل مصفوفة
tasks - كل مهمة ستكون كائنًا:
{ text, done } - عند إضافة/حذف/تغيير حالة مهمة → نحدّث المصفوفة ثم نعيد رسم القائمة (render)
قاعدة ذهبية: واجهة الصفحة تتبع البيانات (Data) وليس العكس.
الواجهة (HTML) التي سنبني عليها
انسخ هذا الـ HTML داخل صفحتك (أو داخل Blade). هو بسيط لكن كافي للمشروع:
قائمة المهام
أضف مهامك وعلّم المنتهي ✅
الإجمالي: 0
المنتهي: 0
لاحظ أننا جهّزنا عناصر لها IDs حتى نمسكها بسهولة في JavaScript.
الخطوة 1: تجهيز عناصر DOM + مصفوفة المهام
document.addEventListener("DOMContentLoaded", function () {
// عناصر الصفحة
const taskForm = document.getElementById("taskForm");
const taskInput = document.getElementById("taskInput");
const taskList = document.getElementById("taskList");
const totalCount = document.getElementById("totalCount");
const doneCount = document.getElementById("doneCount");
const clearDone = document.getElementById("clearDone");
// بيانات التطبيق (المهام)
let tasks = [];
});
لماذا وضعنا كل شيء داخل DOMContentLoaded؟
حتى نتأكد أن العناصر موجودة قبل ما نمسكها.
الخطوة 2: دالة render لإعادة رسم القائمة
كل مرة تتغير المهام (إضافة/حذف/تحديد) نحتاج نعيد بناء القائمة على الشاشة. هذه هي وظيفة render.
function render() {
// امسح القائمة الحالية
taskList.innerHTML = "";
// احسب الإحصائيات
totalCount.textContent = tasks.length;
doneCount.textContent = tasks.filter(t => t.done).length;
// ارسم كل مهمة
tasks.forEach(function (task, index) {
const li = document.createElement("li");
li.className = "flex items-center justify-between gap-3 p-4 rounded-xl border border-gray-200 dark:border-gray-800 bg-white/60 dark:bg-gray-900";
const left = document.createElement("div");
left.className = "flex items-center gap-3";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.done;
checkbox.className = "w-5 h-5";
const text = document.createElement("span");
text.textContent = task.text;
text.className = task.done ? "line-through text-gray-500" : "font-semibold";
// عند تغيير علامة الصح
checkbox.addEventListener("change", function () {
tasks[index].done = checkbox.checked;
render();
});
left.appendChild(checkbox);
left.appendChild(text);
const delBtn = document.createElement("button");
delBtn.textContent = "حذف";
delBtn.className = "px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700 transition";
// حذف مهمة واحدة
delBtn.addEventListener("click", function () {
tasks.splice(index, 1);
render();
});
li.appendChild(left);
li.appendChild(delBtn);
taskList.appendChild(li);
});
}
ليش render مهم؟
لأننا بدل ما نتلاعب بعناصر كثيرة يدويًا، نعيد بناء الواجهة من بياناتنا مباشرة. هذا يجعل التطبيق منظّم ويقلل الأخطاء.
الخطوة 3: إضافة مهمة (submit)
taskForm.addEventListener("submit", function (event) {
event.preventDefault();
const value = taskInput.value.trim();
if (value === "") {
alert("اكتب مهمة أولاً");
return;
}
// أضف مهمة جديدة (كائن)
tasks.push({ text: value, done: false });
// فرّغ الحقل
taskInput.value = "";
// اعرض التحديث
render();
});
استخدمنا trim() حتى نمنع إدخال فراغات فقط.
الخطوة 4: حذف المهام المنتهية
clearDone.addEventListener("click", function () {
tasks = tasks.filter(t => !t.done);
render();
});
هنا استخدمنا filter لإرجاع مهام “غير منتهية” فقط، ثم استبدلنا المصفوفة.
إذا لم تفهم filter الآن لا تقلق… ستفهمها أكثر في درس المصفوفات المتقدم. لكن الفكرة بسيطة: نحتفظ بما نريد ونرمي الباقي.
الكود كامل في ملف واحد (جاهز للنسخ)
ضع هذا الكود في script.js أو داخل <script> قبل </body>.
document.addEventListener("DOMContentLoaded", function () {
const taskForm = document.getElementById("taskForm");
const taskInput = document.getElementById("taskInput");
const taskList = document.getElementById("taskList");
const totalCount = document.getElementById("totalCount");
const doneCount = document.getElementById("doneCount");
const clearDone = document.getElementById("clearDone");
let tasks = [];
function render() {
taskList.innerHTML = "";
totalCount.textContent = tasks.length;
doneCount.textContent = tasks.filter(t => t.done).length;
tasks.forEach(function (task, index) {
const li = document.createElement("li");
li.className = "flex items-center justify-between gap-3 p-4 rounded-xl border border-gray-200 dark:border-gray-800 bg-white/60 dark:bg-gray-900";
const left = document.createElement("div");
left.className = "flex items-center gap-3";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.done;
checkbox.className = "w-5 h-5";
const text = document.createElement("span");
text.textContent = task.text;
text.className = task.done ? "line-through text-gray-500" : "font-semibold";
checkbox.addEventListener("change", function () {
tasks[index].done = checkbox.checked;
render();
});
left.appendChild(checkbox);
left.appendChild(text);
const delBtn = document.createElement("button");
delBtn.textContent = "حذف";
delBtn.className = "px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700 transition";
delBtn.addEventListener("click", function () {
tasks.splice(index, 1);
render();
});
li.appendChild(left);
li.appendChild(delBtn);
taskList.appendChild(li);
});
}
taskForm.addEventListener("submit", function (event) {
event.preventDefault();
const value = taskInput.value.trim();
if (value === "") {
alert("اكتب مهمة أولاً");
return;
}
tasks.push({ text: value, done: false });
taskInput.value = "";
render();
});
clearDone.addEventListener("click", function () {
tasks = tasks.filter(t => !t.done);
render();
});
render();
});
أفكار تطوير سريعة (اختياري)
- حفظ المهام في localStorage (حتى لا تضيع عند تحديث الصفحة)
- إضافة زر “تعديل المهمة”
- إضافة فلتر: (الكل / المنتهي / غير المنتهي)
- إضافة تاريخ أو وقت لكل مهمة
ماذا بعد هذا الدرس؟
الدرس القادم: localStorage (حفظ البيانات في المتصفح)
سنطوّر نفس المشروع بحيث تبقى المهام محفوظة حتى بعد إغلاق المتصفح.