JavaScript والـ DOM: صفحة تتفاعل
درس 7 من 8
7JavaScript والـ DOM: صفحة تتفاعل
الصورة الكبيرةالمتصفح يحوّل HTML لشجرة كائنات اسمها DOM. JavaScript يقرأ هذه الشجرة ويعدّلها، فتتغيّر الصفحة أمامك بلا إعادة تحميل. هنا يلتقي كل ما تعلّمته: HTML يبني، CSS يلبس، وJavaScript يحرّك.
ماذا تشاهد في الرسم: دورة كل واجهة تفاعلية، من قائمة مهام إلى React: حدث، تحديث حالة، رسم. أطر العمل لاحقًا تؤتمت خطوة الرسم فقط.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
الصفحة مثل لوحة إعلانات. HTML وضع الأوراق عليها، وJavaScript يدك: تستطيع أن تنزع ورقة، تكتب على أخرى، تضيف ورقة جديدة. الحدث جرس يرنّ حين يضغط الزائر شيئًا أو يكتب، فتعرف متى تتحرّك يدك. والفكرة الذكية: بدل أن تمسح وتكتب على اللوحة في كل مكان، احتفظ بدفتر فيه قائمة الأوراق (الحالة)، وكلما تغيّر الدفتر أعد ترتيب اللوحة منه (الرسم).
DOM واجهة كائنات (Node، Element، Document) فوق شجرة المستند. querySelector/querySelectorAll تقبل أي محدّد CSS وتعيد عنصرًا أو NodeList ساكنة. تعديل textContent آمن، أما innerHTML مع مدخلات المستخدم فيفتح ثغرة XSS (حقن سكربت). الأحداث تنتشر بثلاث مراحل (capture ثم target ثم bubble)، فالتفويض يضع مستمعًا واحدًا على الأب ويحدّد الهدف بـ event.target.closest(). preventDefault يوقف السلوك الافتراضي وstopPropagation يوقف الانتشار. كل تعديل على الشجرة قد يكلّف إعادة حساب أنماط وتخطيط (reflow)، فاجمع التعديلات بـ replaceChildren أو DocumentFragment. والسكربت يُحمَّل بـ defer أو في آخر body حتى تكون العناصر موجودة. وlocalStorage تخزين متزامن نصّي لكل أصل (origin) بحدود بضعة ميغابايت.
قبل أن تبدأ
🧰الدوال والمصفوفات والكائنات
تعرف الدوال السهمية وmap وfilter والنشر.
🧱HTML الدلالي
تعرف النماذج والأزرار والعناصر الدلالية.
🎨CSS للتخطيط
تعرف الأصناف وكيف تغيّر الشكل.
الفكرة الجوهرية
1شجرة DOM: الصفحة ككائنات
حين يقرأ المتصفح HTML، يبني منه شجرة: document في الجذر، تحته html، تحته head وbody، وهكذا. كل عنصر في الشجرة كائن JavaScript له خصائص تقرؤها وتغيّرها (textContent، id، className) وطرق تناديها (append، remove). غيّر الكائن، تتغيّر الصفحة فورًا.
اقرأ الشجرة وغيّرها
لاحظ أن العنوان تغيّر رغم أن HTML يقول «مرحبًا»: JavaScript عدّل الشجرة بعد أن رسمها المتصفح. أضف فقرة ثالثة بصنف note وشاهد العدد.
document.body.style.background = "pink" واضغط Enter. غيّرت صفحة غيرك (على جهازك فقط، وتعود عند التحديث). هذه قوة DOM.2اختيار العناصر وتعديلها
document.querySelector(".total") يعطيك أول عنصر يطابق المحدّد (نفس محدّدات CSS)، أو null إن لم يجد. وquerySelectorAll يعطيك كلها في قائمة تمرّ عليها بـ forEach. بعدها: textContent للنص، وclassList.add/remove/toggle للأصناف (والشكل يأتي من CSS)، وsetAttribute أو الخاصية مباشرة (img.src) للخصائص، وdataset لخصائص data-*، وstyle للأنماط المباشرة.
اختر وغيّر النص والأصناف
اضغط الزر. ثم غيّر items[2] إلى items[0] ليصير اللاتيه هو النافد. لاحظ أن الشكل كله في CSS، وJavaScript أضاف صنفًا فقط.
document.querySelector(".totl") بخطأ إملائي يعيد null، ثم null.textContent = ... يوقف البرنامج بخطأ «Cannot set properties of null». إن رأيت هذه الرسالة، فالمحدّد لم يجد شيئًا: افحص الاسم، أو أن السكربت يعمل قبل أن يوجد العنصر.3إنشاء العناصر وحذفها
document.createElement("li") يصنع عنصرًا جديدًا في الذاكرة، تعطيه نصًّا وأصنافًا، ثم تضعه في الصفحة بـ parent.append(el) (في النهاية) أو prepend (في البداية). وel.remove() يحذفه. وparent.replaceChildren(...list) يستبدل كل الأبناء بقائمة جديدة دفعة واحدة، وهو مثالي لإعادة رسم قائمة.
أضف عناصر واحذفها
أضف ثلاثة مشروبات واحذف واحدًا بزر ✕. ثم جرّب كتابة <b>عريض</b> كاسم: يظهر كنص كما هو لأننا استخدمنا textContent.
4الأحداث: استمع ثم تصرّف
el.addEventListener("click", fn) يربط دالة بحدث: «حين يحدث هذا، نفّذ ذلك». أشهر الأحداث: click للضغط، وinput كلما تغيّر نص حقل، وchange حين يتأكّد التغيير، وsubmit لإرسال النموذج، وkeydown لضغط مفتاح. والدالة تستلم كائن الحدث event فيه تفاصيله: event.target العنصر الذي وقع عليه، وevent.key المفتاح المضغوط.
عدّاد حروف وزر إعجاب
اكتب في المربع وراقب العدّاد يتغيّر مع كل حرف، ويحمرّ بعد 120.
النماذج: استمع لـ submit على النموذج نفسه لا لـ click على الزر، فيغطّي Enter أيضًا ويحافظ على التحقّق المدمج. ونادِ event.preventDefault() أول سطر حتى لا تُعاد تحميل الصفحة. وform.elements.name.value يقرأ قيمة الحقل الذي name="name".
5تفويض الأحداث: مستمع واحد لقائمة كاملة
قائمة فيها مئة زر حذف؟ لا تضع مئة مستمع. الأحداث تصعد (bubble) من العنصر الذي ضُغط إلى أبيه ثم جدّه حتى document. فضع مستمعًا واحدًا على القائمة، وevent.target.closest("li") يخبرك أي بند ضُغط. ويعمل حتى للبنود التي تُضاف لاحقًا، لأن المستمع على الأب الثابت.
// simulating bubbling without a browser: each node knows its parent
const tree = { name: "ul#list", parent: null };
const li = { name: "li[data-id=2]", parent: tree };
const button = { name: "button.delete", parent: li };
function dispatch(target) {
const path = [];
for (let node = target; node; node = node.parent) path.push(node.name);
return path;
}
console.log("click travels:", dispatch(button).join(" -> "));
console.log("so a single listener on", tree.name, "hears clicks from every item");6الحالة أولًا ثم الرسم
أكبر فكرة في هذا الدرس: بدل أن تعدّل الصفحة في عشرة أماكن (أضف هنا، احذف هناك، حدّث العدّاد)، احفظ البيانات في متغير واحد (الحالة)، واكتب دالة render() واحدة ترسم الصفحة كاملة منها. كل حدث يعدّل الحالة ثم ينادي render. الصفحة لا تختلف عن البيانات أبدًا، والأخطاء تقل كثيرًا. وهذه بالضبط الفكرة التي بُني عليها React.
// the logic part runs anywhere; the page part needs a browser
let todos = [{ id: 1, text: "buy beans", done: false }];
function add(text) {
todos = [...todos, { id: todos.length + 1, text, done: false }];
}
function toggle(id) {
todos = todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
}
function render() { // in the browser this builds <li> elements
return todos.map((t) => `${t.done ? "[x]" : "[ ]"} ${t.text}`).join("\n");
}
add("grind coffee");
toggle(1);
console.log(render());
console.log("left:", todos.filter((t) => !t.done).length);7localStorage: ذاكرة تبقى بعد التحديث
كل متغيرات JavaScript تُمسح حين تُحدَّث الصفحة. localStorage مخزن صغير في المتصفح يبقى بعد التحديث وإغلاق المتصفح: setItem(key, text) تحفظ وgetItem(key) تقرأ. وهو يخزّن نصوصًا فقط، لذلك تحوّل المصفوفة بـ JSON.stringify قبل الحفظ وتعيدها بـ JSON.parse بعد القراءة.
// in the browser
localStorage.setItem("todos", JSON.stringify(todos)); // save
const saved = JSON.parse(localStorage.getItem("todos") ?? "[]"); // load (or empty list)8الأمان: textContent لا innerHTML
innerHTML يفسّر النص كـ HTML. لو وضعت فيه شيئًا كتبه المستخدم، وكتب <img src=x onerror="سرقة_الجلسة()">، سيُنفَّذ كوده في صفحتك على أجهزة كل من يرى تعليقه. هذا هجوم XSS (حقن سكربت عبر المواقع)، من أشهر ثغرات الويب. القاعدة: أي نص من المستخدم يدخل بـ textContent، والعناصر تُبنى بـ createElement.
شاهد XSS بعينك (في بيئة معزولة)
اضغط الزر الأول: الصفحة صارت حمراء، أي أن كود «المستخدم» نُفِّذ. المهاجم الحقيقي كان سيسرق جلستك بدل تغيير اللون. ثم اضغط الثاني: يظهر النص كما هو ولا يُنفَّذ شيء. المعاينة معزولة تمامًا عن صفحة الدرس.
مثال محلول
قائمة مهام كاملة، تعمل هنا الآن: الحالة في مصفوفة، render يرسم كل شيء منها، مستمع واحد للنموذج وواحد مفوَّض للقائمة، وlocalStorage للحفظ. أضف مهمة، اضغط على نص مهمة لشطبها، واحذف أخرى. ثم اقرأ الكود من أعلى لأسفل: كل سطر فيه شرحناه في هذا الدرس.
قائمة المهام
بعد أن تفهمها، أضف ميزة: زر «امسح المنجز» يحذف كل المهام المشطوبة بسطر filter واحد.
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| تحتاج React لأي صفحة تفاعلية. | JavaScript العادي يكفي لكثير من الصفحات، وفهمه يجعل React أسهل بكثير. |
| innerHTML أسرع فاستخدمه دائمًا. | مع مدخلات المستخدم هو ثغرة أمنية. textContent وcreateElement آمنان. |
| تغيير الصفحة يحتاج إعادة تحميلها. | DOM يتغيّر مباشرة، وهذا ما يجعل تطبيقات الويب تشعر كتطبيقات الجوال. |
أين تقابله في العمل
🛒سلة المتجر
زر «أضف» يحدّث الحالة والعدّاد دون تحميل.
🔎البحث الفوري
حدث input يصفّي النتائج مع كل حرف.
📝النماذج الذكية
إظهار حقل حسب اختيار سابق، ورسائل خطأ مخصّصة.
اختبر فهمك
نموذج يعيد تحميل الصفحة عند الإرسال. ما الحل؟
preventDefault يوقف السلوك الافتراضي للإرسال، والنموذج يبقى صالحًا لـ Enter والوصول.
لماذا تفويض الأحداث مفيد في قائمة تتغيّر؟
الحدث يصعد من العنصر للأب، فلا تحتاج ربط كل عنصر جديد.
أي سطر آمن لعرض اسم كتبه المستخدم؟
textContent يعامل الإدخال كنص فقط فلا يُنفَّذ أي وسم.
رسالة «Cannot set properties of null». ما السبب الأرجح؟
المحدّد خاطئ أو السكربت يعمل قبل وجود العنصر، فرجع null.
لماذا نحوّل المصفوفة بـ JSON.stringify قبل localStorage؟
بدون تحويل يُحفظ "[object Object]" وتضيع البيانات.
بطاقات مراجعة سريعة
تحقق من فهمك
ما معنى «الحالة أولًا ثم الرسم»؟
البيانات في متغير واحد هي الحقيقة، وكل تغيير يحدّثها ثم يعيد رسم الصفحة منها بدل تعديل العناصر يدويًا في أماكن كثيرة.
لماذا نستمع لـ submit لا لـ click على زر الإرسال؟
لأن submit يغطّي الضغط وEnter معًا ويحترم التحقّق المدمج قبل أن يصل لكودك.
ما الفرق بين querySelector وquerySelectorAll؟
الأول يعيد أول تطابق أو null، والثاني قائمة بكل التطابقات.
أين تضع وسم script حتى يجد العناصر؟
في آخر body، أو في head مع خاصية defer، حتى يُنفَّذ بعد بناء الشجرة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
مقالات1
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.