{"id":"l-js-4-1","courseId":"javascript-fundamentals","lang":"ar","kind":"lesson","summary":"تفهم الـ DOM على أنه شجرة الصفحة الحيّة في المتصفّح، وتحدّد العناصر بمحدّدات CSS، وتغيّر نصوصها وأصنافها وسماتها، وتعرض قائمة انطلاقًا من البيانات.","html":"<p>كل ما فعله Pocket حتى الآن حدث في الـ console. لكن المستخدم الحقيقي لا يفتح الـ console أبدًا؛ إنه يرى صفحة. وباقي هذا القسم ينقل Pocket إلى تلك الصفحة، وأول خطوة أن تتعلّم كيف ترى JavaScript صفحة الويب أصلًا.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الـ DOM: الصفحة على شكل كائنات</h2>\n<p>حين يحمّل المتصفّح HTML، لا يحتفظ به نصًا. بل يبني شجرة من الكائنات، كائنًا لكل عنصر، ويرسم الصفحة من تلك الشجرة. هذه الشجرة هي <strong>الـ DOM</strong> (نموذج كائنات المستند، Document Object Model)، والكائن العام <code dir=\"ltr\">document</code> هو جذرها.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">index.html</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">body</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Pocket&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">h1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Total: $0.00&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ul</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"expense-list\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ul</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">script</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> src</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"app.js\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> defer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">script</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">body</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 230\" role=\"img\" aria-labelledby=\"l-js-4-1-t1\"><defs><marker id=\"arrow-l-js-4-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-js-4-1-t1\">شجرة الـ DOM لصفحة Pocket: يحتوي document على body، ويحتوي body على h1، وعلى p معرّفه total، وعلى ul معرّفه expense-list. تقرأ JavaScript هذه الكائنات وتغيّرها، فيعيد المتصفّح رسم الصفحة.</title>\n  <rect class=\"d-box-accent\" x=\"250\" y=\"14\" width=\"140\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"320\" y=\"39\" text-anchor=\"middle\">document</text>\n  <rect class=\"d-box\" x=\"260\" y=\"80\" width=\"120\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"320\" y=\"105\" text-anchor=\"middle\">body</text>\n  <rect class=\"d-box\" x=\"40\" y=\"160\" width=\"140\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"110\" y=\"187\" text-anchor=\"middle\">h1</text>\n  <rect class=\"d-box-primary\" x=\"250\" y=\"160\" width=\"140\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"320\" y=\"187\" text-anchor=\"middle\">p#total</text>\n  <rect class=\"d-box-primary\" x=\"460\" y=\"160\" width=\"160\" height=\"44\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"540\" y=\"187\" text-anchor=\"middle\">ul#expense-list</text>\n  <path class=\"d-line\" d=\"M320 54 L320 80\"/>\n  <path class=\"d-line\" d=\"M300 120 L110 160\"/>\n  <path class=\"d-line\" d=\"M320 120 L320 160\"/>\n  <path class=\"d-line\" d=\"M340 120 L540 160\"/>\n  <text class=\"d-label-muted\" x=\"540\" y=\"130\" text-anchor=\"middle\">JavaScript تغيّر هذه</text>\n</svg><figcaption>يحوّل المتصفّح HTML إلى شجرة من كائنات العناصر</figcaption></figure>\n<p>الـ DOM حيّ. غيّر كائنًا في الشجرة فيعيد المتصفّح رسم ذلك الجزء من الصفحة على الفور. وهذا كل ما يعنيه “تحديث الصفحة”.</p>\n<p>السمة <code dir=\"ltr\">defer</code> تخبر المتصفّح أن يشغّل <code dir=\"ltr\">app.js</code> بعد أن يبني الشجرة كلها. ودونها، يُنفَّذ السكربت الموجود في <code dir=\"ltr\">&lt;head&gt;</code> قبل أن يوجد الـ body، فلا يجد أي بحث شيئًا. أما السكربتات المحمّلة بـ <code dir=\"ltr\">type=&quot;module&quot;</code>، التي ستتعرّف عليها في القسم 5، فتُؤجَّل تلقائيًا.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">تحديد العناصر</h2>\n<p><code dir=\"ltr\">document.querySelector(selector)</code> تأخذ محدّد CSS، أي الصياغة نفسها التي تكتبها في ملف الأنماط، وتُرجع أول عنصر مطابق. و<code dir=\"ltr\">querySelectorAll</code> تُرجع كل العناصر المطابقة.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> totalEl</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);          </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// by id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> list</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#expense-list\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> firstItem</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#expense-list li\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> allItems</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelectorAll</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"li.big\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);      </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// every match</span></span></code></pre></div>\n<p><code dir=\"ltr\">querySelectorAll</code> تُرجع NodeList. لها <code dir=\"ltr\">forEach</code> وتعمل مع <code dir=\"ltr\">for...of</code>؛ وانشرها، <code dir=\"ltr\">[...allItems]</code>، حين تريد دوال المصفوفات مثل <code dir=\"ltr\">map</code> و<code dir=\"ltr\">filter</code>.</p>\n<p>حين لا يطابق شيء، تُرجع <code dir=\"ltr\">querySelector</code> القيمة <code dir=\"ltr\">null</code>، فيرمي السطر التالي الذي يستخدم النتيجة <code dir=\"ltr\">TypeError: Cannot set properties of null</code>. وهذا الخطأ يعني في الغالب خطأً إملائيًا في المحدّد أو سكربتًا نُفّذ مبكرًا جدًا.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">تغيير ما هو موجود</h2>\n<p>حين تحصل على عنصر، تصبح خصائصه أدوات التحكّم:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">totalEl.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"Total: $30.20\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;      </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// replace the text</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">totalEl.classList.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"over-budget\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);       </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// add a CSS class</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">totalEl.classList.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toggle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"highlight\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);      </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// add if absent, remove if present</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">totalEl.hidden </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;                     </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// show or hide</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">list.dataset.month </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"2026-10\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;             </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// sets data-month=\"2026-10\"</span></span></code></pre></div>\n<p><code dir=\"ltr\">textContent</code> تضبط نصًا عاديًا. وفضّل الأصناف (classes) على ضبط <code dir=\"ltr\">style</code> مباشرة: فملف CSS يقرّر شكل <code dir=\"ltr\">.over-budget</code>، وJavaScript تقرّر فقط <em>هل</em> يحمل العنصر هذا الصنف. وعندها يستطيع المصمّمون تغيير الشكل دون أن يمسّوا منطقك.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>innerHTML مع بيانات المستخدم</span></p>\n<p><code dir=\"ltr\">li.innerHTML = expense.label</code> تسلّم التسمية إلى محلّل HTML. فإن كتب مستخدم <code dir=\"ltr\">&lt;img src=x onerror=&quot;…&quot;&gt;</code> تسميةً، يُنفَّذ ذلك الكود في الصفحة. هذا يُسمّى البرمجة النصية عبر المواقع (XSS)، وهو من أكثر الأخطاء الأمنية شيوعًا على الويب. استخدم <code dir=\"ltr\">textContent</code> لكل ما جاء من البيانات أو من مستخدم. واحتفظ بـ <code dir=\"ltr\">innerHTML</code> للوسوم الثابتة التي كتبتها بنفسك.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">إنشاء العناصر وعرض قائمة</h2>\n<p>لتضيف أشياء جديدة إلى الصفحة، أنشئ العناصر، واملأها، ثم ألحقها بالصفحة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> li</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">createElement</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"li\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">li.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"Coffee: $4.50\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">li.dataset.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"exp-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">list.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">append</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(li);</span></span></code></pre></div>\n<p><code dir=\"ltr\">createElement</code> تصنع عنصرًا ليس على الصفحة بعد. و<code dir=\"ltr\">append</code> تُلحقه ابنًا أخيرًا لـ <code dir=\"ltr\">list</code>، وعندها فقط يظهر. أما <code dir=\"ltr\">element.remove()</code> فتُزيل عنصرًا من الصفحة.</p>\n<p>لدى Pocket مصفوفة مصاريف ويحتاج إلى <code dir=\"ltr\">&lt;li&gt;</code> لكل مصروف. وأنظف طريقة لإبقاء الصفحة والبيانات متّفقتين هي <strong>دالة render</strong> (دالة العرض): تُفرغ الحاوية، ثم تعيد بناءها من البيانات، في كل مرة تتغيّر فيها البيانات.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">expenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  list.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">replaceChildren</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// remove the old items</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> expense</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> li</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">createElement</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"li\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    li.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expense</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">label</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}: ${</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expense</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">amount</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    li.dataset.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expense.id;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (expense.amount </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 10000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      li.classList.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">add</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"big\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    list.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">append</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(li);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> total</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">reduce</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">sum</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sum </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> e.amount, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  totalEl.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `Total: ${</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">total</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>إعادة بناء كل شيء تبدو هدرًا، وهي كذلك مع آلاف العناصر. لكن مع عشرات العناصر في تطبيق شخصي، فهي سريعة، والأهم أنها بسيطة: هناك دالة واحدة فقط تقرّر شكل القائمة، ولا يمكن للصفحة أن تخرج عن تزامنها مع البيانات. وهذه الفكرة، أن الشاشة دالةٌ في البيانات، هي جوهر أطر العمل مثل React، التي تفعل الشيء نفسه بذكاء أكبر.</p>\n<p>والقاعدة التي تجعل هذا ينجح هي الانضباط في الاتجاه. البيانات تسير في اتجاه واحد: غيّر المصفوفة، ثم استدعِ <code dir=\"ltr\">renderExpenses</code>. لا تُصلح عنصر <code dir=\"ltr\">&lt;li&gt;</code> واحدًا يدويًا “هذه المرة فقط”، ولا تقرأ المصاريف من الصفحة لتحسب شيئًا؛ فالصفحة صورة للبيانات، لا البيانات نفسها. وفي اللحظة التي يصبح فيها مكانان قادرين على تغيير ما على الشاشة، سيختلفان حتمًا في يوم ما.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>استكشف أي صفحة في DevTools</span></p>\n<p>افتح DevTools في أي موقع، واختر عنصرًا في لوحة Elements، ثم اكتب <code dir=\"ltr\">$0</code> في الـ Console. <code dir=\"ltr\">$0</code> هو العنصر الذي اخترته، فتستطيع تجربة <code dir=\"ltr\">$0.textContent</code> أو <code dir=\"ltr\">$0.classList</code> على صفحات حقيقية.</p>\n</aside>\n<p>صارت الصفحة تعرض بيانات Pocket، لكنها لا تستطيع التفاعل مع المستخدم بعد. الدرس التالي: الأحداث.</p>\n","toc":[{"id":"sec-1","text":"الـ DOM: الصفحة على شكل كائنات","level":2},{"id":"sec-2","text":"تحديد العناصر","level":2},{"id":"sec-3","text":"تغيير ما هو موجود","level":2},{"id":"sec-4","text":"إنشاء العناصر وعرض قائمة","level":2}],"takeaways":["يحوّل المتصفّح كود HTML إلى الـ DOM، وهو شجرة من الكائنات؛ وتغيير هذه الكائنات بـ JavaScript يغيّر الصفحة فورًا.","<code dir=\"ltr\">querySelector</code> تُرجع أول عنصر يطابق محدّد CSS، أو <code dir=\"ltr\">null</code>؛ و<code dir=\"ltr\">querySelectorAll</code> تُرجع كل العناصر المطابقة.","اضبط النص باستخدام <code dir=\"ltr\">textContent</code> لا <code dir=\"ltr\">innerHTML</code> كلما جاء المحتوى من البيانات أو من المستخدمين، كي لا يُنفَّذ أبدًا على أنه HTML.","غيّر المظهر عبر <code dir=\"ltr\">classList</code>، واترك لـ CSS أن تقرّر شكل هذه الأصناف.","دالة <code dir=\"ltr\">render</code> تُفرغ الحاوية وتعيد بناءها من بياناتك تُبقي الصفحة والبيانات متزامنتين."],"further":[{"title":"Introduction to the DOM (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction"},{"title":"Document.querySelector() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector"},{"title":"Element.classList (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Element/classList"}],"quiz":[{"q":"<p>السطر <code dir=\"ltr\">document.querySelector(&quot;#totl&quot;).textContent = &quot;$0.00&quot;;</code> يرمي <code dir=\"ltr\">TypeError: Cannot set properties of null</code>. ما السبب الأرجح؟</p>","options":[{"text":"المحدّد لم يطابق شيئًا، فأرجعت <code dir=\"ltr\">querySelector</code> القيمة <code dir=\"ltr\">null</code>.","why":"صحيح. خطأ إملائي في الـ id (<code dir=\"ltr\">totl</code>)، أو تشغيل السكربت قبل وجود العنصر، كلاهما يُنتج <code dir=\"ltr\">null</code>."},{"text":"لا يمكن ضبط <code dir=\"ltr\">textContent</code> على الفقرات.","why":"يمكن ضبط <code dir=\"ltr\">textContent</code> على أي عنصر. والخطأ يقول إن ما قبل <code dir=\"ltr\">.textContent</code> كان <code dir=\"ltr\">null</code>."},{"text":"يجب إعادة تحميل الصفحة بعد تغيير الـ DOM.","why":"تغييرات الـ DOM تظهر فورًا. لا حاجة إلى إعادة التحميل ولا رغبة فيها."}],"answer":0},{"q":"<p>يكتب مستخدم <code dir=\"ltr\">&lt;img src=x onerror=alert(1)&gt;</code> تسميةً لمصروف. أيّ سطر يعرضها بأمان؟</p>","options":[{"text":"<code dir=\"ltr\">li.innerHTML = label;</code>","why":"<code dir=\"ltr\">innerHTML</code> تحلّل النص على أنه HTML، فيُنشأ وسم الصورة ويُنفَّذ كود <code dir=\"ltr\">onerror</code>. وهذه ثغرة XSS."},{"text":"<code dir=\"ltr\">li.textContent = label;</code>","why":"صحيح. <code dir=\"ltr\">textContent</code> تُدرج نصًا عاديًا، فيرى المستخدم الحروف التي كتبها ولا يُنفَّذ شيء."},{"text":"<code dir=\"ltr\">li.innerHTML = `&lt;span&gt;${label}&lt;/span&gt;`;</code>","why":"وضع التسمية داخل template literal ما زال يرسلها إلى محلّل HTML. والوسم الذي بداخلها يُنشأ مع ذلك."}],"answer":1},{"q":"<p>دالة <code dir=\"ltr\">render</code> لديك تضيف <code dir=\"ltr\">&lt;li&gt;</code> لكل مصروف. بعد إضافة مصروف رابع واستدعاء <code dir=\"ltr\">render</code> مجددًا، تعرض القائمة سبعة عناصر. ما الناقص؟</p>","options":[{"text":"إفراغ القائمة قبل إعادة بنائها، مثلًا باستخدام <code dir=\"ltr\">list.replaceChildren()</code>.","why":"صحيح. دون الإفراغ، يضيف كل عرض نسخة كاملة جديدة بعد العناصر القديمة."},{"text":"استدعاء <code dir=\"ltr\">querySelectorAll</code> بدلًا من <code dir=\"ltr\">querySelector</code>.","why":"الحاوية عنصر واحد، لذلك <code dir=\"ltr\">querySelector</code> صحيحة. المشكلة أن الأبناء القدامى لا يُزالون أبدًا."},{"text":"<code dir=\"ltr\">const</code> بدلًا من <code dir=\"ltr\">let</code> لمصفوفة المصاريف.","why":"طريقة التصريح بالمصفوفة لا تؤثّر في الـ DOM. والتكرار يأتي من عدم إزالة العناصر السابقة أبدًا."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<h1>Pocket</h1>\n<p id=\"total\">Total: $0.00</p>\n<ul id=\"expense-list\"></ul>\n","css":".big { font-weight: 700; color: #b42318; }\n","js":"const expenses = [\n  { id: \"exp-1\", label: \"Coffee\", amount: 450 },\n  { id: \"exp-2\", label: \"Flight\", amount: 18900 },\n  { id: \"exp-3\", label: \"Lunch\", amount: 1350 },\n];\n\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\n\nfunction renderExpenses(expenses) {\n  // 1. Clear #expense-list\n  // 2. Add one <li> per expense: \"Coffee: $4.50\", with data-id set to its id,\n  //    and the class \"big\" when the amount is 10000 or more\n  // 3. Set #total to \"Total: $207.00\" (the sum, formatted)\n}\n\nrenderExpenses(expenses);\n"},"solution":{"html":"<h1>Pocket</h1>\n<p id=\"total\">Total: $0.00</p>\n<ul id=\"expense-list\"></ul>\n","css":".big { font-weight: 700; color: #b42318; }\n","js":"const expenses = [\n  { id: \"exp-1\", label: \"Coffee\", amount: 450 },\n  { id: \"exp-2\", label: \"Flight\", amount: 18900 },\n  { id: \"exp-3\", label: \"Lunch\", amount: 1350 },\n];\n\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\n\nconst list = document.querySelector(\"#expense-list\");\nconst totalEl = document.querySelector(\"#total\");\n\nfunction renderExpenses(expenses) {\n  list.replaceChildren();\n  for (const expense of expenses) {\n    const li = document.createElement(\"li\");\n    li.textContent = `${expense.label}: ${formatMoney(expense.amount)}`;\n    li.dataset.id = expense.id;\n    if (expense.amount >= 10000) {\n      li.classList.add(\"big\");\n    }\n    list.append(li);\n  }\n  const total = expenses.reduce((sum, e) => sum + e.amount, 0);\n  totalEl.textContent = `Total: ${formatMoney(total)}`;\n}\n\nrenderExpenses(expenses);\n"},"checks":[{"id":"items","test":"const items = $$('#expense-list li');\nreturn items.length === 3 && items[0].textContent.trim() === 'Coffee: $4.50' && items[2].textContent.trim() === 'Lunch: $13.50';\n"},{"id":"data-id","test":"return $$('#expense-list li').map((li) => li.dataset.id).join() === 'exp-1,exp-2,exp-3';\n"},{"id":"big-class","test":"const items = $$('#expense-list li');\nreturn items.length === 3 && items[1].classList.contains('big') && !items[0].classList.contains('big') && !items[2].classList.contains('big');\n"},{"id":"total","test":"return text('#total') === 'Total: $207.00';\n"},{"id":"rerender","test":"renderExpenses([{ id: 'x', label: '<b>Tea</b>', amount: 300 }]);\nconst items = $$('#expense-list li');\nreturn items.length === 1 && items[0].textContent.trim() === '<b>Tea</b>: $3.00' && !$('#expense-list b') && text('#total') === 'Total: $3.00';\n"}],"title":"اعرض قائمة Pocket","prompt":"<p>اكتب جسم الدالة <code dir=\"ltr\">renderExpenses(expenses)</code> بحيث تعيد بناء الصفحة من البيانات:</p>\n<ol>\n<li>أفرغ <code dir=\"ltr\">#expense-list</code>.</li>\n<li>أضف <code dir=\"ltr\">&lt;li&gt;</code> لكل مصروف نصّه مثل <code dir=\"ltr\">Coffee: $4.50</code> (التسمية، ثم نقطتان، ثم المبلغ منسّقًا)، واضبط <code dir=\"ltr\">data-id</code> الخاص به على <code dir=\"ltr\">id</code> المصروف، وأضف الصنف <code dir=\"ltr\">big</code> حين يكون المبلغ 10000 سنت أو أكثر.</li>\n<li>اضبط <code dir=\"ltr\">#total</code> على <code dir=\"ltr\">Total: </code> متبوعًا بالمجموع منسّقًا.</li>\n</ol>\n<p>يجب أن يستبدل استدعاء <code dir=\"ltr\">renderExpenses</code> مرة أخرى القائمة لا أن يضيف إليها، ويجب أن تُعرض التسميات نصًا عاديًا.</p>\n","hints":["حدّد الحاويتين مرة واحدة في الأعلى: <code dir=\"ltr\">document.querySelector(&quot;#expense-list&quot;)</code> و<code dir=\"ltr\">document.querySelector(&quot;#total&quot;)</code>.","لكل مصروف: <code dir=\"ltr\">document.createElement(&quot;li&quot;)</code>، ثم اضبط <code dir=\"ltr\">textContent</code>، ثم <code dir=\"ltr\">li.dataset.id</code>، وربما <code dir=\"ltr\">li.classList.add(&quot;big&quot;)</code>، ثم <code dir=\"ltr\">list.append(li)</code>.","ابدأ الدالة بـ <code dir=\"ltr\">list.replaceChildren();</code> كي لا يكرّر العرض الثاني العناصر، واحسب المجموع باستخدام <code dir=\"ltr\">reduce</code>."],"explanation":"<p>دالة render هي المكان الوحيد الذي يقرّر شكل القائمة. والإفراغ أولًا بـ <code dir=\"ltr\">replaceChildren()</code> يعني أن استدعاءها مجددًا، بعد تغيّر البيانات، يُنتج عنصرًا واحدًا بالضبط لكل مصروف بدلًا من إضافة نسخ مكرّرة. وتُدرج <code dir=\"ltr\">textContent</code> التسمية نصًا عاديًا، فتُعرض تسمية مثل <code dir=\"ltr\">&lt;b&gt;Tea&lt;/b&gt;</code> حرفيًا بدلًا من أن تصبح عريضة (أو، مع مدخلات أخبث، أن تشغّل كودًا). والصنف <code dir=\"ltr\">big</code> يكتفي بتمييز العنصر؛ أما ملف الأنماط فيقرّر شكل العناصر المميّزة. و<code dir=\"ltr\">dataset.id</code> تكتب سمة <code dir=\"ltr\">data-id</code> ستستخدمها في الدرس التالي لتعرف إلى أيّ مصروف تعود النقرة.</p>\n","checkLabels":{"items":"عنصر <li> واحد لكل مصروف، مثل “Coffee: $4.50”","data-id":"كل عنصر يحمل معرّف مصروفه في data-id","big-class":"المصاريف التي قيمتها 10000 سنت أو أكثر فقط تحصل على الصنف “big”","total":"يعرض #total النص “Total: $207.00”","rerender":"العرض مجددًا يستبدل القائمة ويعرض التسميات نصًا عاديًا"}},"words":580,"readMinutes":3}