أساسيات JavaScript

التعامل مع DOM: إنشاء وتعديل العناصر

45 دقيقة الدرس 22 من 60

مقدمة في التعامل مع DOM

في الدرس السابق، تعلمت كيفية البحث عن العناصر وتحديدها في DOM. الآن حان الوقت لتتعلم كيفية إنشاء وتعديل وإزالة العناصر. التعامل مع DOM هو ما يحول صفحة HTML ثابتة إلى تطبيق ويب ديناميكي وتفاعلي. في كل مرة ترى تعليقا جديدا يظهر بدون إعادة تحميل الصفحة، أو إشعارا ينبثق، أو عنصر قائمة يضاف أو يحذف، أو مؤشر تحميل يظهر -- هذا هو التعامل مع DOM في العمل.

يمنحك JavaScript تحكما كاملا في DOM. يمكنك إنشاء عناصر جديدة بالكامل، وتغيير محتواها النصي وHTML، وتعديل سماتها وأنماطها، ونقلها في شجرة المستند، واستنساخها، وإزالتها بالكامل. إتقان هذه التقنيات ضروري لبناء واجهات الويب الحديثة.

createElement -- بناء عناصر جديدة

طريقة document.createElement() تنشئ عنصر HTML جديدا في الذاكرة. العنصر ليس جزءا من الصفحة المرئية بعد -- إنه موجود فقط في ذاكرة JavaScript حتى تضيفه صراحة إلى شجرة DOM. هذه العملية المكونة من خطوتين (الإنشاء ثم الإرفاق) تمنحك فرصة لتكوين العنصر بالكامل قبل أن يحتاج المتصفح لعرضه، وهذا أفضل للأداء.

مثال: إنشاء عناصر باستخدام createElement

// إنشاء عنصر فقرة جديد
const paragraph = document.createElement('p');
console.log(paragraph); // <p></p> (فارغ، ليس في DOM بعد)

// إنشاء div جديد
const div = document.createElement('div');

// إنشاء زر جديد
const button = document.createElement('button');

// إنشاء صورة جديدة
const img = document.createElement('img');

// إنشاء رابط جديد
const link = document.createElement('a');

// إنشاء عنصر قائمة جديد
const listItem = document.createElement('li');

// العنصر موجود في الذاكرة لكنه غير مرئي على الصفحة بعد
// يجب إضافته إلى شجرة DOM لجعله يظهر

createTextNode -- إنشاء محتوى نصي

طريقة document.createTextNode() تنشئ عقدة نصية يمكنك إلحاقها بعنصر. بينما يمكنك غالبا استخدام textContent لتعيين النص مباشرة، فإن createTextNode() مفيدة عندما تحتاج لإدراج نص بجانب عقد أخرى أو عند بناء بنى عناصر معقدة برمجيا.

مثال: إنشاء وإلحاق عقد نصية

// إنشاء عقدة نصية
const text = document.createTextNode('مرحبا بالعالم!');

// إنشاء فقرة وإضافة النص إليها
const paragraph = document.createElement('p');
paragraph.appendChild(text);
// paragraph الآن: <p>مرحبا بالعالم!</p>

// يمكنك إنشاء عقد نصية متعددة في عنصر واحد
const greeting = document.createElement('p');
const part1 = document.createTextNode('مرحبا، ');
const bold = document.createElement('strong');
bold.textContent = 'أحمد';
const part2 = document.createTextNode('! سعيد برؤيتك.');

greeting.appendChild(part1);
greeting.appendChild(bold);
greeting.appendChild(part2);
// النتيجة: <p>مرحبا، <strong>أحمد</strong>! سعيد برؤيتك.</p>

appendChild -- إضافة عناصر إلى DOM

طريقة appendChild() تضيف عقدة كـ آخر ابن لعنصر أبوي. هذه هي الطريقة الكلاسيكية لإدراج العناصر في DOM. تأخذ وسيطة واحدة -- العقدة المراد إلحاقها -- وتعيد العقدة الملحقة. إذا كانت العقدة التي تلحقها موجودة بالفعل في DOM، فإن appendChild() ستقوم بـ نقلها من موقعها الحالي إلى الموقع الجديد بدلا من استنساخها.

مثال: استخدام appendChild لبناء قائمة

// الحصول على القائمة غير المرتبة الموجودة
const list = document.getElementById('todo-list');

// إنشاء عنصر قائمة جديد
const newItem = document.createElement('li');
newItem.textContent = 'شراء البقالة';
newItem.classList.add('todo-item');

// إلحاقه كآخر ابن للقائمة
list.appendChild(newItem);

// إنشاء وإلحاق عناصر متعددة
const tasks = ['تنظيف المنزل', 'المشي مع الكلب', 'قراءة كتاب'];
tasks.forEach(function(task) {
    const item = document.createElement('li');
    item.textContent = task;
    item.classList.add('todo-item');
    list.appendChild(item);
});

