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

المهمة محددة: إعادة بناء محتويات عرض JavaScript مع إبقاء أزراره مستجيبة والاختيار محفوظًا. تحتاج إلى معرفة أساسية بـ JavaScript ومتصفح Safari حديث. يستخدم المثال واجهات برمجة التطبيقات القياسية للمتصفح ولا يتطلب إطار عمل أو تثبيت حزمة.

لماذا تتوقف النقرات عن العمل بعد استبدال DOM؟

ينتمي المستمع المسجل باستخدام addEventListener() إلى هدف الحدث الذي سُجل عليه. يؤدي إسناد قيمة إلى innerHTML لحاوية ما إلى تحليل الترميز المقدم واستبدال العناصر المتفرعة منها. لذلك، لا يرث الزر المنشأ حديثًا مستمعًا أُرفق بالزر السابق، حتى إن تطابق معرّفه وتسميته. راجع مرجع innerHTML على MDN ونموذج مستمعي الأحداث في معيار DOM.

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

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

أين ينبغي أن يكون المستمع؟

ابدأ بتحديد حدود DOM الدقيقة التي تستبدلها عملية العرض. ثم اختر أضيق عنصر أب مناسب يقع خارج هذه الحدود. يطبق دليل القرار التالي سلوك الاستبدال والمستمع الموثق على أنماط التحديث الشائعة. MDN: replaceChildren()، معيار DOM: EventTarget.

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

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

أنشئ عرضًا كاملًا آمنًا عند الاستبدال في Safari

أولًا، جهّز علامة تبويب متصفح يمكن التخلص منها. في Safari على Mac، افتح Settings…، واختر Advanced، وفعّل Show features for web developers. افتح Web Inspector من Develop → Show Web Inspector، أو اضغط Option-Command-I. هذه هي الخطوات التي يوثقها دليل WebKit لتمكين Web Inspector.

انتقل إلى about:blank، وافتح Web Inspector، واختر Console. الصق البرنامج الكامل أدناه في موجه الأوامر ونفّذه. توضح WebKit أن موجه Console ينفّذ JavaScript في سياق التنفيذ المحدد للصفحة التي يجري فحصها. استخدم علامة التبويب الفارغة لإبقاء العرض التوضيحي منفصلًا عن تطبيقك.

هذا برنامج متصفح كامل: ينشئ DOM الخاص به، ويعرّف بياناته النموذجية، ويحمّل العرض، ويوفر دالة للتنظيف. ويستبدل العناصر الفرعية للعرض باستخدام replaceChildren()، التي تقبل كائنات العقد وتستبدل مجموعة العناصر الفرعية الموجودة. يصف مرجع الطريقة على MDN هذه العملية.

(() => {
  window.domClickDemo?.destroy();

  const root = document.createElement("section");
  root.id = "dom-click-demo";
  const controller = new AbortController();
  const items = [
    { id: "a", label: "Tile A" },
    { id: "b", label: "Tile B" },
    { id: "c", label: "Tile C" }
  ];
  const state = { selected: "None", refreshes: 0 };

  function makeButton(action, label, id) {
    const button = document.createElement("button");
    button.type = "button";
    button.dataset.action = action;
    if (id !== undefined) button.dataset.id = id;
    const caption = document.createElement("span");
    caption.textContent = label;
    button.append(caption);
    return button;
  }

  function render() {
    const heading = document.createElement("h2");
    heading.textContent = "Tile picker";
    const status = document.createElement("p");
    status.textContent =
      `Selected: ${state.selected}; refreshes: ${state.refreshes}`;
    const list = document.createElement("ul");
    for (const item of items) {
      const row = document.createElement("li");
      row.append(makeButton("select", item.label, item.id));
      list.append(row);
    }
    const refresh = makeButton("refresh", "Replace view content");
    root.replaceChildren(heading, status, list, refresh);
  }

  function handleClick(event) {
    if (!(event.target instanceof Element)) return;
    const button = event.target.closest("button[data-action]");
    if (!button || !root.contains(button) || button.disabled) return;

    if (button.dataset.action === "select") {
      const item = items.find(item => item.id === button.dataset.id);
      if (!item) return;
      state.selected = item.label;
    } else if (button.dataset.action === "refresh") {
      state.refreshes += 1;
    } else {
      return;
    }
    render();
  }

  root.addEventListener("click", handleClick, {
    signal: controller.signal
  });
  render();
  document.body.append(root);

  window.domClickDemo = {
    root,
    destroy() {
      controller.abort();
      root.remove();
    }
  };
})();

يُسجَّل المستمع خارج render(). تنشئ كل عملية اختيار وتحديث أزرارًا جديدة، لكن جميع هذه الأزرار تظل ضمن الجذر الأصلي. ويظل كائن الحالة أيضًا خارج الشجرة الفرعية المعروضة، لذا فإن استبدال فقرة الحالة لا يعيد تعيين المربع المحدد أو عدد مرات التحديث.

وجود التسمية المتداخلة مقصود. قد يستهدف النقر على نص الزر عنصره الفرعي. تبحث closest("button[data-action]") في الهدف وأسلافه عن عنصر التحكم المطابق، ما يجنبك معالجًا لا يعمل إلا عندما يكون الزر نفسه هدف الحدث. يوثق مرجع closest() على MDN هذا البحث والنتيجة null عند عدم العثور على تطابق.

