مشروع To-Do List — قائمة مهام تفاعلية

5:00 دقائق مجاني
في هذا الدرس سنبني مشروعًا حقيقيًا يجمع DOM + Events + Arrays: قائمة مهام 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 (حفظ البيانات في المتصفح)

    سنطوّر نفس المشروع بحيث تبقى المهام محفوظة حتى بعد إغلاق المتصفح.