// نقل عنصر موجود (وليس استنساخ!)
const firstItem = list.children[0];
list.appendChild(firstItem);
// العنصر الأول أصبح الآن العنصر الأخير -- تم نقله وليس نسخه

insertBefore -- الإدراج في موضع محدد

طريقة insertBefore() تدرج عقدة قبل عقدة مرجعية محددة داخل عنصر أبوي. تأخذ وسيطتين: العقدة الجديدة المراد إدراجها والعقدة المرجعية التي يجب أن تأتي بعدها. إذا كانت العقدة المرجعية null، فإن insertBefore() تتصرف مثل appendChild() وتضيف العنصر في النهاية.

مثال: استخدام insertBefore

const list = document.getElementById('todo-list');

// إنشاء عنصر ذي أولوية
const urgentItem = document.createElement('li');
urgentItem.textContent = 'عاجل: إصلاح خطأ الإنتاج';
urgentItem.classList.add('todo-item', 'urgent');

// إدراجه قبل العنصر الأول في القائمة
const firstItem = list.children[0];
list.insertBefore(urgentItem, firstItem);
// العنصر العاجل هو الآن العنصر الأول في القائمة

// الإدراج قبل عنصر محدد (الابن الثالث)
const newItem = document.createElement('li');
newItem.textContent = 'مراجعة طلبات السحب';
const thirdItem = list.children[2];
list.insertBefore(newItem, thirdItem);
// newItem الآن في الموضع 2 (قبل العنصر الثالث القديم)

// عندما تكون المرجعية null، يلحق في النهاية
const lastItem = document.createElement('li');
lastItem.textContent = 'عنصر نهاية القائمة';
list.insertBefore(lastItem, null);
// مثل list.appendChild(lastItem)

طرق الإدراج الحديثة: append و prepend و after و before

يوفر JavaScript الحديث أربع طرق مريحة أسهل في الاستخدام من appendChild() و insertBefore(). هذه الطرق تقبل وسائط متعددة ويمكنها أخذ كل من العناصر والسلاسل النصية (التي تتحول تلقائيا إلى عقد نصية). لا تعيد العقدة المدرجة، على عكس appendChild().

مثال: طرق الإدراج الحديثة

const container = document.getElementById('container');

// append() -- تضيف في نهاية العنصر (مثل appendChild لكن أفضل)
const p1 = document.createElement('p');
p1.textContent = 'الفقرة الأولى';
container.append(p1);

// append() يمكنها أخذ وسائط متعددة مرة واحدة!
const p2 = document.createElement('p');
p2.textContent = 'الفقرة الثانية';
const p3 = document.createElement('p');
p3.textContent = 'الفقرة الثالثة';
container.append(p2, p3); // كلاهما أضيف في النهاية

// append() يمكنها أيضا أخذ سلاسل نصية عادية
container.append('بعض النص اللاحق');

// prepend() -- تضيف في بداية العنصر
const heading = document.createElement('h1');
heading.textContent = 'مرحبا';
container.prepend(heading);
// heading هو الآن الابن الأول للحاوية

// before() -- تدرج قبل العنصر (كأخ)
const notice = document.createElement('div');
notice.textContent = 'إشعار مهم';
container.before(notice);
// notice هو الآن أخ يظهر مباشرة قبل الحاوية

// after() -- تدرج بعد العنصر (كأخ)
const footer = document.createElement('footer');
footer.textContent = 'تذييل الصفحة';
container.after(footer);
// footer هو الآن أخ يظهر مباشرة بعد الحاوية

// جميع الطرق الأربع تقبل وسائط متعددة
const divA = document.createElement('div');
const divB = document.createElement('div');
container.prepend(divA, divB, 'بعض النص');
ملاحظة: الفرق الرئيسي بين append() و appendChild() هو أن append() تقبل السلاسل النصية ووسائط متعددة، بينما appendChild() تقبل فقط عقدة Node واحدة. أيضا، appendChild() تعيد العقدة الملحقة بينما append() تعيد undefined. للكود الجديد، يفضل عموما استخدام append() و prepend() لمرونتهما.

replaceChild و replaceWith

أحيانا تحتاج لاستبدال عنصر بآخر. طريقة replaceChild() الأقدم تستدعى على العنصر الأبوي وتأخذ وسيطتين: العقدة الجديدة والعقدة القديمة المراد استبدالها. طريقة replaceWith() الحديثة تستدعى مباشرة على العنصر الذي تريد استبداله، وهو أمر أكثر بديهية.

مثال: استبدال العناصر

// استخدام replaceChild (الطريقة الأقدم -- تستدعى على الأبوي)
const list = document.getElementById('todo-list');
const oldItem = list.children[1]; // العنصر الثاني

const newItem = document.createElement('li');
newItem.textContent = 'مهمة محدثة';
newItem.classList.add('todo-item', 'updated');