يضمن فحص الاحتواء بقاء عنصر التحكم المطابق داخل حدود هذا العرض. ويعرّف معيار DOM الدالة contains() بالتحقق مما إذا كانت العقدة المقدمة هي العقدة نفسها أو إحدى العقد المتفرعة منها. معيار DOM: Node.contains().

أبقِ التهيئة والعرض والتنظيف منفصلة

يتضمن هذا المثال ثلاث مسؤوليات واضحة. تنشئ التهيئة الجذر وتسجل مستمعه. ويحدّث العرض العناصر الفرعية للجذر انطلاقًا من الحالة. ويزيل التنظيف المستمع والجذر. هذا التقسيم هو عقد دورة الحياة للتنفيذ؛ حافظ على المسؤوليات المكافئة عند تكييفه مع تطبيقك.

يتلقى المستمع AbortSignal، وتستدعي destroy() دالة الإلغاء الخاصة به. يؤدي الإلغاء إلى إزالة التسجيل المرتبط، كما هو موثق في مرجع addEventListener() على MDN. وعند تشغيل البرنامج الكامل مجددًا، ينظف أولًا العرض التوضيحي السابق، ما يمنع تراكم عروض توضيحية متعددة.

لا تضع مستمعًا مجهولًا يُنشأ حديثًا داخل كل عملية عرض. فكل تعبير دالة مجهول متكرر ينشئ كائن دالة مستقلًا، لذا قد تتراكم تسجيلاتها على جذر يبقى موجودًا. يؤدي استخدام رد النداء نفسه ونوع الحدث وإعداد الالتقاط إلى تجنب التسجيل المكرر، لكن إبقاء الربط ضمن التهيئة يوضح الملكية أكثر. MDN: addEventListener()، معيار DOM: إضافة المستمعين.

تحقّق من تكرار الاستبدال والاختيار والتنظيف

استخدم التسلسل التالي لاختبار المثال. تستند النتائج المتوقعة إلى تحديثات الحالة الصريحة في البرنامج؛ وهي ليست ادعاءً بإجراء اختبار مكتمل على Safari.

  1. انقر على Tile B. ينبغي أن تعرض الحالة Selected: Tile B; refreshes: 0.
  2. انقر على Replace view content ثلاث مرات. ينبغي أن تعرض الحالة Selected: Tile B; refreshes: 3.
  3. انقر على نص Tile C. ينبغي أن تتغير الحالة إلى Selected: Tile C; refreshes: 3.
  4. انقر على العنوان أو فقرة الحالة. ينبغي ألا تتغير أي من القيمتين.
  5. نفّذ window.domClickDemo.destroy() في Console. ينبغي أن يختفي العرض التوضيحي.
  6. نفّذ البرنامج الكامل مرة أخرى. ينبغي أن يظهر عرض جديد واحد بالحالة Selected: None; refreshes: 0.

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

تم التحقق من صياغة JavaScript في البرنامج باستخدام Node، وأكد فحص منفصل وحتمي لـ EventTarget إزالة ازدواجية رد النداء وإزالته عبر إشارة إلغاء. لم يُنفّذ عرض DOM في المتصفح أو التفاعل مع Safari ضمن ذلك الفحص؛ استخدم التسلسل أعلاه للتحقق منهما على Mac.

ماذا لو ظل المعالج لا يعمل في تطبيقك؟

تحرّ عن أول نقطة يختلف عندها السلوك. أبقِ نطاق المثال صغيرًا أثناء فحص حدود الاستبدال ومسار الحدث وتحديثات الحالة في تطبيقك.

العَرَض ما الذي ينبغي فحصه أولًا؟ التصحيح المناسب
تعمل النقرات حتى يُحدَّث عرض خارجي هل استُبدل جذر المستمع أيضًا؟ اربط الجذر الجديد أثناء التهيئة أو اختر عنصرًا أبًا يبقى موجودًا
يصل النقر، لكن لا يُنفَّذ أي إجراء هل لا يزال عنصر التحكم الجديد يطابق المحدد وقيمة الإجراء؟ حافظ على عقد المحدد والإجراء لعنصر التحكم
يختلف السلوك عند النقر على نص الزر هل يتحقق المعالج من الهدف المطابق تمامًا فقط؟ حدّد عنصر التحكم باستخدام closest()
يحدّث نقرة واحدة الحالة عدة مرات هل توجد تسجيلات متعددة أو معالجات مباشرة ومفوّضة نشطة معًا؟ اجعل مسار تهيئة واحدًا مسؤولًا عن الإجراء
يعمل المعالج، لكن تُعاد تهيئة الشاشة هل تُنشأ الحالة داخل عملية العرض؟ أبقِ الحالة المطلوبة خارج الشجرة الفرعية المستبدلة

افحص الانتشار أيضًا. فقد يمنع معالج في عنصر متفرع يستدعي stopPropagation() وصول الحدث المنتشر صعودًا إلى المستمع المفوّض. تشرح MDN كلاً من إيقاف الانتشار وتفويض الأحداث. راجع سبب إيقاف الانتشار قبل تغيير مراحل المستمعين، لأن تغيير المراحل يغيّر ترتيب التنفيذ أيضًا.

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

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

المصادر