list.replaceChild(newItem, oldItem);
// oldItem تمت إزالته، newItem حل مكانه

// استخدام replaceWith (الطريقة الحديثة -- تستدعى على العنصر نفسه)
const heading = document.querySelector('h1');

const newHeading = document.createElement('h2');
newHeading.textContent = 'عنوان جديد';
newHeading.id = 'main-title';

heading.replaceWith(newHeading);
// h1 اختفى، واستبدل بـ h2

// replaceWith تقبل عقدا وسلاسل نصية متعددة
const oldParagraph = document.querySelector('.old-content');
const span1 = document.createElement('span');
span1.textContent = 'الجزء 1';
const span2 = document.createElement('span');
span2.textContent = 'الجزء 2';
oldParagraph.replaceWith(span1, ' و ', span2);
// الفقرة استبدلت بـ: <span>الجزء 1</span> و <span>الجزء 2</span>

removeChild و remove

لإزالة العناصر من DOM، لديك خياران. طريقة removeChild() الأقدم تستدعى على العنصر الأبوي وتأخذ الابن المراد إزالته كوسيطة. طريقة remove() الحديثة تستدعى مباشرة على العنصر الذي تريد إزالته -- لا حاجة للإشارة إلى الأبوي.

مثال: إزالة العناصر

// استخدام removeChild (الطريقة الأقدم)
const list = document.getElementById('todo-list');
const itemToRemove = list.children[0]; // العنصر الأول
list.removeChild(itemToRemove);

// استخدام remove (الطريقة الحديثة -- أبسط بكثير!)
const notification = document.querySelector('.notification');
notification.remove();
// الإشعار اختفى من DOM

// إزالة جميع أبناء عنصر
const container = document.getElementById('container');

// الطريقة 1: حلقة مع removeChild
while (container.firstChild) {
    container.removeChild(container.firstChild);
}

// الطريقة 2: تعيين innerHTML لسلسلة فارغة (أسرع لكن لها محاذير)
container.innerHTML = '';

// الطريقة 3: replaceChildren بدون وسائط (حديثة ونظيفة)
container.replaceChildren();

// إزالة العناصر بشروط
const errors = document.querySelectorAll('.error-message');
errors.forEach(function(error) {
    if (error.textContent.trim() === '') {
        error.remove(); // إزالة حاويات الخطأ الفارغة
    }
});
خطأ شائع: بعد استدعاء remove() على عنصر، المتغير في JavaScript لا يزال يحتفظ بمرجع للعقدة المزالة. العنصر اختفى من الصفحة، لكنه لا يزال موجودا في الذاكرة. يمكنك حتى إعادة إدراجه في DOM لاحقا باستخدام appendChild() أو append(). إذا كنت تريد حقا التخلص من العنصر، عين المتغير إلى null للسماح بجمع النفايات.

cloneNode -- استنساخ العناصر

طريقة cloneNode() تنشئ نسخة من عنصر. تأخذ وسيطة منطقية واحدة: true لـ استنساخ عميق (نسخ العنصر وجميع أحفاده) أو false لـ استنساخ سطحي (نسخ العنصر نفسه فقط بدون أبنائه). الاستنساخ مفيد عندما تريد تكرار عنصر قالب عدة مرات.

مثال: استنساخ العناصر

// العنصر الأصلي:
// <div class="card" id="template-card">
//   <h3 class="card-title">العنوان</h3>
//   <p class="card-body">نص المحتوى</p>
// </div>

const originalCard = document.getElementById('template-card');

// استنساخ سطحي -- فقط div الخارجي، بدون أبناء
const shallowCopy = originalCard.cloneNode(false);
console.log(shallowCopy.innerHTML); // "" (فارغ، بدون أبناء)
console.log(shallowCopy.className); // "card" (السمات تم نسخها)

// استنساخ عميق -- div وجميع أبنائه
const deepCopy = originalCard.cloneNode(true);
console.log(deepCopy.innerHTML);
// <h3 class="card-title">العنوان</h3><p class="card-body">نص المحتوى</p>

// مهم: أزل المعرف لتجنب التكرارات!
deepCopy.removeAttribute('id');

// تخصيص النسخة
deepCopy.querySelector('.card-title').textContent = 'بطاقة جديدة';
deepCopy.querySelector('.card-body').textContent = 'محتوى جديد هنا.';

// إضافة النسخة إلى الصفحة
document.getElementById('card-container').appendChild(deepCopy);

// إنشاء بطاقات متعددة من قالب
const cardData = [
    { title: 'بطاقة 1', body: 'محتوى البطاقة الأولى' },
    { title: 'بطاقة 2', body: 'محتوى البطاقة الثانية' },
    { title: 'بطاقة 3', body: 'محتوى البطاقة الثالثة' },
];

cardData.forEach(function(data) {
    const card = originalCard.cloneNode(true);
    card.removeAttribute('id');
    card.querySelector('.card-title').textContent = data.title;
    card.querySelector('.card-body').textContent = data.body;
    document.getElementById('card-container').appendChild(card);
});
نصيحة احترافية: عند استنساخ العناصر، تذكر أن cloneNode() تنسخ سمات العنصر (بما في ذلك id) لكنها لا تنسخ مستمعي الأحداث المضافين بـ addEventListener(). معالجات الأحداث المضمنة في سمات HTML (مثل onclick="...") يتم نسخها لأنها سمات. أزل دائما أو غير id على العناصر المستنسخة لتجنب وجود معرفات مكررة في مستندك.

innerHTML مقابل textContent مقابل innerText

هذه الخصائص الثلاث جميعها تتعامل مع محتوى العنصر، لكنها تتصرف بشكل مختلف جدا. فهم متى تستخدم كل واحدة أمر حاسم لكل من الوظيفية والأمان.

innerHTML تحصل على أو تعين ترميز HTML داخل العنصر. تحلل أي وسوم HTML في السلسلة النصية وتنشئ عناصر DOM المقابلة. هذا قوي لكنه يحمل مخاطر أمنية إذا أدرجت بيانات المستخدم بدون تنقية.

textContent تحصل على أو تعين المحتوى النصي الخام للعنصر وجميع أحفاده. لا تحلل HTML -- أي وسوم في السلسلة تعامل كنص حرفي. تعيد نص العناصر المخفية أيضا، وهي الخيار الأكثر أمانا لإدراج بيانات المستخدم.

innerText تشبه textContent لكنها مدركة لتنسيق CSS. تعيد فقط النص المعروض مرئيا (تتخطى العناصر المخفية)، وتحترم خصائص CSS مثل text-transform. الوصول إلى innerText يثير إعادة تدفق، مما يجعلها أبطأ من textContent.

مثال: مقارنة innerHTML و textContent و innerText

// بالنظر إلى هذا HTML:
// <div id="example">
//   <p>مرحبا <strong>بالعالم</strong></p>
//   <p style="display: none;">نص مخفي</p>
// </div>

const el = document.getElementById('example');

// innerHTML -- تعيد ترميز HTML الخام
console.log(el.innerHTML);
// '<p>مرحبا <strong>بالعالم</strong></p><p style="display: none;">نص مخفي</p>'

// textContent -- تعيد كل النص، بما في ذلك العناصر المخفية
console.log(el.textContent);
// 'مرحبا بالعالم\nنص مخفي'

// innerText -- تعيد النص المرئي فقط
console.log(el.innerText);
// 'مرحبا بالعالم' (الفقرة المخفية مستبعدة)

// تعيين المحتوى بـ innerHTML (يحلل HTML)
el.innerHTML = '<h2>عنوان جديد</h2><p>فقرة جديدة</p>';
// الـ div يحتوي الآن على h2 وعنصر p

// تعيين المحتوى بـ textContent (يهرب HTML -- آمن)
el.textContent = '<h2>هذا ليس عنوانا</h2>';
// الـ div يحتوي الآن على النص الحرفي "<h2>هذا ليس عنوانا</h2>"
// الوسوم مرئية كنص، غير محللة كـ HTML

// الأمان: لا تستخدم أبدا innerHTML مع مدخلات المستخدم!
const userInput = '<script>alert("اختراق!")</script>';
el.innerHTML = userInput; // خطير -- السكريبت قد ينفذ!
el.textContent = userInput; // آمن -- يعرض كنص عادي
تحذير أمني: لا تستخدم أبدا innerHTML لإدراج محتوى مقدم من المستخدم مباشرة في الصفحة. هذا ينشئ ثغرة Cross-Site Scripting (XSS) حيث يمكن للمهاجمين حقن سكريبتات خبيثة. استخدم دائما textContent للنص الذي ينشئه المستخدم، أو نقِّ محتوى HTML قبل إدراجه بـ innerHTML.

تعيين السمات: setAttribute و dataset

العناصر لها سمات مثل id و class و href و src وسمات data-* المخصصة. يوفر JavaScript عدة طرق لقراءة وتعديل هذه السمات. طريقتا setAttribute() و getAttribute() تعملان مع أي سمة، بينما خاصية dataset توفر واجهة مريحة خصيصا لسمات data-*.

مثال: العمل مع السمات

const link = document.createElement('a');

// setAttribute -- تعين أي سمة
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
link.setAttribute('class', 'external-link');
link.setAttribute('id', 'my-link');

// getAttribute -- تقرأ قيمة سمة
console.log(link.getAttribute('href')); // "https://example.com"
console.log(link.getAttribute('target')); // "_blank"

// hasAttribute -- تتحقق من وجود سمة
console.log(link.hasAttribute('target')); // true
console.log(link.hasAttribute('download')); // false

// removeAttribute -- تزيل سمة
link.removeAttribute('target');
console.log(link.hasAttribute('target')); // false

// الوصول المباشر بالخاصية (يعمل مع السمات القياسية)
link.href = 'https://newsite.com';
link.id = 'updated-link';
console.log(link.href); // "https://newsite.com"

// dataset -- الطريقة السهلة للعمل مع سمات data-*
const card = document.createElement('div');
card.setAttribute('data-user-id', '42');
card.setAttribute('data-role', 'admin');
card.setAttribute('data-is-active', 'true');

// القراءة بـ dataset (لاحظ: التحويل لـ camelCase)
console.log(card.dataset.userId); // "42" (data-user-id تصبح userId)
console.log(card.dataset.role); // "admin"
console.log(card.dataset.isActive); // "true"

// التعيين بـ dataset
card.dataset.score = '100'; // تنشئ data-score="100"
card.dataset.lastLogin = '2024-01-15'; // تنشئ data-last-login="2024-01-15"

// حذف سمة بيانات
delete card.dataset.isActive; // تزيل data-is-active
ملاحظة: خاصية dataset تحول تلقائيا بين تنسيق HTML (data-user-id بشرطات) وتنسيق JavaScript (userId بـ camelCase). عندما تعين card.dataset.firstName = 'أحمد'، تصبح سمة HTML data-first-name="أحمد". جميع قيم dataset هي سلاسل نصية، لذا إذا خزنت أرقاما أو قيما منطقية، ستحتاج لتحويلها عند القراءة.

تعديل الأنماط: خاصية style

كل عنصر DOM له خاصية style تتيح لك تعيين أنماط CSS مضمنة مباشرة من JavaScript. أسماء خصائص CSS تتحول إلى camelCase في JavaScript: background-color تصبح backgroundColor و font-size تصبح fontSize وهكذا. تعيين نمط بهذه الطريقة يضيفه كنمط مضمن على العنصر، وهو ذو خصوصية عالية ويتجاوز معظم قواعد أوراق الأنماط.

مثال: تعديل الأنماط المضمنة

const box = document.getElementById('my-box');

// تعيين أنماط فردية
box.style.backgroundColor = '#3498db';
box.style.color = 'white';
box.style.padding = '20px';
box.style.borderRadius = '8px';
box.style.fontSize = '18px';
box.style.fontWeight = 'bold';
box.style.marginTop = '10px';
box.style.display = 'flex';
box.style.justifyContent = 'center';
box.style.alignItems = 'center';

// قراءة نمط محسوب (ما يعرضه المتصفح فعليا)
const computedStyles = window.getComputedStyle(box);
console.log(computedStyles.backgroundColor); // "rgb(52, 152, 219)"
console.log(computedStyles.fontSize); // "18px"

// إزالة نمط مضمن بتعيينه لسلسلة فارغة
box.style.backgroundColor = '';
// العنصر يعود لما تحدده ورقة الأنماط

// تعيين أنماط متعددة مرة واحدة باستخدام cssText
box.style.cssText = 'background: red; color: white; padding: 10px;';
// تحذير: cssText تستبدل جميع الأنماط المضمنة الحالية!

// طريقة أفضل: استخدام Object.assign لأنماط متعددة
Object.assign(box.style, {
    background: '#2ecc71',
    color: 'white',
    padding: '15px',
    borderRadius: '5px'
});

classList -- إدارة فئات CSS

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

مثال: استخدام طرق classList

const button = document.querySelector('.action-btn');

// add() -- تضيف فئة أو أكثر
button.classList.add('active');
button.classList.add('primary', 'large'); // فئات متعددة مرة واحدة

// remove() -- تزيل فئة أو أكثر
button.classList.remove('inactive');
button.classList.remove('small', 'outlined'); // متعددة مرة واحدة

// toggle() -- تضيف إذا كانت مفقودة، تزيل إذا كانت موجودة
button.classList.toggle('active');
// إذا كان الزر يملك "active"، تتم إزالتها. وإلا تتم إضافتها.

// toggle() مع معامل القوة
button.classList.toggle('active', true); // تضيف دائما (مثل add)
button.classList.toggle('active', false); // تزيل دائما (مثل remove)

// contains() -- تتحقق من وجود فئة (تعيد true/false)
if (button.classList.contains('active')) {
    console.log('الزر نشط');
} else {
    console.log('الزر غير نشط');
}

// replace() -- تستبدل فئة بأخرى
button.classList.replace('primary', 'secondary');
// تزيل "primary" وتضيف "secondary" في خطوة واحدة

// عملي: تبديل الوضع المظلم
const body = document.body;
const themeToggle = document.getElementById('theme-toggle');

themeToggle.addEventListener('click', function() {
    body.classList.toggle('dark-mode');

    // تحديث نص الزر بناء على الحالة الحالية
    if (body.classList.contains('dark-mode')) {
        themeToggle.textContent = 'التبديل إلى الوضع الفاتح';
    } else {
        themeToggle.textContent = 'التبديل إلى الوضع المظلم';
    }
});

// عملي: تمييز التنقل النشط
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(function(link) {
    link.addEventListener('click', function() {
        // إزالة active من جميع الروابط
        navLinks.forEach(function(l) {
            l.classList.remove('active');
        });
        // إضافة active للرابط الذي تم النقر عليه
        this.classList.add('active');
    });
});
نصيحة احترافية: فضل دائما classList على تعديل خاصية className مباشرة. تعيين element.className = 'newClass' يستبدل جميع الفئات الحالية، مما قد يزيل عرضيا فئات أضافتها أجزاء أخرى من كودك. مع classList.add() و classList.remove()، أنت تؤثر فقط على الفئات المحددة التي تنوي تغييرها.

insertAdjacentHTML -- إدراج سلاسل HTML

طريقة insertAdjacentHTML() تحلل سلسلة HTML وتدرج العقد الناتجة في موضع محدد نسبة للعنصر. إنها أسرع من innerHTML لإضافة المحتوى لأنها لا تعيد تحليل المحتوى الحالي للعنصر. تأخذ وسيطتين: سلسلة الموضع وسلسلة HTML المراد إدراجها.

القيم الأربع للموضع هي:

  • 'beforebegin' -- قبل العنصر نفسه (كأخ سابق)
  • 'afterbegin' -- داخل العنصر، قبل ابنه الأول
  • 'beforeend' -- داخل العنصر، بعد آخر ابن
  • 'afterend' -- بعد العنصر نفسه (كأخ تالٍ)

مثال: استخدام insertAdjacentHTML

const container = document.getElementById('content');

// الإدراج قبل العنصر (كأخ)
container.insertAdjacentHTML('beforebegin',
    '<div class="alert">إشعار: تم تحديث المحتوى!</div>'
);

// الإدراج في بداية محتوى العنصر
container.insertAdjacentHTML('afterbegin',
    '<h2>آخر التحديثات</h2>'
);

// الإدراج في نهاية محتوى العنصر
container.insertAdjacentHTML('beforeend',
    '<p>آخر تحديث: يناير 2024</p>'
);

// الإدراج بعد العنصر (كأخ)
container.insertAdjacentHTML('afterend',
    '<footer>نهاية المحتوى</footer>'
);

// عملي: إضافة صف جديد لجدول
const tableBody = document.querySelector('#data-table tbody');
tableBody.insertAdjacentHTML('beforeend',
    '<tr>' +
    '<td>أحمد محمد</td>' +
    '<td>ahmed@example.com</td>' +
    '<td>مدير</td>' +
    '</tr>'
);

// بناء نظام إشعارات
function showNotification(message, type) {
    const html = '<div class="notification ' + type + '">' +
                 '<span class="notification-text">' + message + '</span>' +
                 '<button class="notification-close">X</button>' +
                 '</div>';

    document.getElementById('notification-area')
            .insertAdjacentHTML('beforeend', html);
}

DocumentFragment -- عمليات DOM المجمعة

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

مثال: استخدام DocumentFragment للإدراج المجمع

// سيئ: إلحاق العناصر واحدة تلو الأخرى (يسبب إعادة تدفق عند كل إلحاق)
const list = document.getElementById('user-list');
const users = ['علي', 'سارة', 'محمد', 'فاطمة', 'عمر',
               'خالد', 'نورة', 'يوسف', 'مريم', 'أحمد'];

// هذا يثير 10 عمليات إعادة تدفق منفصلة!
users.forEach(function(user) {
    const li = document.createElement('li');
    li.textContent = user;
    list.appendChild(li); // إعادة تدفق تحدث هنا كل مرة
});

// جيد: استخدام DocumentFragment (إعادة تدفق واحدة فقط)
const fragment = document.createDocumentFragment();

users.forEach(function(user) {
    const li = document.createElement('li');
    li.textContent = user;
    li.classList.add('user-item');
    fragment.appendChild(li); // لا إعادة تدفق -- الجزء ليس في DOM
});

// إلحاق واحد يثير إعادة تدفق واحدة فقط
list.appendChild(fragment);
// الجزء نفسه يختفي؛ فقط أبناؤه تتم إضافتهم

// عملي: بناء جدول بيانات من مصفوفة
function buildTable(data) {
    const fragment = document.createDocumentFragment();

    data.forEach(function(row) {
        const tr = document.createElement('tr');

        const tdName = document.createElement('td');
        tdName.textContent = row.name;
        tr.appendChild(tdName);

        const tdEmail = document.createElement('td');
        tdEmail.textContent = row.email;
        tr.appendChild(tdEmail);

        const tdRole = document.createElement('td');
        tdRole.textContent = row.role;
        tr.appendChild(tdRole);

        fragment.appendChild(tr);
    });

    document.querySelector('#data-table tbody').appendChild(fragment);
}
ملاحظة: عندما تلحق DocumentFragment بـ DOM، لا يتم إدراج الجزء نفسه -- فقط عقده الفرعية. بعد الإلحاق، يصبح الجزء فارغا. هذا بالتصميم: الجزء هو حاوية مؤقتة، وليس جزءا دائما من المستند. المتصفحات الحديثة تحسن أيضا append() مع وسائط متعددة لتجميع العمليات بشكل مشابه، لكن DocumentFragment يبقى النمط القياسي للإدراجات واسعة النطاق.

مثال واقعي: تطبيق قائمة مهام ديناميكي

لنجمع كل ما تعلمناه في تطبيق قائمة مهام كامل وعملي. هذا المثال يستخدم createElement و classList و dataset و insertAdjacentHTML وتفويض الأحداث وإزالة العناصر -- جميع تقنيات التعامل مع DOM الأساسية تعمل معا.

مثال: قائمة مهام كاملة مع التعامل مع DOM

// بنية HTML:
// <div id="todo-app">
//   <h2>قائمة مهامي</h2>
//   <form id="todo-form">
//     <input type="text" id="todo-input" placeholder="أضف مهمة جديدة...">
//     <button type="submit">إضافة</button>
//   </form>
//   <ul id="todo-list"></ul>
//   <p id="todo-count"></p>
// </div>

// تخزين مراجع DOM مؤقتا
const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');
const todoCount = document.getElementById('todo-count');
let taskId = 0;

// دالة لإنشاء عنصر مهمة جديد
function createTodoItem(text) {
    taskId++;

    const li = document.createElement('li');
    li.classList.add('todo-item');
    li.dataset.id = taskId;

    const checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    checkbox.classList.add('todo-checkbox');

    const span = document.createElement('span');
    span.classList.add('todo-text');
    span.textContent = text;

    const deleteBtn = document.createElement('button');
    deleteBtn.classList.add('todo-delete');
    deleteBtn.textContent = 'حذف';
    deleteBtn.setAttribute('aria-label', 'حذف المهمة: ' + text);

    li.append(checkbox, span, deleteBtn);
    return li;
}

// دالة لتحديث عدد المهام
function updateCount() {
    const total = todoList.children.length;
    const completed = todoList.querySelectorAll('.completed').length;
    todoCount.textContent = completed + ' من ' + total + ' مهام مكتملة';
}

// معالجة إرسال النموذج
todoForm.addEventListener('submit', function(event) {
    event.preventDefault();
    const text = todoInput.value.trim();

    if (text === '') return;

    const item = createTodoItem(text);
    todoList.appendChild(item);

    todoInput.value = '';
    todoInput.focus();
    updateCount();
});

// معالجة النقرات باستخدام تفويض الأحداث على القائمة
todoList.addEventListener('click', function(event) {
    const todoItem = event.target.closest('.todo-item');
    if (!todoItem) return;

    // معالجة تبديل مربع الاختيار
    if (event.target.classList.contains('todo-checkbox')) {
        todoItem.classList.toggle('completed');
        updateCount();
    }

    // معالجة زر الحذف
    if (event.target.classList.contains('todo-delete')) {
        todoItem.remove();
        updateCount();
    }
});

مثال واقعي: شبكة بطاقات ديناميكية

إليك مثالا عمليا آخر يوضح إنشاء تخطيط بطاقات ديناميكي من البيانات. هذا النمط شائع في مواقع التجارة الإلكترونية ولوحات المعلومات وأنظمة إدارة المحتوى حيث تحتاج لعرض مجموعة من العناصر من بيانات جلبت من واجهة برمجة التطبيقات.

مثال: عرض شبكة بطاقات من البيانات

// بيانات منتجات نموذجية (محاكاة استجابة API)
const products = [
    { id: 1, name: 'سماعات لاسلكية', price: 79.99,
      category: 'إلكترونيات', inStock: true },
    { id: 2, name: 'حذاء رياضي', price: 129.99,
      category: 'رياضة', inStock: true },
    { id: 3, name: 'صانعة قهوة', price: 49.99,
      category: 'مطبخ', inStock: false },
    { id: 4, name: 'سجادة يوغا', price: 29.99,
      category: 'رياضة', inStock: true },
];

function createProductCard(product) {
    const card = document.createElement('div');
    card.classList.add('product-card');
    card.dataset.productId = product.id;
    card.dataset.category = product.category;

    if (!product.inStock) {
        card.classList.add('out-of-stock');
    }

    const title = document.createElement('h3');
    title.classList.add('product-title');
    title.textContent = product.name;

    const price = document.createElement('p');
    price.classList.add('product-price');
    price.textContent = '$' + product.price.toFixed(2);

    const badge = document.createElement('span');
    badge.classList.add('stock-badge');
    badge.textContent = product.inStock ? 'متوفر' : 'غير متوفر';
    badge.classList.add(product.inStock ? 'badge-success' : 'badge-danger');

    const button = document.createElement('button');
    button.classList.add('add-to-cart');
    button.textContent = 'أضف للسلة';
    if (!product.inStock) {
        button.disabled = true;
        button.textContent = 'غير متاح';
    }

    card.append(title, price, badge, button);
    return card;
}

// عرض جميع المنتجات باستخدام DocumentFragment
function renderProducts(productList) {
    const container = document.getElementById('product-grid');
    container.replaceChildren(); // مسح المحتوى الحالي

    const fragment = document.createDocumentFragment();
    productList.forEach(function(product) {
        fragment.appendChild(createProductCard(product));
    });
    container.appendChild(fragment);
}

// العرض الأولي
renderProducts(products);

// التصفية حسب الفئة
function filterByCategory(category) {
    if (category === 'all') {
        renderProducts(products);
    } else {
        const filtered = products.filter(function(p) {
            return p.category === category;
        });
        renderProducts(filtered);
    }
}

ملخص طرق التعامل مع DOM

إليك مرجعا شاملا لجميع طرق التعامل مع DOM التي تم تغطيتها في هذا الدرس:

  • document.createElement(tag) -- تنشئ عنصرا جديدا في الذاكرة.
  • document.createTextNode(text) -- تنشئ عقدة نصية جديدة.
  • parent.appendChild(node) -- تلحق عقدة كآخر ابن.
  • parent.insertBefore(newNode, refNode) -- تدرج عقدة قبل عقدة مرجعية.
  • element.append(...nodes) -- تلحق عقدا أو سلاسل نصية متعددة في النهاية.
  • element.prepend(...nodes) -- تدرج عقدا أو سلاسل نصية متعددة في البداية.
  • element.after(...nodes) -- تدرج أشقاء بعد العنصر.
  • element.before(...nodes) -- تدرج أشقاء قبل العنصر.
  • parent.replaceChild(newNode, oldNode) -- تستبدل ابنا بآخر.
  • element.replaceWith(...nodes) -- تستبدل العنصر بعقد جديدة.
  • parent.removeChild(node) -- تزيل عقدة ابن من الأبوي.
  • element.remove() -- تزيل العنصر من DOM.
  • element.cloneNode(deep) -- تنشئ نسخة سطحية أو عميقة من عنصر.
  • element.innerHTML -- تحصل على أو تعين محتوى HTML (استخدم بحذر).
  • element.textContent -- تحصل على أو تعين المحتوى النصي (آمن لبيانات المستخدم).
  • element.setAttribute(name, value) -- تعين سمة على العنصر.
  • element.dataset -- الوصول وتعديل سمات data-*.
  • element.style -- قراءة وتعيين أنماط CSS المضمنة.
  • element.classList -- إضافة وإزالة وتبديل والتحقق من فئات CSS.
  • element.insertAdjacentHTML(position, html) -- إدراج HTML محلل في موضع محدد.
  • document.createDocumentFragment() -- إنشاء حاوية خفيفة لعمليات DOM المجمعة.

تمرين عملي

ابنِ تطبيق قائمة جهات اتصال ديناميكي باستخدام التعامل مع DOM فقط بـ JavaScript. أنشئ صفحة HTML تحتوي على نموذج بحقول إدخال للاسم والبريد الإلكتروني ورقم الهاتف، بالإضافة إلى زر إضافة جهة اتصال. أسفل النموذج، أضف جدولا فارغا برؤوس (الاسم، البريد الإلكتروني، الهاتف، الإجراءات). اكتب JavaScript ينجز ما يلي: (1) عند إرسال النموذج، أنشئ صف جدول جديد باستخدام createElement واملأ كل خلية ببيانات النموذج باستخدام textContent. (2) أضف زر حذف في عمود الإجراءات يزيل الصف عند النقر باستخدام remove(). (3) أضف زر تعديل يستبدل خلايا النص بحقول إدخال معبأة مسبقا بالقيم الحالية، ويغير زر التعديل إلى زر حفظ. عند النقر على حفظ، حدث الخلايا بالقيم الجديدة. (4) استخدم classList لإضافة فئة تمييز عند التمرير فوق الصفوف. (5) استخدم DocumentFragment لإضافة خمس جهات اتصال نموذجية عند تحميل الصفحة. (6) أضف حقل بحث فوق الجدول يصفي الصفوف المرئية بتعيين style.display بناء على ما إذا كان الاسم يحتوي على نص البحث. (7) استخدم سمات dataset لتخزين معرف فريد على كل صف. اختبر جميع الميزات وتحقق من أن الإضافة والتعديل والحذف والبحث كلها تعمل بشكل صحيح.